feat(week1): complete foundation - schema, migrations, enhanced SQL, sanitizer
This commit is contained in:
92
src/pages/api/god/schema/init.ts
Normal file
92
src/pages/api/god/schema/init.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { runMigrationFile, getMigrationStatus } from '@/lib/db/migrate';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
/**
|
||||
* Schema Initialization Endpoint
|
||||
* Executes migration files with transactional safety
|
||||
*/
|
||||
|
||||
function validateGodToken(request: Request): boolean {
|
||||
const token = request.headers.get('X-God-Token') ||
|
||||
request.headers.get('Authorization')?.replace('Bearer ', '') ||
|
||||
new URL(request.url).searchParams.get('token');
|
||||
|
||||
const godToken = process.env.GOD_MODE_TOKEN || import.meta.env.GOD_MODE_TOKEN;
|
||||
if (!godToken) return true; // Dev mode
|
||||
return token === godToken;
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the main migration file
|
||||
const migrationPath = join(process.cwd(), 'migrations', '01_init_complete.sql');
|
||||
const sqlContent = await readFile(migrationPath, 'utf-8');
|
||||
|
||||
// Run migrations in transaction
|
||||
const result = await runMigrationFile(sqlContent);
|
||||
|
||||
if (!result.success) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Migration failed',
|
||||
details: result.error,
|
||||
rolledBack: result.rolledBack,
|
||||
migrationsRun: result.migrationsRun
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Get final status
|
||||
const status = await getMigrationStatus();
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
message: '🔱 Database schema initialized successfully',
|
||||
migrationsRun: result.migrationsRun,
|
||||
tablesCreated: status.tables,
|
||||
timestamp: new Date().toISOString()
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Failed to read or execute migration file',
|
||||
details: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const status = await getMigrationStatus();
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
tables: status.tables,
|
||||
tableCount: status.tables.length,
|
||||
initialized: status.tables.length > 0
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
236
src/pages/api/god/sql.ts
Normal file
236
src/pages/api/god/sql.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { pool } from '@/lib/db';
|
||||
import { sanitizeSQL, sanitizeSQLForMaintenance } from '@/lib/db/sanitizer';
|
||||
import { vacuumAnalyze, killLocks } from '@/lib/db/mechanic';
|
||||
import { queues } from '@/lib/queue/config';
|
||||
|
||||
/**
|
||||
* Enhanced SQL Endpoint - The Ultimate God Mode Feature
|
||||
*
|
||||
* Features:
|
||||
* 1. Multi-statement transactions (auto-rollback on failure)
|
||||
* 2. SQL sanitization (blocks dangerous commands)
|
||||
* 3. Mechanic integration (auto-cleanup after large ops)
|
||||
* 4. Queue injection (push results to BullMQ)
|
||||
*/
|
||||
|
||||
function validateGodToken(request: Request): boolean {
|
||||
const token = request.headers.get('X-God-Token') ||
|
||||
request.headers.get('Authorization')?.replace('Bearer ', '') ||
|
||||
new URL(request.url).searchParams.get('token');
|
||||
|
||||
const godToken = process.env.GOD_MODE_TOKEN || import.meta.env.GOD_MODE_TOKEN;
|
||||
if (!godToken) return true;
|
||||
return token === godToken;
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const {
|
||||
query,
|
||||
run_mechanic, // 'vacuum' | 'kill-locks'
|
||||
push_to_queue, // 'publish_job' | 'generate_article'
|
||||
transaction = true // Auto-wrap in transaction if multiple statements
|
||||
} = body;
|
||||
|
||||
if (!query) {
|
||||
return new Response(JSON.stringify({ error: 'Missing query' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Step 1: SQL Sanitization
|
||||
const sanitizationMode = run_mechanic ? 'maintenance' : 'normal';
|
||||
const sanitizationResult = sanitizationMode === 'maintenance'
|
||||
? sanitizeSQLForMaintenance(query)
|
||||
: sanitizeSQL(query);
|
||||
|
||||
if (!sanitizationResult.safe) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'SQL blocked by safety check',
|
||||
reason: sanitizationResult.blocked,
|
||||
tip: 'Use run_mechanic flag for maintenance operations'
|
||||
}), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Step 2: Split into statements for transaction handling
|
||||
const statements = query.split(';').map((s: string) => s.trim()).filter(Boolean);
|
||||
const isMultiStatement = statements.length > 1;
|
||||
|
||||
// Step 3: Execute Query
|
||||
const client = await pool.connect();
|
||||
let result;
|
||||
let rowsAffected = 0;
|
||||
|
||||
try {
|
||||
if (isMultiStatement && transaction) {
|
||||
// Multi-statement transaction
|
||||
await client.query('BEGIN');
|
||||
console.log(`🔱 [SQL] Starting transaction with ${statements.length} statements`);
|
||||
|
||||
const results = [];
|
||||
for (const stmt of statements) {
|
||||
const r = await client.query(stmt);
|
||||
results.push(r);
|
||||
rowsAffected += r.rowCount || 0;
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log(`✅ [SQL] Transaction committed - ${rowsAffected} rows affected`);
|
||||
|
||||
result = results[results.length - 1]; // Return last result
|
||||
|
||||
} else {
|
||||
// Single statement
|
||||
result = await client.query(query);
|
||||
rowsAffected = result.rowCount || 0;
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
if (isMultiStatement && transaction) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error(`❌ [SQL] Transaction rolled back: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
// Step 4: Mechanic Integration
|
||||
let mechanicResult;
|
||||
if (run_mechanic) {
|
||||
console.log(`🔧 [Mechanic] Running ${run_mechanic}...`);
|
||||
|
||||
if (run_mechanic === 'vacuum') {
|
||||
await vacuumAnalyze();
|
||||
mechanicResult = { action: 'vacuum', status: 'completed' };
|
||||
} else if (run_mechanic === 'kill-locks') {
|
||||
const killed = await killLocks();
|
||||
mechanicResult = { action: 'kill-locks', killed };
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Queue Injection
|
||||
let queueResult;
|
||||
if (push_to_queue && result.rows && result.rows.length > 0) {
|
||||
console.log(`📋 [Queue] Pushing ${result.rows.length} rows to ${push_to_queue}`);
|
||||
|
||||
const jobs = result.rows.map((row: any, index: number) => ({
|
||||
name: `${push_to_queue}-${index}`,
|
||||
data: row
|
||||
}));
|
||||
|
||||
// Use addBulk for efficiency
|
||||
const queue = queues.generation; // Default queue
|
||||
await queue.addBulk(jobs);
|
||||
|
||||
queueResult = {
|
||||
queue: push_to_queue,
|
||||
jobsAdded: jobs.length
|
||||
};
|
||||
}
|
||||
|
||||
// Step 6: Build Response
|
||||
const response: any = {
|
||||
success: true,
|
||||
rowCount: result.rowCount,
|
||||
rows: result.rows,
|
||||
command: result.command,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (sanitizationResult.warnings.length > 0) {
|
||||
response.warnings = sanitizationResult.warnings;
|
||||
}
|
||||
|
||||
if (isMultiStatement) {
|
||||
response.transaction = {
|
||||
statements: statements.length,
|
||||
committed: true,
|
||||
rowsAffected
|
||||
};
|
||||
}
|
||||
|
||||
if (mechanicResult) {
|
||||
response.mechanic = mechanicResult;
|
||||
}
|
||||
|
||||
if (queueResult) {
|
||||
response.queue = queueResult;
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(response), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
return new Response(JSON.stringify({
|
||||
error: error.message,
|
||||
code: error.code,
|
||||
detail: error.detail,
|
||||
hint: error.hint
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// GET for documentation
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
if (!validateGodToken(request)) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
endpoint: 'POST /api/god/sql',
|
||||
description: 'Execute raw SQL with enhanced features',
|
||||
features: {
|
||||
'1_transactions': 'Auto-wrap multiple statements in BEGIN/COMMIT',
|
||||
'2_sanitization': 'Blocks dangerous commands (DROP DATABASE, etc)',
|
||||
'3_mechanic': 'Auto-cleanup with run_mechanic flag',
|
||||
'4_queue_injection': 'Push results to BullMQ with push_to_queue flag'
|
||||
},
|
||||
usage: {
|
||||
basic: {
|
||||
query: 'SELECT * FROM sites LIMIT 10'
|
||||
},
|
||||
transaction: {
|
||||
query: 'INSERT INTO sites (domain, name) VALUES (\'example.com\', \'Example\'); UPDATE sites SET status=\'active\';',
|
||||
transaction: true
|
||||
},
|
||||
with_vacuum: {
|
||||
query: 'DELETE FROM posts WHERE status=\'draft\'',
|
||||
run_mechanic: 'vacuum'
|
||||
},
|
||||
queue_injection: {
|
||||
query: 'SELECT id, url FROM posts WHERE status=\'draft\'',
|
||||
push_to_queue: 'publish_job'
|
||||
}
|
||||
},
|
||||
safety: {
|
||||
blocked: ['DROP DATABASE', 'ALTER USER', 'DELETE without WHERE'],
|
||||
warnings: ['TRUNCATE', 'DROP TABLE', 'UPDATE without WHERE']
|
||||
}
|
||||
}, null, 2), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user