Files
gutscheintracker/lib/db-migrate.ts
T

126 lines
7.2 KiB
TypeScript

import { getDb } from "./db";
/**
* Stellt additive Schema-Änderungen sicher, ohne dass auf dem Server ein
* separater `prisma migrate`-Lauf nötig ist. Idempotent: fehlende Tabellen
* und Spalten werden genau einmal ergänzt, alles andere bleibt unangetastet.
* Best-effort: Fehler werden geloggt, der Start läuft trotzdem weiter.
*/
export async function ensureSchema(): Promise<void> {
try {
const db = getDb();
const tables = (await db.$queryRaw<{ name: string }[]>`SELECT name FROM sqlite_master WHERE type='table'`).map(
(t) => t.name
);
if (!tables.includes("AppSetting")) {
await db.$executeRaw`CREATE TABLE "AppSetting" ("key" TEXT NOT NULL PRIMARY KEY, "value" TEXT NOT NULL, "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP)`;
}
if (tables.includes("HouseholdMember")) {
const legacyCols = (await db.$queryRaw<{ name: string }[]>`SELECT name FROM pragma_table_info('HouseholdMember')`).map(
(c: { name: string }) => c.name
);
if (legacyCols.includes("name") && !legacyCols.includes("householdId")) {
await db.$executeRaw`ALTER TABLE "HouseholdMember" RENAME TO "HouseholdMemberLegacy"`;
tables.splice(tables.indexOf("HouseholdMember"), 1);
}
}
if (!tables.includes("Household")) {
await db.$executeRaw`CREATE TABLE "Household" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "name" TEXT NOT NULL, "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP)`;
await db.$executeRaw`CREATE UNIQUE INDEX "Household_name_key" ON "Household"("name")`;
}
if (!tables.includes("HouseholdMember")) {
await db.$executeRaw`CREATE TABLE "HouseholdMember" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "householdId" INTEGER NOT NULL, "userId" INTEGER NOT NULL, "role" TEXT NOT NULL DEFAULT 'member', "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "HouseholdMember_householdId_fkey" FOREIGN KEY ("householdId") REFERENCES "Household"("id") ON DELETE CASCADE, CONSTRAINT "HouseholdMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE)`;
await db.$executeRaw`CREATE UNIQUE INDEX "HouseholdMember_userId_key" ON "HouseholdMember"("userId")`;
await db.$executeRaw`CREATE UNIQUE INDEX "HouseholdMember_householdId_userId_key" ON "HouseholdMember"("householdId", "userId")`;
}
if (!tables.includes("User")) {
await db.$executeRaw`CREATE TABLE "User" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "name" TEXT NOT NULL, "email" TEXT, "passwordHash" TEXT, "role" TEXT NOT NULL DEFAULT 'member', "oidcSubject" TEXT, "tokenVersion" INTEGER NOT NULL DEFAULT 0, "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP)`;
await db.$executeRaw`CREATE UNIQUE INDEX "User_email_key" ON "User"("email")`;
await db.$executeRaw`CREATE UNIQUE INDEX "User_oidcSubject_key" ON "User"("oidcSubject")`;
}
if (!tables.includes("Invite")) {
await db.$executeRaw`CREATE TABLE "Invite" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "email" TEXT NOT NULL, "tokenHash" TEXT NOT NULL, "role" TEXT NOT NULL DEFAULT 'member', "householdId" INTEGER, "expiresAt" DATETIME NOT NULL, "acceptedAt" DATETIME, "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "Invite_householdId_fkey" FOREIGN KEY ("householdId") REFERENCES "Household"("id") ON DELETE CASCADE)`;
await db.$executeRaw`CREATE UNIQUE INDEX "Invite_email_key" ON "Invite"("email")`;
await db.$executeRaw`CREATE UNIQUE INDEX "Invite_tokenHash_key" ON "Invite"("tokenHash")`;
} else {
const cols = (await db.$queryRaw<{ name: string }[]>`SELECT name FROM pragma_table_info('Invite')`).map(
(c) => c.name
);
if (!cols.includes("householdId")) {
await db.$executeRaw`ALTER TABLE "Invite" ADD COLUMN "householdId" INTEGER REFERENCES "Household"("id") ON DELETE CASCADE`;
}
}
const vcols = (await db.$queryRaw<{ name: string }[]>`SELECT name FROM pragma_table_info('Voucher')`).map(
(c) => c.name
);
if (!vcols.includes("providerDomain")) {
await db.$executeRaw`ALTER TABLE "Voucher" ADD COLUMN "providerDomain" TEXT`;
}
if (!vcols.includes("createdBy")) {
await db.$executeRaw`ALTER TABLE "Voucher" ADD COLUMN "createdBy" TEXT`;
}
if (!vcols.includes("wertTyp")) {
await db.$executeRaw`ALTER TABLE "Voucher" ADD COLUMN "wertTyp" TEXT NOT NULL DEFAULT 'euro'`;
}
if (!vcols.includes("prozentWert")) {
await db.$executeRaw`ALTER TABLE "Voucher" ADD COLUMN "prozentWert" INTEGER`;
}
if (!vcols.includes("mindestbestellwertCents")) {
await db.$executeRaw`ALTER TABLE "Voucher" ADD COLUMN "mindestbestellwertCents" INTEGER`;
}
if (!vcols.includes("shopUrl")) {
await db.$executeRaw`ALTER TABLE "Voucher" ADD COLUMN "shopUrl" TEXT`;
}
if (!vcols.includes("ownerId")) {
await db.$executeRaw`ALTER TABLE "Voucher" ADD COLUMN "ownerId" INTEGER NOT NULL DEFAULT 0`;
}
if (!vcols.includes("householdId")) {
await db.$executeRaw`ALTER TABLE "Voucher" ADD COLUMN "householdId" INTEGER REFERENCES "Household"("id")`;
}
const ccols = (await db.$queryRaw<{ name: string }[]>`SELECT name FROM pragma_table_info('Contact')`).map(
(c) => c.name
);
if (!ccols.includes("ownerId")) {
await db.$executeRaw`ALTER TABLE "Contact" ADD COLUMN "ownerId" INTEGER NOT NULL DEFAULT 0`;
}
if (!ccols.includes("householdId")) {
await db.$executeRaw`ALTER TABLE "Contact" ADD COLUMN "householdId" INTEGER REFERENCES "Household"("id")`;
}
} catch (err) {
console.error("ensureSchema fehlgeschlagen (Start läuft weiter):", err);
}
}
/**
* Mehrhaushalt-Migration (einmalig, idempotent):
* - Standard-Haushalt "Haushalt" anlegen, falls keiner existiert.
* - Bestand ohne Zuordnung (ownerId 0) dem Standard-Haushalt und dem ältesten
* Admin als Besitzer zuordnen.
* - Bestehende Nutzer werden Mitglieder des Standard-Haushalts (Admins als Haushalts-Admins).
*/
export async function ensureMultitenancy(): Promise<void> {
try {
const db = getDb();
const households = await db.household.findMany({ orderBy: { id: "asc" } });
let standard = households[0] ?? null;
if (!standard) {
standard = await db.household.create({ data: { name: "Haushalt" } });
}
const admins = await db.user.findMany({ where: { role: "admin" }, orderBy: { id: "asc" } });
const firstAdminId = admins[0]?.id ?? (await db.user.findFirst({ orderBy: { id: "asc" } }))?.id ?? 0;
await db.voucher.updateMany({ where: { ownerId: 0 }, data: { ownerId: firstAdminId, householdId: standard.id } });
await db.contact.updateMany({ where: { ownerId: 0 }, data: { ownerId: firstAdminId, householdId: standard.id } });
const users = await db.user.findMany({ select: { id: true, role: true } });
for (const u of users) {
const existing = await db.householdMember.findUnique({ where: { userId: u.id } });
if (!existing) {
await db.householdMember.create({
data: { householdId: standard.id, userId: u.id, role: u.role === "admin" ? "admin" : "member" },
});
}
}
} catch (err) {
console.error("ensureMultitenancy fehlgeschlagen (Start läuft weiter):", err);
}
}