Skip to content

Commit 414dfe2

Browse files
authored
feat(db): add database export utility with backup management (#2089)
1 parent 4c91e55 commit 414dfe2

File tree

4 files changed

+147
-2
lines changed

4 files changed

+147
-2
lines changed

db/backups/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Ignore all backup files
2+
*.sql
3+
4+
# Keep the directory in git
5+
!.gitignore

db/drizzle.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ if (!process.env.DATABASE_URL) {
2222
throw new Error("DATABASE_URL environment variable is required");
2323
}
2424

25+
// Validate DATABASE_URL format (basic PostgreSQL URL validation)
26+
if (!process.env.DATABASE_URL.match(/^postgresql?:\/\/.+/)) {
27+
throw new Error("DATABASE_URL must be a valid PostgreSQL connection string");
28+
}
29+
2530
/**
2631
* Drizzle ORM configuration for Neon PostgreSQL database
2732
*

db/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
"seed": "bun scripts/seed.ts",
2525
"seed:staging": "bun --env ENVIRONMENT=staging scripts/seed.ts",
2626
"seed:prod": "bun --env ENVIRONMENT=prod scripts/seed.ts",
27+
"export": "bun scripts/export.ts",
28+
"export:staging": "bun --env ENVIRONMENT=staging scripts/export.ts",
29+
"export:prod": "bun --env ENVIRONMENT=prod scripts/export.ts",
2730
"introspect": "drizzle-kit introspect",
2831
"up": "drizzle-kit up",
2932
"check": "drizzle-kit check",
30-
"drop": "drizzle-kit drop",
31-
"export": "drizzle-kit export"
33+
"drop": "drizzle-kit drop"
3234
},
3335
"peerDependencies": {
3436
"drizzle-orm": "^0.44.4"

db/scripts/export.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env bun
2+
3+
/**
4+
* PostgreSQL database export utility with schema/data options
5+
*
6+
* Usage:
7+
* bun scripts/export.ts # Schema only (default)
8+
* bun scripts/export.ts --data # Schema + data
9+
* bun scripts/export.ts --data-only # Data only
10+
* bun scripts/export.ts --table=users # Specific table
11+
* bun scripts/export.ts -- --inserts # Pass pg_dump flags directly
12+
*
13+
* Environment:
14+
* bun --env ENVIRONMENT=staging scripts/export.ts
15+
* bun --env ENVIRONMENT=prod scripts/export.ts
16+
*
17+
* REQUIREMENTS:
18+
* - DATABASE_URL environment variable must be set and valid PostgreSQL connection string
19+
* - pg_dump binary must be available in PATH (PostgreSQL client tools required, validated at runtime)
20+
* - ./backups/ directory will be created automatically if it doesn't exist
21+
* - Output filenames include timestamp, environment, and export type for uniqueness
22+
* - Process exits with code 1 on any failure for CI/CD integration
23+
* - File permissions on output SQL files are restricted (readable by owner only)
24+
* - Script handles concurrent executions without filename conflicts
25+
*
26+
* SPDX-FileCopyrightText: 2014-present Kriasoft
27+
* SPDX-License-Identifier: MIT
28+
*/
29+
30+
import { $ } from "bun";
31+
import { existsSync } from "fs";
32+
import { chmod, mkdir } from "fs/promises";
33+
import { resolve } from "path";
34+
35+
// Import drizzle config to trigger environment loading and validation
36+
import "../drizzle.config";
37+
38+
// Parse arguments
39+
const args = process.argv.slice(2);
40+
const passThrough: string[] = [];
41+
let includeData = false;
42+
let dataOnly = false;
43+
let table: string | undefined;
44+
45+
// Find pass-through arguments (after --)
46+
const dashIndex = args.indexOf("--");
47+
if (dashIndex !== -1) {
48+
passThrough.push(...args.slice(dashIndex + 1));
49+
args.splice(dashIndex);
50+
}
51+
52+
// Parse named arguments
53+
for (const arg of args) {
54+
if (arg === "--data") {
55+
includeData = true;
56+
} else if (arg === "--data-only") {
57+
dataOnly = true;
58+
} else if (arg.startsWith("--table=")) {
59+
table = arg.split("=")[1];
60+
}
61+
}
62+
63+
// Build pg_dump command
64+
const pgDumpArgs: string[] = [];
65+
66+
// pg_dump requires the connection string as the last positional argument
67+
// or through -d/--dbname flag
68+
pgDumpArgs.push("--dbname", process.env.DATABASE_URL!);
69+
70+
// Default options
71+
pgDumpArgs.push("--format=plain", "--encoding=UTF-8");
72+
73+
// Handle export type
74+
if (dataOnly) {
75+
pgDumpArgs.push("--data-only");
76+
} else if (!includeData) {
77+
pgDumpArgs.push("--schema-only");
78+
}
79+
80+
// Handle table selection
81+
if (table) {
82+
pgDumpArgs.push(`--table=${table}`);
83+
}
84+
85+
// Add pass-through arguments
86+
pgDumpArgs.push(...passThrough);
87+
88+
// Generate filename based on options with high precision timestamp
89+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
90+
const envSuffix = process.env.ENVIRONMENT ? `-${process.env.ENVIRONMENT}` : "";
91+
const typeSuffix = dataOnly ? "-data" : includeData ? "-full" : "-schema";
92+
const tableSuffix = table ? `-${table}` : "";
93+
94+
// Ensure backups directory exists
95+
const backupsDir = resolve("./backups");
96+
if (!existsSync(backupsDir)) {
97+
await mkdir(backupsDir, { recursive: true });
98+
console.log(`📁 Created backups directory: ${backupsDir}`);
99+
}
100+
101+
const outputPath = resolve(
102+
backupsDir,
103+
`dump${envSuffix}${typeSuffix}${tableSuffix}-${timestamp}.sql`,
104+
);
105+
106+
pgDumpArgs.push(`--file=${outputPath}`);
107+
108+
// Check if pg_dump is available
109+
try {
110+
await $`which pg_dump`.quiet();
111+
} catch {
112+
console.error(
113+
"❌ pg_dump not found. Please install PostgreSQL client tools.",
114+
);
115+
process.exit(1);
116+
}
117+
118+
console.log("📤 Exporting database...");
119+
console.log(`📁 Output: ${outputPath}`);
120+
121+
try {
122+
// Execute pg_dump
123+
await $`pg_dump ${pgDumpArgs}`;
124+
125+
// Set file permissions to owner-only readable (600)
126+
await chmod(outputPath, 0o600);
127+
128+
console.log(`✅ Export completed successfully!`);
129+
} catch (error) {
130+
console.error("❌ Export failed:");
131+
console.error(error);
132+
process.exit(1);
133+
}

0 commit comments

Comments
 (0)