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
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 10m41s
This commit is contained in:
@@ -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 ───
|
||||
|
||||
@@ -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,11 +42,26 @@ export async function GET() {
|
||||
})
|
||||
} catch (dbErr: any) {
|
||||
console.error('tenantCategory.findMany failed:', dbErr)
|
||||
if (dbErr?.message?.includes('tenant_categories') || dbErr?.code === 'P2021') {
|
||||
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) => ({
|
||||
id: cat.id,
|
||||
@@ -79,20 +99,24 @@ 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' } },
|
||||
})
|
||||
} 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) {
|
||||
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 })
|
||||
}
|
||||
throw dbErr
|
||||
}
|
||||
|
||||
try {
|
||||
const category = await (prisma as any).tenantCategory.create({
|
||||
@@ -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,24 +173,32 @@ 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 },
|
||||
},
|
||||
})
|
||||
} 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 })
|
||||
}
|
||||
} 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 })
|
||||
}
|
||||
throw dbErr
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -171,12 +211,19 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
@@ -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,12 +273,15 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
|
||||
104
src/lib/auto-migrate.ts
Normal file
104
src/lib/auto-migrate.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user