mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 01:21:09 +02:00
- Add safe-db-push.mjs script for safer database migrations - Update docker-entrypoint.sh with db:push fallback when migrations fail - Add validate-migrations.mjs script for CI migration validation - Update CI workflow to use migration validation - Update drizzle.config.ts with improved configuration
204 lines
5.5 KiB
JavaScript
204 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Migration Validation Script
|
|
*
|
|
* Scans migration files for destructive SQL patterns and fails CI if found.
|
|
* This prevents accidental data loss in production deployments.
|
|
*
|
|
* Usage:
|
|
* node scripts/validate-migrations.mjs [--allow-destructive]
|
|
*
|
|
* Destructive patterns detected:
|
|
* - DROP TABLE
|
|
* - DROP COLUMN
|
|
* - DROP INDEX (without CONCURRENTLY)
|
|
* - DROP SCHEMA
|
|
* - TRUNCATE
|
|
* - DELETE FROM (without WHERE)
|
|
*
|
|
* Safe patterns (allowed):
|
|
* - DROP ... IF EXISTS (only creates if not exists)
|
|
* - CREATE ... IF NOT EXISTS
|
|
* - ALTER TABLE ... ADD COLUMN
|
|
* - CREATE INDEX CONCURRENTLY
|
|
*/
|
|
|
|
import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
// Configuration
|
|
const MIGRATION_DIRS = [
|
|
'services/mana-core-auth/src/db/migrations',
|
|
// Add other services as needed
|
|
];
|
|
|
|
// Destructive patterns to detect
|
|
const DESTRUCTIVE_PATTERNS = [
|
|
{
|
|
pattern: /DROP\s+TABLE(?!\s+IF\s+EXISTS)/gi,
|
|
message: 'DROP TABLE without IF EXISTS - will fail if table does not exist and is destructive',
|
|
severity: 'error',
|
|
},
|
|
{
|
|
pattern: /DROP\s+TABLE\s+IF\s+EXISTS\s+(?!.*CASCADE\s*;)/gi,
|
|
message: 'DROP TABLE IF EXISTS - THIS WILL DELETE DATA',
|
|
severity: 'error',
|
|
},
|
|
{
|
|
pattern: /DROP\s+TABLE.*CASCADE/gi,
|
|
message: 'DROP TABLE CASCADE - THIS WILL DELETE DATA AND DEPENDENT OBJECTS',
|
|
severity: 'error',
|
|
},
|
|
{
|
|
pattern: /ALTER\s+TABLE\s+\S+\s+DROP\s+COLUMN/gi,
|
|
message: 'DROP COLUMN - THIS WILL DELETE DATA',
|
|
severity: 'error',
|
|
},
|
|
{
|
|
pattern: /DROP\s+SCHEMA(?!\s+IF\s+EXISTS)/gi,
|
|
message: 'DROP SCHEMA without IF EXISTS',
|
|
severity: 'error',
|
|
},
|
|
{
|
|
pattern: /DROP\s+SCHEMA\s+IF\s+EXISTS.*CASCADE/gi,
|
|
message: 'DROP SCHEMA CASCADE - THIS WILL DELETE ALL TABLES IN SCHEMA',
|
|
severity: 'error',
|
|
},
|
|
{
|
|
pattern: /TRUNCATE\s+(?:TABLE\s+)?/gi,
|
|
message: 'TRUNCATE - THIS WILL DELETE ALL DATA IN TABLE',
|
|
severity: 'error',
|
|
},
|
|
{
|
|
pattern: /DELETE\s+FROM\s+\S+\s*(?:;|$)/gim,
|
|
message: 'DELETE FROM without WHERE clause - THIS WILL DELETE ALL DATA',
|
|
severity: 'error',
|
|
},
|
|
{
|
|
pattern: /DROP\s+INDEX(?!\s+CONCURRENTLY)/gi,
|
|
message: 'DROP INDEX without CONCURRENTLY - may cause table locks',
|
|
severity: 'warning',
|
|
},
|
|
];
|
|
|
|
// Safe patterns that override destructive checks
|
|
const SAFE_PATTERNS = [
|
|
// These patterns indicate safe, idempotent operations
|
|
/CREATE\s+(?:TABLE|INDEX|SCHEMA|TYPE)\s+IF\s+NOT\s+EXISTS/gi,
|
|
/DO\s+\$\$.*EXCEPTION.*WHEN\s+duplicate_object/gis, // Safe enum creation pattern
|
|
];
|
|
|
|
function findMigrationFiles(dir) {
|
|
const files = [];
|
|
|
|
if (!existsSync(dir)) {
|
|
return files;
|
|
}
|
|
|
|
const entries = readdirSync(dir);
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = join(dir, entry);
|
|
const stat = statSync(fullPath);
|
|
|
|
if (stat.isDirectory() && entry !== 'meta') {
|
|
// Check subdirectories for .sql files
|
|
files.push(...findMigrationFiles(fullPath));
|
|
} else if (entry.endsWith('.sql')) {
|
|
files.push(fullPath);
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
function validateMigration(filePath) {
|
|
const content = readFileSync(filePath, 'utf-8');
|
|
const issues = [];
|
|
|
|
// Check if file uses safe patterns throughout (reserved for future use)
|
|
// const isSafeFile = SAFE_PATTERNS.some((pattern) => pattern.test(content));
|
|
void SAFE_PATTERNS; // Silence unused warning - patterns reserved for future enhancements
|
|
|
|
for (const { pattern, message, severity } of DESTRUCTIVE_PATTERNS) {
|
|
// Reset regex lastIndex
|
|
pattern.lastIndex = 0;
|
|
|
|
let match;
|
|
while ((match = pattern.exec(content)) !== null) {
|
|
// Find line number
|
|
const beforeMatch = content.substring(0, match.index);
|
|
const lineNumber = (beforeMatch.match(/\n/g) || []).length + 1;
|
|
|
|
issues.push({
|
|
file: filePath,
|
|
line: lineNumber,
|
|
message,
|
|
severity,
|
|
match: match[0].trim(),
|
|
});
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
function main() {
|
|
const args = process.argv.slice(2);
|
|
const allowDestructive = args.includes('--allow-destructive');
|
|
|
|
console.log('🔍 Validating migration files for destructive patterns...\n');
|
|
|
|
const allIssues = [];
|
|
let filesChecked = 0;
|
|
|
|
for (const dir of MIGRATION_DIRS) {
|
|
const files = findMigrationFiles(dir);
|
|
filesChecked += files.length;
|
|
|
|
for (const file of files) {
|
|
const issues = validateMigration(file);
|
|
allIssues.push(...issues);
|
|
}
|
|
}
|
|
|
|
// Separate errors and warnings
|
|
const errors = allIssues.filter((i) => i.severity === 'error');
|
|
const warnings = allIssues.filter((i) => i.severity === 'warning');
|
|
|
|
console.log(`📁 Checked ${filesChecked} migration files\n`);
|
|
|
|
if (warnings.length > 0) {
|
|
console.log('⚠️ WARNINGS:\n');
|
|
for (const issue of warnings) {
|
|
console.log(` ${issue.file}:${issue.line}`);
|
|
console.log(` ${issue.message}`);
|
|
console.log(` Found: ${issue.match}\n`);
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
console.log('❌ DESTRUCTIVE PATTERNS DETECTED:\n');
|
|
for (const issue of errors) {
|
|
console.log(` ${issue.file}:${issue.line}`);
|
|
console.log(` ${issue.message}`);
|
|
console.log(` Found: ${issue.match}\n`);
|
|
}
|
|
|
|
if (allowDestructive) {
|
|
console.log('⚠️ --allow-destructive flag set, continuing despite errors\n');
|
|
console.log('🟡 Migration validation passed with warnings');
|
|
process.exit(0);
|
|
} else {
|
|
console.log('💡 To proceed with destructive migrations, use --allow-destructive flag');
|
|
console.log(' Or review and update the migration to use safe patterns.\n');
|
|
console.log('❌ Migration validation FAILED');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
console.log('✅ Migration validation passed - no destructive patterns found');
|
|
process.exit(0);
|
|
}
|
|
|
|
main();
|