/** * Wipe all data from the database, preserving schema + drizzle migration * history. Run before swapping seed fixtures. * * pnpm tsx scripts/db-reset.ts (refuses without --confirm) * pnpm tsx scripts/db-reset.ts --confirm * * Truncates every table in the `public` schema except the drizzle * migration tracker, then resets sequences. Wraps the loop in a single * transaction so a mid-wipe failure rolls back cleanly. * * Refuses to run when DATABASE_URL points at anything that doesn't look * like a local/dev host. Override with --i-know-what-im-doing. */ import 'dotenv/config'; import postgres from 'postgres'; const url: string = process.env.DATABASE_URL ?? ''; if (!url) { console.error('DATABASE_URL is not set; aborting.'); process.exit(1); } const args = new Set(process.argv.slice(2)); if (!args.has('--confirm')) { console.error('Refusing to wipe without --confirm'); console.error('Run again as: pnpm tsx scripts/db-reset.ts --confirm'); process.exit(1); } // Best-effort safety: refuse for anything that doesn't look like a local DB. function looksLocal(u: string): boolean { try { const parsed = new URL(u); return ( parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' || parsed.hostname === '::1' || parsed.hostname.endsWith('.local') || parsed.hostname.endsWith('.internal') || parsed.hostname === 'host.docker.internal' || // Docker compose service names commonly used here parsed.hostname === 'postgres' || parsed.hostname === 'db' ); } catch { return false; } } if (!looksLocal(url) && !args.has('--i-know-what-im-doing')) { console.error( `DATABASE_URL host doesn't look local. Refusing to wipe a remote DB without --i-know-what-im-doing.`, ); process.exit(1); } const sql = postgres(url, { max: 1 }); async function main() { console.log('Resetting database...'); console.log(` url: ${url.replace(/:[^:@]*@/, ':***@')}`); const tables = await sql<{ tablename: string }[]>` SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename NOT LIKE 'drizzle_%' AND tablename != '__drizzle_migrations' `; if (tables.length === 0) { console.log(' no user tables found, nothing to do.'); await sql.end(); return; } // Single TRUNCATE … CASCADE is faster than per-table loops and handles // FK ordering for us. Quote table names defensively. const tableList = tables.map((t) => `"public"."${t.tablename}"`).join(', '); console.log(` truncating ${tables.length} tables...`); await sql.unsafe(`TRUNCATE ${tableList} RESTART IDENTITY CASCADE`); console.log(' done.'); await sql.end(); console.log(''); console.log('Database reset complete. Run a seed script next:'); console.log(' pnpm db:seed # realistic NocoDB-shaped fixture'); console.log(' pnpm db:seed:synthetic # one client per pipeline stage'); } main().catch(async (err) => { console.error('Reset failed:', err); await sql.end().catch(() => undefined); process.exit(1); });