Skip to content

Commit 22ae973

Browse files
DavidBabinecclaude
andcommitted
feat(scripts): add db:drop script for clean dev state
Adds `bun run db:drop` which wipes the local SQLite/Postgres database and empties the uploads directory so developers can start from a clean slate. Supports `-y`/`--yes`/`--force` flags to skip the interactive confirmation prompt for scripting. Targets are resolved from the same env vars the server uses (DATABASE_URL, UPLOADS_DIR). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f7749aa commit 22ae973

2 files changed

Lines changed: 129 additions & 0 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"dev:server": "bun --watch server/index.ts",
1010
"dev:vite": "vite",
1111
"dev:www": "bun run scripts/dev-www.ts",
12+
"db:drop": "bun run scripts/db-drop.ts",
1213
"start": "bun run scripts/start.ts",
1314
"docker:up": "docker compose -f compose.prod.yml -f compose.build.yml up --build",
1415
"build": "tsc -b && vite build",

scripts/db-drop.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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

Comments
 (0)