feat(week1): complete foundation - schema, migrations, enhanced SQL, sanitizer
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { query } from '../db';
|
||||
import { query, pool } from '../db';
|
||||
|
||||
export const MECHANIC_OPS = {
|
||||
// 1. DIAGNOSTICS (The Stethoscope)
|
||||
@@ -53,3 +53,65 @@ export const MECHANIC_OPS = {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Kill stuck database locks/queries
|
||||
* Returns number of processes terminated
|
||||
*/
|
||||
export async function killLocks(): Promise<number> {
|
||||
const result = await query(`
|
||||
SELECT pg_terminate_backend(pid)
|
||||
FROM pg_stat_activity
|
||||
WHERE state = 'active'
|
||||
AND query_start < NOW() - INTERVAL '30 seconds'
|
||||
AND pid <> pg_backend_pid()
|
||||
`);
|
||||
|
||||
console.log(`🔧 [Mechanic] Killed ${result.rowCount} stuck processes`);
|
||||
return result.rowCount || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run VACUUM ANALYZE on a table or entire database
|
||||
* NOTE: Must be run outside of transaction
|
||||
*/
|
||||
export async function vacuumAnalyze(tableName?: string): Promise<void> {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const table = tableName || '';
|
||||
const sql = table ? `VACUUM ANALYZE ${table}` : 'VACUUM ANALYZE';
|
||||
|
||||
console.log(`🔧 [Mechanic] Running: ${sql}`);
|
||||
await client.query(sql);
|
||||
console.log(`✅ [Mechanic] Vacuum complete`);
|
||||
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table bloat statistics
|
||||
*/
|
||||
export async function getTableBloat(): Promise<any[]> {
|
||||
const result = await query(`
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size,
|
||||
n_dead_tup as dead_rows,
|
||||
n_live_tup as live_rows,
|
||||
CASE
|
||||
WHEN n_live_tup > 0
|
||||
THEN round(100.0 * n_dead_tup / n_live_tup, 2)
|
||||
ELSE 0
|
||||
END as bloat_pct
|
||||
FROM pg_stat_user_tables
|
||||
WHERE n_dead_tup > 0
|
||||
ORDER BY n_dead_tup DESC
|
||||
LIMIT 20
|
||||
`);
|
||||
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
100
src/lib/db/migrate.ts
Normal file
100
src/lib/db/migrate.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { pool } from './db';
|
||||
|
||||
/**
|
||||
* Migration System for God Mode
|
||||
* Handles transactional execution of SQL migration files
|
||||
*/
|
||||
|
||||
export interface MigrationResult {
|
||||
success: boolean;
|
||||
migrationsRun: number;
|
||||
error?: string;
|
||||
rolledBack?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run multiple SQL commands in a single transaction
|
||||
* Automatically rolls back if ANY command fails
|
||||
*/
|
||||
export async function runMigrations(sqlCommands: string[]): Promise<MigrationResult> {
|
||||
const client = await pool.connect();
|
||||
let migrationsRun = 0;
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
console.log('🔱 [Migration] Starting transaction...');
|
||||
|
||||
for (const command of sqlCommands) {
|
||||
// Skip empty commands or comments
|
||||
const trimmed = command.trim();
|
||||
if (!trimmed || trimmed.startsWith('--')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`[Migration] Executing: ${trimmed.substring(0, 100)}...`);
|
||||
await client.query(trimmed);
|
||||
migrationsRun++;
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log(`✅ [Migration] Successfully committed ${migrationsRun} migrations`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
migrationsRun
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('❌ [Migration] Error - Rolling back all changes:', error.message);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
migrationsRun,
|
||||
error: error.message,
|
||||
rolledBack: true
|
||||
};
|
||||
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single large SQL file (like migrations)
|
||||
* Splits by semicolon and runs each statement in transaction
|
||||
*/
|
||||
export async function runMigrationFile(sqlContent: string): Promise<MigrationResult> {
|
||||
// Split by semicolon, but be smart about it
|
||||
const statements = sqlContent
|
||||
.split(';')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
|
||||
return runMigrations(statements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if migrations have been run
|
||||
*/
|
||||
export async function getMigrationStatus(): Promise<{
|
||||
tables: string[];
|
||||
lastMigration?: Date;
|
||||
}> {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name
|
||||
`);
|
||||
|
||||
return {
|
||||
tables: result.rows.map(r => r.table_name)
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
tables: []
|
||||
};
|
||||
}
|
||||
}
|
||||
135
src/lib/db/sanitizer.ts
Normal file
135
src/lib/db/sanitizer.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* SQL Sanitizer - The Last Line of Defense
|
||||
* Validates raw SQL before execution to prevent catastrophic accidents
|
||||
*/
|
||||
|
||||
export interface SanitizationResult {
|
||||
safe: boolean;
|
||||
warnings: string[];
|
||||
blocked?: string;
|
||||
}
|
||||
|
||||
// Dangerous patterns that should NEVER be allowed
|
||||
const BLOCKED_PATTERNS = [
|
||||
/DROP\s+DATABASE/i,
|
||||
/DROP\s+SCHEMA/i,
|
||||
/ALTER\s+USER/i,
|
||||
/ALTER\s+ROLE/i,
|
||||
/CREATE\s+USER/i,
|
||||
/CREATE\s+ROLE/i,
|
||||
/GRANT\s+.*\s+TO/i,
|
||||
/REVOKE\s+.*\s+FROM/i,
|
||||
];
|
||||
|
||||
// Patterns that require special attention
|
||||
const WARNING_PATTERNS = [
|
||||
{ pattern: /TRUNCATE/i, message: 'TRUNCATE detected - use with caution' },
|
||||
{ pattern: /DROP\s+TABLE/i, message: 'DROP TABLE detected - irreversible' },
|
||||
{ pattern: /DELETE\s+FROM\s+\w+\s*;/i, message: 'DELETE without WHERE clause - will delete ALL rows' },
|
||||
{ pattern: /UPDATE\s+\w+\s+SET\s+.*\s*;/i, message: 'UPDATE without WHERE clause - will update ALL rows' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate table name is safe (alphanumeric + underscores only)
|
||||
*/
|
||||
export function validateTableName(tableName: string): boolean {
|
||||
return /^[a-zA-Z0-9_]+$/.test(tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize raw SQL before execution
|
||||
*/
|
||||
export function sanitizeSQL(sql: string): SanitizationResult {
|
||||
const trimmed = sql.trim();
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Check for blocked patterns
|
||||
for (const pattern of BLOCKED_PATTERNS) {
|
||||
if (pattern.test(trimmed)) {
|
||||
return {
|
||||
safe: false,
|
||||
warnings: [],
|
||||
blocked: `Blocked dangerous command: ${pattern.source}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for warning patterns
|
||||
for (const { pattern, message } of WARNING_PATTERNS) {
|
||||
if (pattern.test(trimmed)) {
|
||||
warnings.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate table names in common operations
|
||||
const tableMatches = trimmed.match(/(?:FROM|INTO|UPDATE|TRUNCATE|DROP TABLE)\s+([a-zA-Z0-9_]+)/gi);
|
||||
if (tableMatches) {
|
||||
for (const match of tableMatches) {
|
||||
const tableName = match.split(/\s+/).pop() || '';
|
||||
if (!validateTableName(tableName)) {
|
||||
return {
|
||||
safe: false,
|
||||
warnings: [],
|
||||
blocked: `Invalid table name: ${tableName}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
safe: true,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract table names from SQL query
|
||||
*/
|
||||
export function extractTableNames(sql: string): string[] {
|
||||
const matches = sql.match(/(?:FROM|INTO|UPDATE|TRUNCATE|DROP TABLE)\s+([a-zA-Z0-9_]+)/gi);
|
||||
if (!matches) return [];
|
||||
|
||||
return matches.map(m => m.split(/\s+/).pop() || '').filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SQL contains WHERE clause
|
||||
*/
|
||||
export function hasWhereClause(sql: string): boolean {
|
||||
return /WHERE\s+/i.test(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* More permissive check for maintenance window
|
||||
* Used when run_mechanic flag is set
|
||||
*/
|
||||
export function sanitizeSQLForMaintenance(sql: string): SanitizationResult {
|
||||
const trimmed = sql.trim();
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Still block the most dangerous commands
|
||||
const criticalPatterns = [
|
||||
/DROP\s+DATABASE/i,
|
||||
/DROP\s+SCHEMA/i,
|
||||
/ALTER\s+USER/i,
|
||||
/CREATE\s+USER/i,
|
||||
];
|
||||
|
||||
for (const pattern of criticalPatterns) {
|
||||
if (pattern.test(trimmed)) {
|
||||
return {
|
||||
safe: false,
|
||||
warnings: [],
|
||||
blocked: `Blocked critical command even in maintenance mode: ${pattern.source}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Allow TRUNCATE and DROP TABLE in maintenance mode
|
||||
warnings.push('Maintenance mode: dangerous operations allowed');
|
||||
|
||||
return {
|
||||
safe: true,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user