Implements all P0 features per OpenSpec change phase-1-mvp-implementation: - Agent Registry Service (CRUD) — full lifecycle management - OAuth 2.0 Token Service (Client Credentials flow) - Credential Management (generate, rotate, revoke) - Immutable Audit Log Service Tech: Node.js 18+, TypeScript 5.3+ strict, Express 4.18+, PostgreSQL 14+, Redis 7+ Standards: OpenAPI 3.0 specs, DRY/SOLID, zero `any` types Quality: 18 unit test suites, 244 tests passing, 97%+ coverage OpenAPI: 4 complete specs (14 endpoints total) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
121 lines
3.3 KiB
TypeScript
121 lines
3.3 KiB
TypeScript
/**
|
|
* Database migration runner for SentryAgent.ai AgentIdP.
|
|
* Reads all .sql files from src/db/migrations/ in alphabetical order,
|
|
* tracks applied migrations in a schema_migrations table, and executes
|
|
* only unapplied migrations.
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { Pool } from 'pg';
|
|
import * as dotenv from 'dotenv';
|
|
|
|
dotenv.config();
|
|
|
|
const MIGRATIONS_DIR = path.join(__dirname, '../src/db/migrations');
|
|
|
|
interface MigrationRow {
|
|
name: string;
|
|
applied_at: Date;
|
|
}
|
|
|
|
/**
|
|
* Ensures the schema_migrations tracking table exists.
|
|
*
|
|
* @param pool - The PostgreSQL connection pool.
|
|
*/
|
|
async function ensureMigrationsTable(pool: Pool): Promise<void> {
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
name VARCHAR(255) PRIMARY KEY,
|
|
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)
|
|
`);
|
|
}
|
|
|
|
/**
|
|
* Returns the list of already-applied migration names.
|
|
*
|
|
* @param pool - The PostgreSQL connection pool.
|
|
* @returns Array of applied migration names.
|
|
*/
|
|
async function getAppliedMigrations(pool: Pool): Promise<string[]> {
|
|
const result = await pool.query<MigrationRow>('SELECT name FROM schema_migrations ORDER BY name');
|
|
return result.rows.map((row) => row.name);
|
|
}
|
|
|
|
/**
|
|
* Applies a single migration file within a transaction.
|
|
*
|
|
* @param pool - The PostgreSQL connection pool.
|
|
* @param name - The migration file name (without path).
|
|
* @param sql - The SQL to execute.
|
|
*/
|
|
async function applyMigration(pool: Pool, name: string, sql: string): Promise<void> {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
await client.query(sql);
|
|
await client.query('INSERT INTO schema_migrations (name) VALUES ($1)', [name]);
|
|
await client.query('COMMIT');
|
|
// eslint-disable-next-line no-console
|
|
console.log(` ✓ Applied: ${name}`);
|
|
} catch (err) {
|
|
await client.query('ROLLBACK');
|
|
throw err;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main migration runner.
|
|
* Reads all .sql files in alphabetical order and applies unapplied ones.
|
|
*/
|
|
async function migrate(): Promise<void> {
|
|
const connectionString = process.env['DATABASE_URL'];
|
|
if (!connectionString) {
|
|
throw new Error('DATABASE_URL environment variable is required');
|
|
}
|
|
|
|
const pool = new Pool({ connectionString });
|
|
|
|
try {
|
|
// eslint-disable-next-line no-console
|
|
console.log('Running database migrations...');
|
|
|
|
await ensureMigrationsTable(pool);
|
|
const applied = await getAppliedMigrations(pool);
|
|
|
|
const files = fs
|
|
.readdirSync(MIGRATIONS_DIR)
|
|
.filter((f) => f.endsWith('.sql'))
|
|
.sort();
|
|
|
|
let count = 0;
|
|
for (const file of files) {
|
|
if (applied.includes(file)) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(` - Skipped (already applied): ${file}`);
|
|
continue;
|
|
}
|
|
|
|
const filePath = path.join(MIGRATIONS_DIR, file);
|
|
const sql = fs.readFileSync(filePath, 'utf-8');
|
|
await applyMigration(pool, file, sql);
|
|
count++;
|
|
}
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(`\nMigrations complete. ${count} migration(s) applied.`);
|
|
} finally {
|
|
await pool.end();
|
|
}
|
|
}
|
|
|
|
migrate().catch((err: unknown) => {
|
|
// eslint-disable-next-line no-console
|
|
console.error('Migration failed:', err);
|
|
process.exit(1);
|
|
});
|