feat(plan5): API POST /api/creation/execute streaming NDJSON
This commit is contained in:
@@ -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' },
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user