fix(categories): kompletter Endpoint auf Raw-SQL umgestellt – unabhängig von Prisma-Client-Modellen
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 10m51s

This commit is contained in:
Pepe Ziberi
2026-05-21 14:31:54 +02:00
parent 3606c9a2a4
commit c291431fd7

View File

@@ -1,11 +1,6 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db' import { prisma } from '@/lib/db'
import { getSession } from '@/lib/auth' import { getSession } from '@/lib/auth'
import { ensureTenantCategoriesTable } from '@/lib/auto-migrate'
function isMissingTable(err: any) {
return err?.message?.includes('tenant_categories') || err?.code === 'P2021'
}
async function getTenantId() { async function getTenantId() {
const user = await getSession() const user = await getSession()
@@ -17,62 +12,37 @@ async function getTenantId() {
return { tenantId: user.tenantId } return { tenantId: user.tenantId }
} }
async function ensureTable() {
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 ────────────────────────────────────────────────────────────────────
/**
* GET /api/tenant/categories
* Liefert alle Tenant-Kategorien des aktuellen Mandanten mit Symbol-Zählung.
*/
export async function GET() { export async function GET() {
try { try {
const auth = await getTenantId() const auth = await getTenantId()
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const { tenantId } = auth const { tenantId } = auth
let categories: any[] = [] await ensureTable()
try {
categories = await (prisma as any).tenantCategory.findMany({
where: { tenantId },
include: {
_count: {
select: { symbols: true },
},
},
orderBy: { sortOrder: 'asc' },
})
} catch (dbErr: any) {
console.error('tenantCategory.findMany failed:', dbErr)
if (isMissingTable(dbErr)) {
try {
await ensureTenantCategoriesTable()
categories = await (prisma as any).tenantCategory.findMany({
where: { tenantId },
include: {
_count: {
select: { symbols: true },
},
},
orderBy: { sortOrder: 'asc' },
})
} catch (retryErr) {
console.error('Retry after auto-migrate failed:', retryErr)
return NextResponse.json({ categories: [] })
}
} else {
throw dbErr
}
}
const result = categories.map((cat: any) => ({ const categories: any[] = await prisma.$queryRawUnsafe(
id: cat.id, `SELECT * FROM tenant_categories WHERE "tenantId" = $1 AND "isActive" = true ORDER BY "sortOrder" ASC`,
name: cat.name, tenantId
sortOrder: cat.sortOrder, )
icon: cat.icon,
symbolCount: cat._count?.symbols ?? 0,
createdAt: cat.createdAt,
}))
return NextResponse.json({ categories: result }) return NextResponse.json({ categories })
} catch (error) { } catch (error) {
console.error('Error fetching tenant categories:', error) console.error('Error fetching tenant categories:', error)
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 }) return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
@@ -81,68 +51,36 @@ export async function GET() {
// ─── POST ─────────────────────────────────────────────────────────────────── // ─── POST ───────────────────────────────────────────────────────────────────
/**
* POST /api/tenant/categories
* Legt eine neue Kategorie an.
*
* Body: { name: string, sortOrder?: number, icon?: string }
*/
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const auth = await getTenantId() const auth = await getTenantId()
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const { tenantId } = auth const { tenantId } = auth
const { name, sortOrder, icon } = await req.json() const { name, sortOrder } = await req.json()
if (!name || typeof name !== 'string') { if (!name || typeof name !== 'string') {
return NextResponse.json({ error: 'Name erforderlich' }, { status: 400 }) return NextResponse.json({ error: 'Name erforderlich' }, { status: 400 })
} }
// Check for duplicate name within tenant await ensureTable()
let existing: any = null
try { // Check for duplicate
existing = await (prisma as any).tenantCategory.findFirst({ const existing = await prisma.$queryRawUnsafe(
where: { tenantId, name: { equals: name, mode: 'insensitive' } }, `SELECT id FROM tenant_categories WHERE "tenantId" = $1 AND LOWER(name) = LOWER($2) LIMIT 1`,
}) tenantId, name.trim()
} catch (dbErr: any) { ) as any[]
if (isMissingTable(dbErr)) { if (existing && existing.length > 0) {
await ensureTenantCategoriesTable()
existing = await (prisma as any).tenantCategory.findFirst({
where: { tenantId, name: { equals: name, mode: 'insensitive' } },
})
} else {
throw dbErr
}
}
if (existing) {
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 }) return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
} }
try { const result = await prisma.$queryRawUnsafe(
const category = await (prisma as any).tenantCategory.create({ `INSERT INTO tenant_categories (id, "tenantId", name, "sortOrder", "createdAt", "updatedAt")
data: { VALUES (gen_random_uuid(), $1, $2, $3, NOW(), NOW())
tenantId, RETURNING *`,
name: name.trim(), tenantId, name.trim(), sortOrder ?? 0
sortOrder: sortOrder ?? 0, ) as any[]
icon: icon || null,
}, return NextResponse.json({ category: result[0] }, { status: 201 })
})
return NextResponse.json({ category }, { status: 201 })
} catch (dbErr: any) {
if (isMissingTable(dbErr)) {
await ensureTenantCategoriesTable()
const category = await (prisma as any).tenantCategory.create({
data: {
tenantId,
name: name.trim(),
sortOrder: sortOrder ?? 0,
icon: icon || null,
},
})
return NextResponse.json({ category }, { status: 201 })
}
throw dbErr
}
} catch (error) { } catch (error) {
console.error('Error creating tenant category:', error) console.error('Error creating tenant category:', error)
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 }) return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
@@ -151,78 +89,41 @@ export async function POST(req: NextRequest) {
// ─── PATCH ────────────────────────────────────────────────────────────────── // ─── PATCH ──────────────────────────────────────────────────────────────────
/**
* PATCH /api/tenant/categories
* Aktualisiert eine Kategorie.
*
* Body: { id: string, name?: string, sortOrder?: number, icon?: string }
*/
export async function PATCH(req: NextRequest) { export async function PATCH(req: NextRequest) {
try { try {
const auth = await getTenantId() const auth = await getTenantId()
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const { tenantId } = auth const { tenantId } = auth
const { id, name, sortOrder, icon } = await req.json() const { id, name, sortOrder } = await req.json()
if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 }) if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 })
const data: any = {} await ensureTable()
if (name !== undefined) data.name = name.trim()
if (sortOrder !== undefined) data.sortOrder = sortOrder
if (icon !== undefined) data.icon = icon || null
// If renaming, check for duplicates // If renaming, check for duplicates
if (name) { if (name) {
let existing: any = null const existing = await prisma.$queryRawUnsafe(
try { `SELECT id FROM tenant_categories WHERE "tenantId" = $1 AND LOWER(name) = LOWER($2) AND id != $3 LIMIT 1`,
existing = await (prisma as any).tenantCategory.findFirst({ tenantId, name.trim(), id
where: { ) as any[]
tenantId, if (existing && existing.length > 0) {
name: { equals: name.trim(), mode: 'insensitive' },
id: { not: id },
},
})
} catch (dbErr: any) {
if (isMissingTable(dbErr)) {
await ensureTenantCategoriesTable()
existing = await (prisma as any).tenantCategory.findFirst({
where: {
tenantId,
name: { equals: name.trim(), mode: 'insensitive' },
id: { not: id },
},
})
} else {
throw dbErr
}
}
if (existing) {
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 }) return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
} }
} }
try { const updates: string[] = []
const category = await (prisma as any).tenantCategory.updateMany({ const params: any[] = [id, tenantId]
where: { id, tenantId }, if (name !== undefined) { updates.push(`name = $${params.length + 1}`); params.push(name.trim()); }
data, if (sortOrder !== undefined) { updates.push(`"sortOrder" = $${params.length + 1}`); params.push(sortOrder); }
}) updates.push(`"updatedAt" = NOW()`)
if (category.count === 0) { const result = await prisma.$queryRawUnsafe(
return NextResponse.json({ error: 'Kategorie nicht gefunden' }, { status: 404 }) `UPDATE tenant_categories SET ${updates.join(', ')} WHERE id = $1 AND "tenantId" = $2 RETURNING *`,
} ...params
} catch (dbErr: any) { ) as any[]
if (isMissingTable(dbErr)) {
await ensureTenantCategoriesTable() if (!result || result.length === 0) {
const category = await (prisma as any).tenantCategory.updateMany({ return NextResponse.json({ error: 'Kategorie nicht gefunden' }, { status: 404 })
where: { id, tenantId },
data,
})
if (category.count === 0) {
return NextResponse.json({ error: 'Kategorie nicht gefunden' }, { status: 404 })
}
} else {
throw dbErr
}
} }
return NextResponse.json({ success: true }) return NextResponse.json({ success: true })
@@ -234,12 +135,6 @@ export async function PATCH(req: NextRequest) {
// ─── DELETE ───────────────────────────────────────────────────────────────── // ─── DELETE ─────────────────────────────────────────────────────────────────
/**
* DELETE /api/tenant/categories
* Löscht eine Kategorie (nur wenn leer).
*
* Body: { id: string }
*/
export async function DELETE(req: NextRequest) { export async function DELETE(req: NextRequest) {
try { try {
const auth = await getTenantId() const auth = await getTenantId()
@@ -249,39 +144,24 @@ export async function DELETE(req: NextRequest) {
const { id } = await req.json() const { id } = await req.json()
if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 }) if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 })
await ensureTable()
// Check if category has symbols // Check if category has symbols
try { const symbolCount = await prisma.$queryRawUnsafe(
const symbolCount = await (prisma as any).tenantSymbol.count({ `SELECT COUNT(*)::int as cnt FROM tenant_symbols WHERE "categoryId" = $1 AND "tenantId" = $2`,
where: { categoryId: id, tenantId }, id, tenantId
}) ) as any[]
if (symbolCount > 0) { if (symbolCount && symbolCount[0]?.cnt > 0) {
return NextResponse.json( return NextResponse.json(
{ error: `Kategorie enthält ${symbolCount} Symbol(e) — bitte zuerst verschieben oder löschen` }, { error: `Kategorie enthält ${symbolCount[0].cnt} Symbol(e) — bitte zuerst verschieben oder löschen` },
{ status: 409 } { status: 409 }
) )
}
} catch (dbErr: any) {
if (dbErr?.message?.includes('tenant_symbols') || dbErr?.code === 'P2021') {
// Skip check if table doesn't exist
} else {
throw dbErr
}
} }
try { await prisma.$queryRawUnsafe(
await (prisma as any).tenantCategory.deleteMany({ `DELETE FROM tenant_categories WHERE id = $1 AND "tenantId" = $2`,
where: { id, tenantId }, id, tenantId
}) )
} catch (dbErr: any) {
if (isMissingTable(dbErr)) {
await ensureTenantCategoriesTable()
await (prisma as any).tenantCategory.deleteMany({
where: { id, tenantId },
})
} else {
throw dbErr
}
}
return NextResponse.json({ success: true }) return NextResponse.json({ success: true })
} catch (error) { } catch (error) {