Separate framework and app migrations
Also add a new develop command: clear-db.
This commit is contained in:
@@ -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)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
express/develop/clear-db.ts
Normal file
17
express/develop/clear-db.ts
Normal 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);
|
||||||
|
});
|
||||||
@@ -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
42
express/develop/util.ts
Normal 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 };
|
||||||
@@ -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
11
framework/develop.d/clear-db
Executable 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 "$@"
|
||||||
Reference in New Issue
Block a user