feat: API route POST /api/sync/execute (NDJSON streaming)

This commit is contained in:
2026-06-08 21:04:45 +02:00
parent d4b54efa17
commit 16fece8ca9
+113
View File
@@ -0,0 +1,113 @@
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import {
createShopifyProduct,
updateShopifyProduct,
deactivateShopifyProduct,
getFirstVariantId,
} from '@/lib/shopifySync'
import type { SyncChange, SyncLogEntry, SyncStreamEvent } from '@/types/sync'
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions)
if (!session?.user) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 })
}
let changes: SyncChange[]
try {
const body = await req.json()
changes = body.changes
} catch {
return NextResponse.json({ error: 'Corps JSON invalide' }, { status: 400 })
}
if (!Array.isArray(changes) || changes.length === 0) {
return NextResponse.json({ error: 'Aucun changement à appliquer' }, { status: 400 })
}
const userId = (session.user as { id: string }).id
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const send = (event: SyncStreamEvent) => {
controller.enqueue(encoder.encode(JSON.stringify(event) + '\n'))
}
const log: SyncLogEntry[] = []
let created = 0, updated = 0, deactivated = 0, errors = 0
const total = changes.filter((c) => c.type !== 'unchanged').length
let current = 0
for (const change of changes) {
if (change.type === 'unchanged') continue
current++
try {
if (change.type === 'create') {
await createShopifyProduct(change.sheetProduct!)
created++
const msg = `${change.ref} créé`
log.push({ ref: change.ref, action: 'create', status: 'success', message: msg })
send({ type: 'progress', current, total, message: msg })
} else if (change.type === 'update') {
const variantId = await getFirstVariantId(change.shopifyProduct!.shopifyId)
await updateShopifyProduct(
change.shopifyProduct!.shopifyId,
change.sheetProduct!,
variantId,
)
updated++
const fields = change.changedFields?.map((f) => f.field).join(', ') ?? ''
const msg = `${change.ref} mis à jour (${fields})`
log.push({ ref: change.ref, action: 'update', status: 'success', message: msg })
send({ type: 'progress', current, total, message: msg })
} else if (change.type === 'deactivate') {
await deactivateShopifyProduct(change.shopifyProduct!.shopifyId)
deactivated++
const msg = `${change.ref} désactivé`
log.push({ ref: change.ref, action: 'deactivate', status: 'success', message: msg })
send({ type: 'progress', current, total, message: msg })
}
} catch (err) {
errors++
const msg = `${change.ref} : ${(err as Error).message}`
log.push({ ref: change.ref, action: change.type as SyncLogEntry['action'], status: 'error', message: msg })
send({ type: 'progress', current, total, message: msg })
}
}
const syncStatus = errors === 0 ? 'success' : errors === total ? 'error' : 'partial'
try {
await prisma.syncRun.create({
data: {
userId,
status: syncStatus,
created,
updated,
deactivated,
errors,
log: JSON.stringify(log),
},
})
} catch (dbErr) {
console.error('Erreur sauvegarde SyncRun:', dbErr)
}
send({
type: 'done',
summary: { created, updated, deactivated, errors, status: syncStatus },
})
controller.close()
},
})
return new Response(stream, {
headers: { 'Content-Type': 'application/x-ndjson; charset=utf-8' },
})
}