fix(tenant-symbols): kompletter Endpoint auf Raw-SQL umgestellt
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled

This commit is contained in:
Pepe Ziberi
2026-05-21 14:58:19 +02:00
parent c291431fd7
commit 93d5519e58

View File

@@ -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 })
}