|
| 1 | +/** |
| 2 | + * Development-only database + uploads reset. |
| 3 | + * |
| 4 | + * `bun run db:drop` wipes local state so you can start from a clean slate: |
| 5 | + * |
| 6 | + * - SQLite (default): deletes the database file and its `-wal` / `-shm` / |
| 7 | + * `-journal` sidecar files (e.g. ./.tmp/dev.db*). |
| 8 | + * - Postgres: drops and recreates the `public` schema, removing every table. |
| 9 | + * - Uploads: empties the uploads directory (media, fonts, published static |
| 10 | + * artefacts) but keeps the directory itself. |
| 11 | + * |
| 12 | + * Paths come from the same env vars the server boots with |
| 13 | + * (DATABASE_URL, UPLOADS_DIR) via readServerConfig, so this always targets |
| 14 | + * whatever database the local dev server is actually using. |
| 15 | + * |
| 16 | + * The next `bun run dev` re-runs migrations and recreates the database from |
| 17 | + * scratch. |
| 18 | + * |
| 19 | + * This is destructive and intentionally local-only — never run it against a |
| 20 | + * database you care about. Pass `-y` / `--yes` to skip the confirmation |
| 21 | + * prompt (useful for scripting). |
| 22 | + */ |
| 23 | + |
| 24 | +import { SQL } from 'bun' |
| 25 | +import { readdir, rm, mkdir } from 'node:fs/promises' |
| 26 | +import { join, resolve } from 'node:path' |
| 27 | +import { isSqliteUrl, parseSqlitePath } from '../server/db' |
| 28 | +import { readServerConfig } from '../server/config' |
| 29 | + |
| 30 | +function log(msg: string): void { |
| 31 | + console.error(`[db:drop] ${msg}`) |
| 32 | +} |
| 33 | + |
| 34 | +function fail(msg: string): never { |
| 35 | + log(msg) |
| 36 | + process.exit(1) |
| 37 | +} |
| 38 | + |
| 39 | +const skipPrompt = process.argv.slice(2).some((arg) => arg === '-y' || arg === '--yes' || arg === '--force') |
| 40 | + |
| 41 | +const config = readServerConfig() |
| 42 | +const { databaseUrl, uploadsDir } = config |
| 43 | + |
| 44 | +// --- confirmation ---------------------------------------------------------- |
| 45 | + |
| 46 | +if (!skipPrompt) { |
| 47 | + log('This will PERMANENTLY delete:') |
| 48 | + log(` • database: ${databaseUrl}`) |
| 49 | + log(` • uploads: ${resolve(uploadsDir)}`) |
| 50 | + const answer = prompt('[db:drop] Type "y" to continue:') |
| 51 | + if (answer?.trim().toLowerCase() !== 'y') { |
| 52 | + fail('Aborted — nothing was deleted.') |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +// --- database -------------------------------------------------------------- |
| 57 | + |
| 58 | +async function dropSqlite(): Promise<void> { |
| 59 | + const dbPath = parseSqlitePath(databaseUrl) |
| 60 | + const targets = [dbPath, `${dbPath}-wal`, `${dbPath}-shm`, `${dbPath}-journal`] |
| 61 | + let removed = 0 |
| 62 | + for (const target of targets) { |
| 63 | + if (await Bun.file(target).exists()) { |
| 64 | + await rm(target, { force: true }) |
| 65 | + removed += 1 |
| 66 | + log(`Removed ${target}`) |
| 67 | + } |
| 68 | + } |
| 69 | + if (removed === 0) { |
| 70 | + log(`No SQLite files found at ${dbPath} — already clean.`) |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +async function dropPostgres(): Promise<void> { |
| 75 | + // Reset the schema rather than dropping the whole database — the connection |
| 76 | + // role keeps its rights and the next migration run rebuilds every table. |
| 77 | + const sql = new SQL(databaseUrl) |
| 78 | + try { |
| 79 | + await sql.unsafe('drop schema if exists public cascade') |
| 80 | + await sql.unsafe('create schema public') |
| 81 | + log('Dropped and recreated the public schema.') |
| 82 | + } catch (err) { |
| 83 | + const message = err instanceof Error ? err.message : 'Unknown error' |
| 84 | + fail( |
| 85 | + `Could not reach Postgres at ${databaseUrl}: ${message}\n` + |
| 86 | + ' Is the database running? For the dockerised dev DB, start it with `bun run dev` first.', |
| 87 | + ) |
| 88 | + } finally { |
| 89 | + await sql.close() |
| 90 | + } |
| 91 | +} |
| 92 | + |
| 93 | +if (isSqliteUrl(databaseUrl)) { |
| 94 | + await dropSqlite() |
| 95 | +} else { |
| 96 | + await dropPostgres() |
| 97 | +} |
| 98 | + |
| 99 | +// --- uploads --------------------------------------------------------------- |
| 100 | + |
| 101 | +async function clearUploads(): Promise<void> { |
| 102 | + const dir = resolve(uploadsDir) |
| 103 | + let entries: string[] |
| 104 | + try { |
| 105 | + entries = await readdir(dir) |
| 106 | + } catch (err) { |
| 107 | + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { |
| 108 | + await mkdir(dir, { recursive: true }) |
| 109 | + log(`Uploads directory did not exist — created empty ${dir}.`) |
| 110 | + return |
| 111 | + } |
| 112 | + throw err |
| 113 | + } |
| 114 | + |
| 115 | + if (entries.length === 0) { |
| 116 | + log(`Uploads directory already empty (${dir}).`) |
| 117 | + return |
| 118 | + } |
| 119 | + |
| 120 | + for (const entry of entries) { |
| 121 | + await rm(join(dir, entry), { recursive: true, force: true }) |
| 122 | + } |
| 123 | + log(`Cleared ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'} from ${dir}.`) |
| 124 | +} |
| 125 | + |
| 126 | +await clearUploads() |
| 127 | + |
| 128 | +log('Done. Run `bun run dev` to recreate the database from migrations.') |
0 commit comments