feat: Phase 1 MVP — complete AgentIdP implementation
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>
This commit is contained in:
120
scripts/migrate.ts
Normal file
120
scripts/migrate.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user