Separate framework and app migrations

Also add a new develop command: clear-db.
This commit is contained in:
2026-01-24 16:38:33 -06:00
parent 579a19669e
commit 8704c4a8d5
10 changed files with 107 additions and 39 deletions

View File

@@ -18,6 +18,7 @@ import type {
} from "./auth/store"; } from "./auth/store";
import { generateToken, hashToken } from "./auth/token"; import { generateToken, hashToken } from "./auth/token";
import type { SessionData, TokenId } from "./auth/types"; import type { SessionData, TokenId } from "./auth/types";
import type { Domain } from "./types";
import { AuthenticatedUser, type User, type UserId } from "./user"; import { AuthenticatedUser, type User, type UserId } from "./user";
// Connection configuration // Connection configuration
@@ -112,7 +113,8 @@ async function raw<T = unknown>(
// //
// Migrations directory: express/migrations/ // Migrations directory: express/migrations/
const MIGRATIONS_DIR = path.join(__dirname, "migrations"); const FRAMEWORK_MIGRATIONS_DIR = path.join(__dirname, "framework/migrations");
const APP_MIGRATIONS_DIR = path.join(__dirname, "migrations");
const MIGRATIONS_TABLE = "_migrations"; const MIGRATIONS_TABLE = "_migrations";
interface MigrationRecord { interface MigrationRecord {
@@ -141,20 +143,30 @@ async function getAppliedMigrations(): Promise<string[]> {
} }
// Get pending migration files // Get pending migration files
function getMigrationFiles(): string[] { function getMigrationFiles(kind: Domain): string[] {
if (!fs.existsSync(MIGRATIONS_DIR)) { const dir = kind === "fw" ? FRAMEWORK_MIGRATIONS_DIR : APP_MIGRATIONS_DIR;
if (!fs.existsSync(dir)) {
return []; return [];
} }
return fs
.readdirSync(MIGRATIONS_DIR) const root = __dirname;
const mm = fs
.readdirSync(dir)
.filter((f) => f.endsWith(".sql")) .filter((f) => f.endsWith(".sql"))
.filter((f) => /^\d{4}-\d{2}-\d{2}_\d{2}-/.test(f)) .filter((f) => /^\d{4}-\d{2}-\d{2}_\d{2}-/.test(f))
.map((f) => `${dir}/${f}`)
.map((f) => f.replace(`${root}/`, ""))
.sort(); .sort();
return mm;
} }
// Run a single migration // Run a single migration
async function runMigration(filename: string): Promise<void> { async function runMigration(filename: string): Promise<void> {
const filepath = path.join(MIGRATIONS_DIR, filename); // const filepath = path.join(MIGRATIONS_DIR, filename);
const filepath = filename;
const content = fs.readFileSync(filepath, "utf-8"); const content = fs.readFileSync(filepath, "utf-8");
process.stdout.write(` Migration: ${filename}...`); process.stdout.write(` Migration: ${filename}...`);
@@ -181,13 +193,21 @@ async function runMigration(filename: string): Promise<void> {
} }
} }
function getAllMigrationFiles() {
const fw_files = getMigrationFiles("fw");
const app_files = getMigrationFiles("app");
const all = [...fw_files, ...app_files];
return all;
}
// Run all pending migrations // Run all pending migrations
async function migrate(): Promise<void> { async function migrate(): Promise<void> {
await ensureMigrationsTable(); await ensureMigrationsTable();
const applied = new Set(await getAppliedMigrations()); const applied = new Set(await getAppliedMigrations());
const files = getMigrationFiles(); const all = getAllMigrationFiles();
const pending = files.filter((f) => !applied.has(f)); const pending = all.filter((all) => !applied.has(all));
if (pending.length === 0) { if (pending.length === 0) {
console.log("No pending migrations"); console.log("No pending migrations");
@@ -207,10 +227,10 @@ async function migrationStatus(): Promise<{
}> { }> {
await ensureMigrationsTable(); await ensureMigrationsTable();
const applied = new Set(await getAppliedMigrations()); const applied = new Set(await getAppliedMigrations());
const files = getMigrationFiles(); const ff = getAllMigrationFiles();
return { return {
applied: files.filter((f) => applied.has(f)), applied: ff.filter((ff) => applied.has(ff)),
pending: files.filter((f) => !applied.has(f)), pending: ff.filter((ff) => !applied.has(ff)),
}; };
} }

View File

@@ -0,0 +1,17 @@
import { connectionConfig, migrate, pool } from "../database";
import { dropTables, exitIfUnforced } from "./util";
async function main(): Promise<void> {
exitIfUnforced();
try {
await dropTables();
} finally {
await pool.end();
}
}
main().catch((err) => {
console.error("Failed to clear database:", err.message);
process.exit(1);
});

View File

@@ -1,38 +1,14 @@
// reset-db.ts // reset-db.ts
// Development command to wipe the database and apply all migrations from scratch // Development command to wipe the database and apply all migrations from scratch
import { migrate, pool, connectionConfig } from "../database"; import { connectionConfig, migrate, pool } from "../database";
import { dropTables, exitIfUnforced } from "./util";
async function main(): Promise<void> { async function main(): Promise<void> {
const args = process.argv.slice(2); exitIfUnforced();
// Require explicit confirmation unless --force is passed
if (!args.includes("--force")) {
console.error("This will DROP ALL TABLES in the database!");
console.error(` Database: ${connectionConfig.database}`);
console.error(` Host: ${connectionConfig.host}:${connectionConfig.port}`);
console.error("");
console.error("Run with --force to proceed.");
process.exit(1);
}
try { try {
console.log("Dropping all tables..."); await dropTables();
// Get all table names in the public schema
const result = await pool.query<{ tablename: string }>(`
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
`);
if (result.rows.length > 0) {
// Drop all tables with CASCADE to handle foreign key constraints
const tableNames = result.rows.map((r) => `"${r.tablename}"`).join(", ");
await pool.query(`DROP TABLE IF EXISTS ${tableNames} CASCADE`);
console.log(`Dropped ${result.rows.length} table(s)`);
} else {
console.log("No tables to drop");
}
console.log(""); console.log("");
await migrate(); await migrate();

42
express/develop/util.ts Normal file
View File

@@ -0,0 +1,42 @@
// FIXME: this is at the wrong level of specificity
import { connectionConfig, migrate, pool } from "../database";
const exitIfUnforced = () => {
const args = process.argv.slice(2);
// Require explicit confirmation unless --force is passed
if (!args.includes("--force")) {
console.error("This will DROP ALL TABLES in the database!");
console.error(` Database: ${connectionConfig.database}`);
console.error(
` Host: ${connectionConfig.host}:${connectionConfig.port}`,
);
console.error("");
console.error("Run with --force to proceed.");
process.exit(1);
}
};
const dropTables = async () => {
console.log("Dropping all tables...");
// Get all table names in the public schema
const result = await pool.query<{ tablename: string }>(`
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
`);
if (result.rows.length > 0) {
// Drop all tables with CASCADE to handle foreign key constraints
const tableNames = result.rows
.map((r) => `"${r.tablename}"`)
.join(", ");
await pool.query(`DROP TABLE IF EXISTS ${tableNames} CASCADE`);
console.log(`Dropped ${result.rows.length} table(s)`);
} else {
console.log("No tables to drop");
}
};
export { dropTables, exitIfUnforced };

View File

@@ -112,4 +112,6 @@ export function requirePermission(call: Call, permission: Permission): User {
return user; return user;
} }
export type Domain = "app" | "fw";
export { methodParser, massageMethod }; export { methodParser, massageMethod };

11
framework/develop.d/clear-db Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
# This file belongs to the framework. You are not expected to modify it.
set -eu
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$DIR/../.."
cd "$ROOT/express"
"$DIR"/../cmd.d/tsx develop/clear-db.ts "$@"