98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
|
|
/**
|
||
|
|
* 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 = 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);
|
||
|
|
});
|