Compare commits
2 Commits
d921679058
...
474420ac1e
| Author | SHA1 | Date | |
|---|---|---|---|
| 474420ac1e | |||
| 960f78a1ad |
27
develop
Executable file
27
develop
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This file belongs to the framework. You are not expected to modify it.
|
||||
|
||||
# Development command runner - parallel to ./mgmt for development tasks
|
||||
# Usage: ./develop <command> [args...]
|
||||
|
||||
set -eu
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: ./develop <command> [args...]"
|
||||
echo ""
|
||||
echo "Available commands:"
|
||||
for cmd in "$DIR"/framework/develop.d/*; do
|
||||
if [ -x "$cmd" ]; then
|
||||
basename "$cmd"
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
subcmd="$1"
|
||||
shift
|
||||
|
||||
exec "$DIR"/framework/develop.d/"$subcmd" "$@"
|
||||
@@ -87,8 +87,8 @@ async function raw<T = unknown>(
|
||||
// ============================================================================
|
||||
|
||||
// Migration file naming convention:
|
||||
// NNNN_description.sql
|
||||
// e.g., 0001_initial.sql, 0002_add_users.sql
|
||||
// yyyy-mm-dd_ss_description.sql
|
||||
// e.g., 2025-01-15_01_initial.sql, 2025-01-15_02_add_users.sql
|
||||
//
|
||||
// Migrations directory: express/migrations/
|
||||
|
||||
@@ -128,7 +128,7 @@ function getMigrationFiles(): string[] {
|
||||
return fs
|
||||
.readdirSync(MIGRATIONS_DIR)
|
||||
.filter((f) => f.endsWith(".sql"))
|
||||
.filter((f) => /^\d{4}_/.test(f))
|
||||
.filter((f) => /^\d{4}-\d{2}-\d{2}_\d{2}-/.test(f))
|
||||
.sort();
|
||||
}
|
||||
|
||||
@@ -137,6 +137,8 @@ async function runMigration(filename: string): Promise<void> {
|
||||
const filepath = path.join(MIGRATIONS_DIR, filename);
|
||||
const content = fs.readFileSync(filepath, "utf-8");
|
||||
|
||||
process.stdout.write(` Migration: ${filename}...`);
|
||||
|
||||
// Run migration in a transaction
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
@@ -147,8 +149,11 @@ async function runMigration(filename: string): Promise<void> {
|
||||
[filename],
|
||||
);
|
||||
await client.query("COMMIT");
|
||||
console.log(`Applied migration: ${filename}`);
|
||||
console.log(" ✓");
|
||||
} catch (err) {
|
||||
console.log(" ✗");
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error(` Error: ${message}`);
|
||||
await client.query("ROLLBACK");
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -169,11 +174,10 @@ async function migrate(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Running ${pending.length} migration(s)...`);
|
||||
console.log(`Applying ${pending.length} migration(s):`);
|
||||
for (const file of pending) {
|
||||
await runMigration(file);
|
||||
}
|
||||
console.log("Migrations complete");
|
||||
}
|
||||
|
||||
// List migration status
|
||||
|
||||
50
express/develop/reset-db.ts
Normal file
50
express/develop/reset-db.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// reset-db.ts
|
||||
// Development command to wipe the database and apply all migrations from scratch
|
||||
|
||||
import { migrate, pool, connectionConfig } from "../database";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
console.log("");
|
||||
await migrate();
|
||||
|
||||
console.log("");
|
||||
console.log("Database reset complete.");
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Failed to reset database:", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
-- 0001_users.sql
|
||||
-- Create users table for authentication
|
||||
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
roles TEXT[] NOT NULL DEFAULT '{}',
|
||||
permissions TEXT[] NOT NULL DEFAULT '{}',
|
||||
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Index for email lookups (login)
|
||||
CREATE INDEX users_email_idx ON users (LOWER(email));
|
||||
|
||||
-- Index for status filtering
|
||||
CREATE INDEX users_status_idx ON users (status);
|
||||
29
express/migrations/2026-01-01_01-users.sql
Normal file
29
express/migrations/2026-01-01_01-users.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
-- 0001_users.sql
|
||||
-- Create users table for authentication
|
||||
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
display_name TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE user_emails (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
email TEXT NOT NULL,
|
||||
normalized_email TEXT NOT NULL,
|
||||
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
verified_at TIMESTAMPTZ,
|
||||
revoked_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- Enforce uniqueness only among *active* emails
|
||||
CREATE UNIQUE INDEX user_emails_unique_active
|
||||
ON user_emails (normalized_email)
|
||||
WHERE revoked_at IS NULL;
|
||||
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
-- Create sessions table for auth tokens
|
||||
|
||||
CREATE TABLE sessions (
|
||||
token_id TEXT PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
user_email_id UUID REFERENCES user_emails(id),
|
||||
token_type TEXT NOT NULL,
|
||||
auth_method TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
last_used_at TIMESTAMPTZ,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
ip_address TEXT,
|
||||
is_used BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
|
||||
20
express/migrations/2026-01-24_01-roles-and-groups.sql
Normal file
20
express/migrations/2026-01-24_01-roles-and-groups.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE roles (
|
||||
id UUID PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE groups (
|
||||
id UUID PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE user_group_roles (
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
group_id UUID NOT NULL REFERENCES groups(id),
|
||||
role_id UUID NOT NULL REFERENCES roles(id),
|
||||
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
revoked_at TIMESTAMPTZ,
|
||||
PRIMARY KEY (user_id, group_id, role_id)
|
||||
);
|
||||
14
express/migrations/2026-01-24_02-capabilities.sql
Normal file
14
express/migrations/2026-01-24_02-capabilities.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE capabilities (
|
||||
id UUID PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE role_capabilities (
|
||||
role_id UUID NOT NULL REFERENCES roles(id),
|
||||
capability_id UUID NOT NULL REFERENCES capabilities(id),
|
||||
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
revoked_at TIMESTAMPTZ,
|
||||
PRIMARY KEY (role_id, capability_id)
|
||||
);
|
||||
|
||||
9
framework/develop.d/reset-db
Executable file
9
framework/develop.d/reset-db
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$DIR/../.."
|
||||
|
||||
cd "$ROOT/express"
|
||||
"$DIR"/../cmd.d/tsx develop/reset-db.ts "$@"
|
||||
Reference in New Issue
Block a user