feat(plan5): API POST /api/creation/execute streaming NDJSON

This commit is contained in:
2026-06-09 10:09:27 +02:00
parent 015833150a
commit 40d537efb6
+107
View File
@@ -0,0 +1,107 @@
import { NextRequest } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { fetchPendingProducts } from '@/lib/creationSheets'
import { generateDescriptions } from '@/lib/openaiClient'
import { ensureMetafieldDefinition, setProductMetafield } from '@/lib/shopifyMetafields'
import { addProductToCollection } from '@/lib/shopifyCollections'
import { createShopifyProduct } from '@/lib/shopifySync'
import { prisma } from '@/lib/prisma'
import type { MetafieldResolution, CreationStreamEvent } from '@/types/creation'
function encode(event: CreationStreamEvent): string {
return JSON.stringify(event) + '\n'
}
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions)
if (!session) {
return new Response(JSON.stringify({ error: 'Non authentifié' }), { status: 401 })
}
const userId = (session.user as { id: string }).id
const body = await req.json().catch(() => ({})) as { resolutions?: MetafieldResolution[] }
const resolutions: MetafieldResolution[] = body.resolutions ?? []
const resolutionMap = new Map<string, MetafieldResolution>(resolutions.map(r => [r.columnHeader, r]))
const stream = new ReadableStream({
async start(controller) {
const send = (event: CreationStreamEvent) => controller.enqueue(encode(event))
try {
const existing = await prisma.productCreation.findMany({ select: { sheetTab: true, sheetRow: true } })
const alreadyCreated = new Set(existing.map(e => `${e.sheetTab}:${e.sheetRow}`))
const pending = await fetchPendingProducts(alreadyCreated)
const total = pending.length
if (total === 0) {
send({ type: 'done', summary: { created: 0, errors: 0, status: 'success' } })
controller.close()
return
}
// Crée les définitions manquantes en amont
for (const resolution of resolutions) {
if (resolution.action === 'create') await ensureMetafieldDefinition(resolution)
}
let created = 0
let errors = 0
for (let i = 0; i < pending.length; i++) {
const product = pending[i]
send({ type: 'progress', current: i + 1, total, tab: product.tab, title: product.title, step: 'Génération descriptions…' })
try {
const { longDesc } = await generateDescriptions({
title: product.title,
rawDescription: '',
comment: product.comment,
vendor: product.vendor,
famille: product.famille,
})
send({ type: 'progress', current: i + 1, total, tab: product.tab, title: product.title, step: 'Création produit Shopify…' })
const shopifyId = await createShopifyProduct({
ref: product.sku,
handle: product.sku.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
title: product.title,
description: longDesc,
price: product.priceActual || '0.00',
stock: product.stock,
status: 'active',
})
if (product.collectionId) await addProductToCollection(shopifyId, product.collectionId)
if (product.subCollectionId) await addProductToCollection(shopifyId, product.subCollectionId)
send({ type: 'progress', current: i + 1, total, tab: product.tab, title: product.title, step: 'Métachamps…' })
for (const [colHeader, value] of Object.entries(product.specificFields)) {
const resolution = resolutionMap.get(colHeader)
if (!resolution || resolution.action === 'skip') continue
await setProductMetafield(shopifyId, resolution, value)
}
await prisma.productCreation.create({
data: { userId, sheetTab: product.tab, sheetRow: product.rowNumber, shopifyId, sku: product.sku, title: product.title, status: 'success' },
})
created++
} catch (err) {
const message = err instanceof Error ? err.message : 'Erreur inconnue'
send({ type: 'error', title: product.title, message })
await prisma.productCreation.create({
data: { userId, sheetTab: product.tab, sheetRow: product.rowNumber, shopifyId: '', sku: product.sku, title: product.title, status: 'error', errorMessage: message },
})
errors++
}
}
const status = errors === 0 ? 'success' : created === 0 ? 'error' : 'partial'
send({ type: 'done', summary: { created, errors, status } })
} catch (err) {
const message = err instanceof Error ? err.message : 'Erreur inconnue'
send({ type: 'done', summary: { created: 0, errors: 1, status: 'error' } })
console.error('[creation/execute]', message)
} finally {
controller.close()
}
},
})
return new Response(stream, {
headers: { 'Content-Type': 'application/x-ndjson', 'Transfer-Encoding': 'chunked' },
})
}