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