fix(auto-migrate): tenant_categories on-the-fly erstellung + migrate.js fail-fast
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 10m41s

This commit is contained in:
Pepe Ziberi
2026-05-21 13:40:34 +02:00
parent 3722a04091
commit 3606c9a2a4
3 changed files with 202 additions and 35 deletions

View File

@@ -212,7 +212,7 @@ async function migrate() {
console.log(' Privacy consent columns skipped:', e.message)
}
// ─── Step 12: Create tenant_symbols table ───
// ─── Step 12: Create tenant_symbols table (CRITICAL — fail-fast) ───
console.log(' [12] Creating tenant_symbols table...')
try {
await prisma.$executeRawUnsafe(`
@@ -226,7 +226,8 @@ async function migrate() {
`)
console.log(' tenant_symbols table created (or already exists)')
} catch (e) {
console.log(' tenant_symbols table skipped:', e.message)
console.error(' ❌ CRITICAL: tenant_symbols table creation failed:', e.message)
throw e
}
// ─── Step 13: Create symbol_templates table ───
@@ -251,7 +252,7 @@ async function migrate() {
console.log(' symbol_templates table skipped:', e.message)
}
// ─── Step 14: Create tenant_categories table ───
// ─── Step 14: Create tenant_categories table (CRITICAL — fail-fast) ───
console.log(' [14] Creating tenant_categories table...')
try {
await prisma.$executeRawUnsafe(`
@@ -268,7 +269,20 @@ async function migrate() {
`)
console.log(' tenant_categories table created (or already exists)')
} catch (e) {
console.log(' tenant_categories table skipped:', e.message)
console.error(' ❌ CRITICAL: tenant_categories table creation failed:', e.message)
throw e
}
// ─── Step 14b: Ensure tenant_categories has NOT NULL defaults ───
console.log(' [14b] Ensuring tenant_categories columns...')
const tcColumns = [
`ALTER TABLE tenant_categories ALTER COLUMN "isActive" SET DEFAULT true`,
`ALTER TABLE tenant_categories ALTER COLUMN "sortOrder" SET DEFAULT 0`,
`ALTER TABLE tenant_categories ALTER COLUMN "createdAt" SET DEFAULT CURRENT_TIMESTAMP`,
`ALTER TABLE tenant_categories ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP`,
]
for (const sql of tcColumns) {
try { await prisma.$executeRawUnsafe(sql) } catch (e) { /* ignore */ }
}
// ─── Step 15: Extend tenant_symbols with Phase 1 columns ───

View File

@@ -1,6 +1,11 @@
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()
@@ -37,10 +42,25 @@ export async function GET() {
})
} catch (dbErr: any) {
console.error('tenantCategory.findMany failed:', dbErr)
if (dbErr?.message?.includes('tenant_categories') || dbErr?.code === 'P2021') {
return NextResponse.json({ categories: [] })
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
}
throw dbErr
}
const result = categories.map((cat: any) => ({
@@ -79,19 +99,23 @@ export async function POST(req: NextRequest) {
}
// Check for duplicate name within tenant
let existing: any = null
try {
const existing = await (prisma as any).tenantCategory.findFirst({
existing = await (prisma as any).tenantCategory.findFirst({
where: { tenantId, name: { equals: name, mode: 'insensitive' } },
})
if (existing) {
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
}
} catch (dbErr: any) {
console.error('tenantCategory.findFirst failed:', dbErr)
if (dbErr?.message?.includes('tenant_categories') || dbErr?.code === 'P2021') {
return NextResponse.json({ error: 'Datenbank nicht bereit Migration läuft möglicherweise noch' }, { status: 503 })
if (isMissingTable(dbErr)) {
await ensureTenantCategoriesTable()
existing = await (prisma as any).tenantCategory.findFirst({
where: { tenantId, name: { equals: name, mode: 'insensitive' } },
})
} else {
throw dbErr
}
throw dbErr
}
if (existing) {
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
}
try {
@@ -105,9 +129,17 @@ export async function POST(req: NextRequest) {
})
return NextResponse.json({ category }, { status: 201 })
} catch (dbErr: any) {
console.error('tenantCategory.create failed:', dbErr)
if (dbErr?.message?.includes('tenant_categories') || dbErr?.code === 'P2021') {
return NextResponse.json({ error: 'Datenbank nicht bereit Migration läuft möglicherweise noch' }, { status: 503 })
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
}
@@ -141,23 +173,31 @@ export async function PATCH(req: NextRequest) {
// If renaming, check for duplicates
if (name) {
let existing: any = null
try {
const existing = await (prisma as any).tenantCategory.findFirst({
existing = await (prisma as any).tenantCategory.findFirst({
where: {
tenantId,
name: { equals: name.trim(), mode: 'insensitive' },
id: { not: id },
},
})
if (existing) {
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
}
} catch (dbErr: any) {
console.error('tenantCategory.findFirst failed:', dbErr)
if (dbErr?.message?.includes('tenant_categories') || dbErr?.code === 'P2021') {
return NextResponse.json({ error: 'Datenbank nicht bereit Migration läuft möglicherweise noch' }, { status: 503 })
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
}
throw dbErr
}
if (existing) {
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
}
}
@@ -171,11 +211,18 @@ export async function PATCH(req: NextRequest) {
return NextResponse.json({ error: 'Kategorie nicht gefunden' }, { status: 404 })
}
} catch (dbErr: any) {
console.error('tenantCategory.updateMany failed:', dbErr)
if (dbErr?.message?.includes('tenant_categories') || dbErr?.code === 'P2021') {
return NextResponse.json({ error: 'Datenbank nicht bereit Migration läuft möglicherweise noch' }, { status: 503 })
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
}
throw dbErr
}
return NextResponse.json({ success: true })
@@ -214,7 +261,6 @@ export async function DELETE(req: NextRequest) {
)
}
} catch (dbErr: any) {
console.error('tenantSymbol.count failed:', dbErr)
if (dbErr?.message?.includes('tenant_symbols') || dbErr?.code === 'P2021') {
// Skip check if table doesn't exist
} else {
@@ -227,11 +273,14 @@ export async function DELETE(req: NextRequest) {
where: { id, tenantId },
})
} catch (dbErr: any) {
console.error('tenantCategory.deleteMany failed:', dbErr)
if (dbErr?.message?.includes('tenant_categories') || dbErr?.code === 'P2021') {
return NextResponse.json({ error: 'Datenbank nicht bereit Migration läuft möglicherweise noch' }, { status: 503 })
if (isMissingTable(dbErr)) {
await ensureTenantCategoriesTable()
await (prisma as any).tenantCategory.deleteMany({
where: { id, tenantId },
})
} else {
throw dbErr
}
throw dbErr
}
return NextResponse.json({ success: true })

104
src/lib/auto-migrate.ts Normal file
View File

@@ -0,0 +1,104 @@
/**
* Auto-migrate helper: ensures critical tables exist on-the-fly.
* Called from API endpoints when a table-missing error is detected.
*/
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function ensureTenantCategoriesTable() {
try {
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
)
`)
console.log('[auto-migrate] tenant_categories ensured')
} catch (e: any) {
if (e.message?.includes('already exists')) return
throw e
}
}
export async function ensureTenantSymbolsTable() {
try {
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,
UNIQUE("tenantId", "iconId")
)
`)
console.log('[auto-migrate] tenant_symbols ensured')
} catch (e: any) {
if (e.message?.includes('already exists')) return
throw e
}
}
export async function ensureTenantSymbolsColumns() {
const columns = [
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "name" TEXT`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "svgPath" TEXT`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "isUploaded" BOOLEAN NOT NULL DEFAULT false`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "categoryId" TEXT`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "migratedFromIconId" TEXT`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP`,
`ALTER TABLE tenant_symbols ADD COLUMN IF NOT EXISTS "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP`,
]
for (const sql of columns) {
try { await prisma.$executeRawUnsafe(sql) } catch (e) { /* ignore */ }
}
}
export async function ensureTenantSymbolsCategoryFk() {
try {
await prisma.$executeRawUnsafe(`
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'tenant_symbols_categoryId_fkey'
AND table_name = 'tenant_symbols'
) THEN
ALTER TABLE tenant_symbols
ADD CONSTRAINT "tenant_symbols_categoryId_fkey"
FOREIGN KEY ("categoryId") REFERENCES tenant_categories(id) ON DELETE SET NULL;
END IF;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'categoryId FK skipped: %', SQLERRM;
END $$;
`)
} catch (e) { /* ignore */ }
}
export async function ensureSymbolTemplatesTable() {
try {
await prisma.$executeRawUnsafe(`
CREATE TABLE IF NOT EXISTS symbol_templates (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
"fileKey" TEXT NOT NULL,
"originalFilename" TEXT NOT NULL,
"displayName" TEXT,
"categoryName" TEXT,
"svgPath" TEXT,
"metadata" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE("fileKey")
)
`)
console.log('[auto-migrate] symbol_templates ensured')
} catch (e: any) {
if (e.message?.includes('already exists')) return
throw e
}
}