import { execFileSync } from "node:child_process"; import { mkdirSync } from "node:fs"; import { resolve } from "node:path"; import { hash } from "@node-rs/argon2"; import { SystemRole } from "@capakraken/shared"; import { PrismaClient } from "@prisma/client"; import { assertDestructiveDbAllowed } from "./destructive-db-guard.js"; import { loadWorkspaceEnv, resolveWorkspacePath } from "./load-workspace-env.js"; import { buildSystemRoleConfigSeedData } from "./system-role-config-defaults.js"; loadWorkspaceEnv(); const prisma = new PrismaClient(); const DEFAULT_BACKUP_DIR = resolveWorkspacePath("packages", "db", "backups"); interface ResetOptions { force: boolean; skipBackup: boolean; backupDir: string; adminEmail: string; adminPassword: string; adminName: string; } function parseArgs(argv: string[]): ResetOptions { const options: ResetOptions = { force: false, skipBackup: false, backupDir: DEFAULT_BACKUP_DIR, adminEmail: "admin@capakraken.dev", adminPassword: "admin123", adminName: "CapaKraken Admin", }; for (let index = 0; index < argv.length; index += 1) { const argument = argv[index]; if (argument === "--force") { options.force = true; continue; } if (argument === "--skip-backup") { options.skipBackup = true; continue; } if (argument === "--backup-dir") { options.backupDir = resolve(argv[index + 1] ?? DEFAULT_BACKUP_DIR); index += 1; continue; } if (argument === "--admin-email") { options.adminEmail = argv[index + 1] ?? options.adminEmail; index += 1; continue; } if (argument === "--admin-password") { options.adminPassword = argv[index + 1] ?? options.adminPassword; index += 1; continue; } if (argument === "--admin-name") { options.adminName = argv[index + 1] ?? options.adminName; index += 1; } } return options; } function createTimestamp() { return new Date().toISOString().replace(/[:.]/g, "-"); } function quoteIdentifier(identifier: string): string { return `"${identifier.replace(/"/g, "\"\"")}"`; } function createDatabaseBackup(databaseUrl: string, backupDir: string): string { mkdirSync(backupDir, { recursive: true }); const backupPath = resolve(backupDir, `dispo-reset-${createTimestamp()}.dump`); execFileSync( "pg_dump", ["--format=custom", "--file", backupPath, databaseUrl], { stdio: "inherit", env: process.env, }, ); return backupPath; } async function listPublicTables() { return prisma.$queryRaw>` SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename <> '_prisma_migrations' ORDER BY tablename ASC `; } async function truncatePublicTables() { const tables = await listPublicTables(); if (tables.length === 0) { return []; } const quotedTables = tables.map((table) => quoteIdentifier(table.tablename)).join(", "); await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${quotedTables} RESTART IDENTITY CASCADE`); return tables.map((table) => table.tablename); } async function bootstrapPlatform(adminEmail: string, adminPassword: string, adminName: string) { const passwordHash = await hash(adminPassword); const admin = await prisma.user.create({ data: { email: adminEmail, name: adminName, passwordHash, systemRole: SystemRole.ADMIN, }, }); await prisma.systemSettings.upsert({ where: { id: "singleton" }, update: { vacationDefaultDays: 28, }, create: { id: "singleton", vacationDefaultDays: 28, }, }); for (const config of buildSystemRoleConfigSeedData()) { await prisma.systemRoleConfig.upsert({ where: { role: config.role }, update: { label: config.label, description: config.description, defaultPermissions: config.defaultPermissions, color: config.color, sortOrder: config.sortOrder, }, create: config, }); } return admin; } async function main() { const options = parseArgs(process.argv.slice(2)); const target = assertDestructiveDbAllowed({ commandName: "db:reset:dispo", allowedDatabaseNames: ["capakraken_test", "capakraken_e2e", "capakraken_ci"], }); const databaseUrl = process.env.DATABASE_URL; if (!options.force) { throw new Error("Refusing to reset the database without --force."); } if (!databaseUrl) { throw new Error("DATABASE_URL is not configured."); } console.warn(`Resetting disposable database '${target.databaseName}'.`); let backupPath: string | null = null; if (options.skipBackup) { console.warn("Skipping pg_dump backup because --skip-backup was provided."); } else { try { backupPath = createDatabaseBackup(databaseUrl, options.backupDir); console.log(`Backup created at ${backupPath}`); } catch (error) { throw new Error( `Backup failed. Install pg_dump or rerun with --skip-backup if this is an intentional disposable environment.\n${String(error)}`, ); } } const truncatedTables = await truncatePublicTables(); console.log(`Truncated ${truncatedTables.length} public tables.`); const admin = await bootstrapPlatform(options.adminEmail, options.adminPassword, options.adminName); console.log(`Bootstrap admin created: ${admin.email}`); if (backupPath) { console.log(`Database backup: ${backupPath}`); } console.log("Dispo import reset/bootstrap complete."); } main() .catch((error) => { console.error(error); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });