diff --git a/express/database.ts b/express/database.ts index 163f579..afb8bf5 100644 --- a/express/database.ts +++ b/express/database.ts @@ -18,6 +18,7 @@ import type { } from "./auth/store"; import { generateToken, hashToken } from "./auth/token"; import type { SessionData, TokenId } from "./auth/types"; +import type { Domain } from "./types"; import { AuthenticatedUser, type User, type UserId } from "./user"; // Connection configuration @@ -112,7 +113,8 @@ async function raw( // // 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"; interface MigrationRecord { @@ -141,20 +143,30 @@ async function getAppliedMigrations(): Promise { } // Get pending migration files -function getMigrationFiles(): string[] { - if (!fs.existsSync(MIGRATIONS_DIR)) { +function getMigrationFiles(kind: Domain): string[] { + const dir = kind === "fw" ? FRAMEWORK_MIGRATIONS_DIR : APP_MIGRATIONS_DIR; + + if (!fs.existsSync(dir)) { return []; } - return fs - .readdirSync(MIGRATIONS_DIR) + + const root = __dirname; + + const mm = fs + .readdirSync(dir) .filter((f) => f.endsWith(".sql")) .filter((f) => /^\d{4}-\d{2}-\d{2}_\d{2}-/.test(f)) + .map((f) => `${dir}/${f}`) + .map((f) => f.replace(`${root}/`, "")) .sort(); + + return mm; } // Run a single migration async function runMigration(filename: string): Promise { - const filepath = path.join(MIGRATIONS_DIR, filename); + // const filepath = path.join(MIGRATIONS_DIR, filename); + const filepath = filename; const content = fs.readFileSync(filepath, "utf-8"); process.stdout.write(` Migration: ${filename}...`); @@ -181,13 +193,21 @@ async function runMigration(filename: string): Promise { } } +function getAllMigrationFiles() { + const fw_files = getMigrationFiles("fw"); + const app_files = getMigrationFiles("app"); + const all = [...fw_files, ...app_files]; + + return all; +} + // Run all pending migrations async function migrate(): Promise { await ensureMigrationsTable(); const applied = new Set(await getAppliedMigrations()); - const files = getMigrationFiles(); - const pending = files.filter((f) => !applied.has(f)); + const all = getAllMigrationFiles(); + const pending = all.filter((all) => !applied.has(all)); if (pending.length === 0) { console.log("No pending migrations"); @@ -207,10 +227,10 @@ async function migrationStatus(): Promise<{ }> { await ensureMigrationsTable(); const applied = new Set(await getAppliedMigrations()); - const files = getMigrationFiles(); + const ff = getAllMigrationFiles(); return { - applied: files.filter((f) => applied.has(f)), - pending: files.filter((f) => !applied.has(f)), + applied: ff.filter((ff) => applied.has(ff)), + pending: ff.filter((ff) => !applied.has(ff)), }; } diff --git a/express/develop/clear-db.ts b/express/develop/clear-db.ts new file mode 100644 index 0000000..2293ff6 --- /dev/null +++ b/express/develop/clear-db.ts @@ -0,0 +1,17 @@ +import { connectionConfig, migrate, pool } from "../database"; +import { dropTables, exitIfUnforced } from "./util"; + +async function main(): Promise { + exitIfUnforced(); + + try { + await dropTables(); + } finally { + await pool.end(); + } +} + +main().catch((err) => { + console.error("Failed to clear database:", err.message); + process.exit(1); +}); diff --git a/express/develop/reset-db.ts b/express/develop/reset-db.ts index abd7dd2..3545c58 100644 --- a/express/develop/reset-db.ts +++ b/express/develop/reset-db.ts @@ -1,38 +1,14 @@ // reset-db.ts // 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 { - 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); - } + exitIfUnforced(); try { - 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"); - } + await dropTables(); console.log(""); await migrate(); diff --git a/express/develop/util.ts b/express/develop/util.ts new file mode 100644 index 0000000..4d138f6 --- /dev/null +++ b/express/develop/util.ts @@ -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 }; diff --git a/express/migrations/2026-01-01_01-users.sql b/express/framework/migrations/2026-01-01_01-users.sql similarity index 100% rename from express/migrations/2026-01-01_01-users.sql rename to express/framework/migrations/2026-01-01_01-users.sql diff --git a/express/migrations/2026-01-01_02-sessions.sql b/express/framework/migrations/2026-01-01_02-sessions.sql similarity index 100% rename from express/migrations/2026-01-01_02-sessions.sql rename to express/framework/migrations/2026-01-01_02-sessions.sql diff --git a/express/migrations/2026-01-24_01-roles-and-groups.sql b/express/framework/migrations/2026-01-24_01-roles-and-groups.sql similarity index 100% rename from express/migrations/2026-01-24_01-roles-and-groups.sql rename to express/framework/migrations/2026-01-24_01-roles-and-groups.sql diff --git a/express/migrations/2026-01-24_02-capabilities.sql b/express/framework/migrations/2026-01-24_02-capabilities.sql similarity index 100% rename from express/migrations/2026-01-24_02-capabilities.sql rename to express/framework/migrations/2026-01-24_02-capabilities.sql diff --git a/express/types.ts b/express/types.ts index e62b77c..2cb2b8d 100644 --- a/express/types.ts +++ b/express/types.ts @@ -112,4 +112,6 @@ export function requirePermission(call: Call, permission: Permission): User { return user; } +export type Domain = "app" | "fw"; + export { methodParser, massageMethod }; diff --git a/framework/develop.d/clear-db b/framework/develop.d/clear-db new file mode 100755 index 0000000..467d490 --- /dev/null +++ b/framework/develop.d/clear-db @@ -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 "$@"