- Add auto-migration system that runs SQL files from migrations/ on server startup - Track applied migrations in schema_migrations table - Show proxy exit location in Workers dashboard - Add "Cleanup Stale" button to remove old workers - Add remove button for individual workers - Include proxy location (city, state, country) in worker heartbeats - Update Proxy interface with location fields - Re-enable bulk proxy import without ON CONFLICT 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
142 lines
3.7 KiB
TypeScript
142 lines
3.7 KiB
TypeScript
/**
|
|
* Auto-Migration System
|
|
*
|
|
* Runs SQL migration files from the migrations/ folder automatically on server startup.
|
|
* Uses a schema_migrations table to track which migrations have been applied.
|
|
*
|
|
* Safe to run multiple times - only applies new migrations.
|
|
*/
|
|
|
|
import { Pool } from 'pg';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
const MIGRATIONS_DIR = path.join(__dirname, '../../migrations');
|
|
|
|
/**
|
|
* Ensure schema_migrations table exists
|
|
*/
|
|
async function ensureMigrationsTable(pool: Pool): Promise<void> {
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(255) UNIQUE NOT NULL,
|
|
applied_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
)
|
|
`);
|
|
}
|
|
|
|
/**
|
|
* Get list of already-applied migrations
|
|
*/
|
|
async function getAppliedMigrations(pool: Pool): Promise<Set<string>> {
|
|
const result = await pool.query('SELECT name FROM schema_migrations');
|
|
return new Set(result.rows.map(row => row.name));
|
|
}
|
|
|
|
/**
|
|
* Get list of migration files from disk
|
|
*/
|
|
function getMigrationFiles(): string[] {
|
|
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
|
console.log('[AutoMigrate] No migrations directory found');
|
|
return [];
|
|
}
|
|
|
|
return fs.readdirSync(MIGRATIONS_DIR)
|
|
.filter(f => f.endsWith('.sql'))
|
|
.sort(); // Sort alphabetically (001_, 002_, etc.)
|
|
}
|
|
|
|
/**
|
|
* Run a single migration file
|
|
*/
|
|
async function runMigration(pool: Pool, filename: string): Promise<void> {
|
|
const filepath = path.join(MIGRATIONS_DIR, filename);
|
|
const sql = fs.readFileSync(filepath, 'utf8');
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
// Run the migration SQL
|
|
await client.query(sql);
|
|
|
|
// Record that this migration was applied
|
|
await client.query(
|
|
'INSERT INTO schema_migrations (name) VALUES ($1) ON CONFLICT (name) DO NOTHING',
|
|
[filename]
|
|
);
|
|
|
|
await client.query('COMMIT');
|
|
console.log(`[AutoMigrate] ✓ Applied: ${filename}`);
|
|
} catch (error: any) {
|
|
await client.query('ROLLBACK');
|
|
console.error(`[AutoMigrate] ✗ Failed: ${filename}`);
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run all pending migrations
|
|
*
|
|
* @param pool - Database connection pool
|
|
* @returns Number of migrations applied
|
|
*/
|
|
export async function runAutoMigrations(pool: Pool): Promise<number> {
|
|
console.log('[AutoMigrate] Checking for pending migrations...');
|
|
|
|
try {
|
|
// Ensure migrations table exists
|
|
await ensureMigrationsTable(pool);
|
|
|
|
// Get applied and available migrations
|
|
const applied = await getAppliedMigrations(pool);
|
|
const available = getMigrationFiles();
|
|
|
|
// Find pending migrations
|
|
const pending = available.filter(f => !applied.has(f));
|
|
|
|
if (pending.length === 0) {
|
|
console.log('[AutoMigrate] No pending migrations');
|
|
return 0;
|
|
}
|
|
|
|
console.log(`[AutoMigrate] Found ${pending.length} pending migrations`);
|
|
|
|
// Run each pending migration in order
|
|
for (const filename of pending) {
|
|
await runMigration(pool, filename);
|
|
}
|
|
|
|
console.log(`[AutoMigrate] Successfully applied ${pending.length} migrations`);
|
|
return pending.length;
|
|
|
|
} catch (error: any) {
|
|
console.error('[AutoMigrate] Migration failed:', error.message);
|
|
// Don't crash the server - log and continue
|
|
// The specific failing migration will have been rolled back
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check migration status without running anything
|
|
*/
|
|
export async function checkMigrationStatus(pool: Pool): Promise<{
|
|
applied: string[];
|
|
pending: string[];
|
|
}> {
|
|
await ensureMigrationsTable(pool);
|
|
|
|
const applied = await getAppliedMigrations(pool);
|
|
const available = getMigrationFiles();
|
|
|
|
return {
|
|
applied: available.filter(f => applied.has(f)),
|
|
pending: available.filter(f => !applied.has(f)),
|
|
};
|
|
}
|