From 93d5519e588c6fd606c1e98d9b50bfaf4f0f3e0c Mon Sep 17 00:00:00 2001 From: Pepe Ziberi Date: Thu, 21 May 2026 14:58:19 +0200 Subject: [PATCH] fix(tenant-symbols): kompletter Endpoint auf Raw-SQL umgestellt --- src/app/api/tenant/symbols/route.ts | 228 +++++++++++++--------------- 1 file changed, 105 insertions(+), 123 deletions(-) diff --git a/src/app/api/tenant/symbols/route.ts b/src/app/api/tenant/symbols/route.ts index 20d17d0..0e59d84 100644 --- a/src/app/api/tenant/symbols/route.ts +++ b/src/app/api/tenant/symbols/route.ts @@ -13,13 +13,40 @@ async function getTenantId() { return { tenantId: user.tenantId } } +async function ensureTables() { + await prisma.$executeRawUnsafe(` + CREATE TABLE IF NOT EXISTS tenant_symbols ( + id TEXT PRIMARY KEY DEFAULT gen_random_uuid(), + "isActive" BOOLEAN NOT NULL DEFAULT true, + "tenantId" TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + "iconId" TEXT REFERENCES icon_assets(id) ON DELETE SET NULL, + "name" TEXT, + "svgPath" TEXT, + "isUploaded" BOOLEAN NOT NULL DEFAULT false, + "categoryId" TEXT, + "migratedFromIconId" TEXT, + "customName" TEXT, + "sortOrder" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + `) + await prisma.$executeRawUnsafe(` + CREATE TABLE IF NOT EXISTS tenant_categories ( + id TEXT PRIMARY KEY DEFAULT gen_random_uuid(), + "name" TEXT NOT NULL, + "description" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "tenantId" TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE + ) + `) +} + // ─── GET ──────────────────────────────────────────────────────────────────── -/** - * GET /api/tenant/symbols - * Liefert die Symbole des Mandanten gruppiert nach TenantCategory. - * Optional: ?grouped=true (default) oder ?grouped=false (flache Liste) - */ export async function GET(req: NextRequest) { try { const auth = await getTenantId() @@ -29,22 +56,16 @@ export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url) const grouped = searchParams.get('grouped') !== 'false' - let tenantSymbols: any[] = [] - try { - tenantSymbols = await (prisma as any).tenantSymbol.findMany({ - where: { tenantId }, - include: { - category: { select: { id: true, name: true, sortOrder: true } }, - }, - orderBy: [{ category: { sortOrder: 'asc' } }, { sortOrder: 'asc' }, { name: 'asc' }], - }) - } catch (dbErr: any) { - console.error('tenantSymbol.findMany failed:', dbErr) - if (dbErr?.message?.includes('tenant_symbols') || dbErr?.code === 'P2021') { - return NextResponse.json({ categories: [], symbols: [] }) - } - throw dbErr - } + await ensureTables() + + const tenantSymbols: any[] = await prisma.$queryRawUnsafe( + `SELECT ts.*, tc.id as "catId", tc.name as "catName", tc."sortOrder" as "catSort" + FROM tenant_symbols ts + LEFT JOIN tenant_categories tc ON ts."categoryId" = tc.id + WHERE ts."tenantId" = $1 AND ts."isActive" = true + ORDER BY COALESCE(tc."sortOrder", 9999) ASC, COALESCE(ts."sortOrder", 9999) ASC, COALESCE(ts.name, ts."customName", 'Unbenannt') ASC`, + tenantId + ) const mapped = tenantSymbols.map((ts: any) => ({ id: ts.id, @@ -52,7 +73,7 @@ export async function GET(req: NextRequest) { customName: ts.customName, svgPath: ts.svgPath, categoryId: ts.categoryId, - categoryName: ts.category?.name || 'Ohne Kategorie', + categoryName: ts.catName || 'Ohne Kategorie', sortOrder: ts.sortOrder, isUploaded: ts.isUploaded, createdAt: ts.createdAt, @@ -70,21 +91,11 @@ export async function GET(req: NextRequest) { groupedResult[catName].push(sym) } - // Sort categories by sortOrder from category - let categories: any[] = [] - try { - categories = await (prisma as any).tenantCategory.findMany({ - where: { tenantId }, - orderBy: { sortOrder: 'asc' }, - }) - } catch (catErr: any) { - console.error('tenantCategory.findMany failed:', catErr) - if (catErr?.message?.includes('tenant_categories') || catErr?.code === 'P2021') { - categories = [] - } else { - throw catErr - } - } + // Get categories for ordering + const categories: any[] = await prisma.$queryRawUnsafe( + `SELECT * FROM tenant_categories WHERE "tenantId" = $1 AND "isActive" = true ORDER BY "sortOrder" ASC`, + tenantId + ) const ordered: Array<{ categoryId: string | null; categoryName: string; symbols: any[] }> = [] for (const cat of categories) { @@ -107,27 +118,23 @@ export async function GET(req: NextRequest) { // ─── POST ─────────────────────────────────────────────────────────────────── -/** - * POST /api/tenant/symbols - * Erstellt ein neues TenantSymbol (manuell oder aus Template/IconAsset). - * - * Body (manuell aus IconAsset): - * { iconId: string, customName?: string, categoryId?: string } - * - * Body (aus Template-Import): - * { templateId: string, categoryId?: string } - * - * Body (manueller Upload): - * FormData mit: file (SVG/PNG), name, categoryId - */ export async function POST(req: NextRequest) { try { const auth = await getTenantId() if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) const { tenantId } = auth + await ensureTables() + const contentType = req.headers.get('content-type') || '' + // Get next sort order + const maxSort = await prisma.$queryRawUnsafe( + `SELECT MAX("sortOrder") as maxsort FROM tenant_symbols WHERE "tenantId" = $1`, + tenantId + ) as any[] + const nextSort = (maxSort[0]?.maxsort ?? -1) + 1 + // ─── File Upload ───────────────────────────────────────────────────────── if (contentType.includes('multipart/form-data')) { const formData = await req.formData() @@ -139,11 +146,6 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'Datei und Name erforderlich' }, { status: 400 }) } - const maxSortAgg = await (prisma as any).tenantSymbol.aggregate({ - where: { tenantId }, - _max: { sortOrder: true }, - }) - const bytes = await file.arrayBuffer() const buffer = Buffer.from(bytes) const ext = file.name.split('.').pop()?.toLowerCase() || 'svg' @@ -152,16 +154,13 @@ export async function POST(req: NextRequest) { await uploadFile(fileKey, buffer, mimeType) - const symbol = await (prisma as any).tenantSymbol.create({ - data: { - tenantId, - name: name.trim(), - svgPath: fileKey, - categoryId: categoryId || null, - sortOrder: (maxSortAgg._max.sortOrder ?? -1) + 1, - isUploaded: true, - }, - }) + const result = await prisma.$queryRawUnsafe( + `INSERT INTO tenant_symbols (id, "tenantId", name, "svgPath", "categoryId", "sortOrder", "isUploaded", "createdAt", "updatedAt") + VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, true, NOW(), NOW()) + RETURNING *`, + tenantId, name.trim(), fileKey, categoryId || null, nextSort + ) as any[] + const symbol = result[0] return NextResponse.json({ id: symbol.id, @@ -177,31 +176,24 @@ export async function POST(req: NextRequest) { const body = await req.json() const { iconId, customName, templateId, categoryId } = body - const maxSortAgg = await (prisma as any).tenantSymbol.aggregate({ - where: { tenantId }, - _max: { sortOrder: true }, - }) - const nextSort = (maxSortAgg._max.sortOrder ?? -1) + 1 - // From SymbolTemplate if (templateId) { - const tpl = await (prisma as any).symbolTemplate.findUnique({ - where: { id: templateId }, - }) - if (!tpl) { + const tpl = await prisma.$queryRawUnsafe( + `SELECT * FROM symbol_templates WHERE id = $1 LIMIT 1`, + templateId + ) as any[] + if (!tpl || tpl.length === 0) { return NextResponse.json({ error: 'Template nicht gefunden' }, { status: 404 }) } + const t = tpl[0] - const symbol = await (prisma as any).tenantSymbol.create({ - data: { - tenantId, - name: tpl.name, - svgPath: tpl.svgPath, - categoryId: categoryId || null, - sortOrder: nextSort, - isUploaded: false, - }, - }) + const result = await prisma.$queryRawUnsafe( + `INSERT INTO tenant_symbols (id, "tenantId", name, "svgPath", "categoryId", "sortOrder", "isUploaded", "createdAt", "updatedAt") + VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, false, NOW(), NOW()) + RETURNING *`, + tenantId, t.name, t.svgPath, categoryId || null, nextSort + ) as any[] + const symbol = result[0] return NextResponse.json({ id: symbol.id, @@ -226,18 +218,13 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'Icon nicht gefunden' }, { status: 404 }) } - const symbol = await (prisma as any).tenantSymbol.create({ - data: { - tenantId, - iconId: icon.id, - customName: customName || null, - name: customName || icon.name, - svgPath: icon.fileKey, - categoryId: categoryId || null, - sortOrder: nextSort, - isUploaded: false, - }, - }) + const result = await prisma.$queryRawUnsafe( + `INSERT INTO tenant_symbols (id, "tenantId", "iconId", "customName", name, "svgPath", "categoryId", "sortOrder", "isUploaded", "createdAt", "updatedAt") + VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, false, NOW(), NOW()) + RETURNING *`, + tenantId, icon.id, customName || null, customName || icon.name, icon.fileKey, categoryId || null, nextSort + ) as any[] + const symbol = result[0] return NextResponse.json({ id: symbol.id, @@ -257,12 +244,6 @@ export async function POST(req: NextRequest) { // ─── PATCH ────────────────────────────────────────────────────────────────── -/** - * PATCH /api/tenant/symbols - * Aktualisiert ein TenantSymbol. - * - * Body: { id: string, name?: string, customName?: string, categoryId?: string, sortOrder?: number } - */ export async function PATCH(req: NextRequest) { try { const auth = await getTenantId() @@ -272,18 +253,22 @@ export async function PATCH(req: NextRequest) { const { id, name, customName, categoryId, sortOrder } = await req.json() if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 }) - const data: any = {} - if (name !== undefined) data.name = name || null - if (customName !== undefined) data.customName = customName || null - if (categoryId !== undefined) data.categoryId = categoryId || null - if (sortOrder !== undefined) data.sortOrder = sortOrder + await ensureTables() - const result = await (prisma as any).tenantSymbol.updateMany({ - where: { id, tenantId }, - data, - }) + const updates: string[] = [] + const params: any[] = [id, tenantId] + if (name !== undefined) { updates.push(`name = $${params.length + 1}`); params.push(name || null); } + if (customName !== undefined) { updates.push(`"customName" = $${params.length + 1}`); params.push(customName || null); } + if (categoryId !== undefined) { updates.push(`"categoryId" = $${params.length + 1}`); params.push(categoryId || null); } + if (sortOrder !== undefined) { updates.push(`"sortOrder" = $${params.length + 1}`); params.push(sortOrder); } + updates.push(`"updatedAt" = NOW()`) - if (result.count === 0) { + const result = await prisma.$queryRawUnsafe( + `UPDATE tenant_symbols SET ${updates.join(', ')} WHERE id = $1 AND "tenantId" = $2 RETURNING *`, + ...params + ) as any[] + + if (!result || result.length === 0) { return NextResponse.json({ error: 'Symbol nicht gefunden' }, { status: 404 }) } @@ -296,12 +281,6 @@ export async function PATCH(req: NextRequest) { // ─── DELETE ───────────────────────────────────────────────────────────────── -/** - * DELETE /api/tenant/symbols - * Löscht ein TenantSymbol. - * - * Body: { id: string } - */ export async function DELETE(req: NextRequest) { try { const auth = await getTenantId() @@ -311,11 +290,14 @@ export async function DELETE(req: NextRequest) { const { id } = await req.json() if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 }) - const result = await (prisma as any).tenantSymbol.deleteMany({ - where: { id, tenantId }, - }) + await ensureTables() - if (result.count === 0) { + const result = await prisma.$queryRawUnsafe( + `DELETE FROM tenant_symbols WHERE id = $1 AND "tenantId" = $2 RETURNING *`, + id, tenantId + ) as any[] + + if (!result || result.length === 0) { return NextResponse.json({ error: 'Symbol nicht gefunden' }, { status: 404 }) }