Files
EpicNext-Cms/scripts/apply-migrations.ts
T
openhands 942bc6fc8d Security hardening, code quality, and ESLint setup
- Remove production DB dump (db_backup_*.sql) and update.log from git tracking
- Add DB backups to .gitignore
- Replace all console.log/console.error with structured logger module
- Translate Dutch error messages to English (link-discord.ts)
- Remove dead code blocks (register-form.tsx false && pattern)
- Add ESLint flat config with TypeScript, React, Next.js, jsx-a11y, and security plugins
- Add Prettier config
- Add eslint-plugin-security for security-aware linting
- Fix all 119+ ESLint warnings across the codebase:
  - Resolve security/detect-object-injection with safe access patterns
  - Resolve security/detect-non-literal-fs-filename with path traversal validation
  - Replace <img> with next/image <Image> component
  - Remove unused variables and imports
  - Replace non-null assertions with proper type guards
  - Replace <a> with <Link> for internal navigation
  - Use next/script Script component for external scripts
- Fix setState-in-useEffect anti-patterns (navbar-color-picker, logo-generator, theme-switcher)
- Add lint and format scripts to package.json

All checks: typecheck ✓, tests 58/58 ✓, lint 0 errors 0 warnings ✓
2026-07-10 22:48:22 +02:00

138 lines
3.8 KiB
TypeScript

import { readFileSync, readdirSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const MIGRATIONS_DIR = resolve(__dirname, "../prisma/migrations");
const TRACKING_TABLE = "cms_migrations";
interface MigrationFile {
id: string;
name: string;
sql: string;
}
function getDbConfig(): { url: string; database: string } {
const url = process.env.DATABASE_URL;
if (!url) throw new Error("DATABASE_URL is required");
const dbName = url.split("/").pop()?.split("?")[0] ?? "atomcms";
return { url, database: dbName };
}
async function ensureConnection(): Promise<void> {
const { url } = getDbConfig();
const mysql = await import("mysql2/promise");
const conn = await mysql.createConnection(url);
try {
await conn.execute(
`CREATE TABLE IF NOT EXISTS \`${TRACKING_TABLE}\` (
id INT AUTO_INCREMENT PRIMARY KEY,
migration VARCHAR(255) NOT NULL UNIQUE,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
);
} finally {
await conn.end();
}
}
async function getApplied(): Promise<Set<string>> {
const { url } = getDbConfig();
const mysql = await import("mysql2/promise");
const conn = await mysql.createConnection(url);
try {
const [rows] = await conn.execute(
`SELECT migration FROM \`${TRACKING_TABLE}\` ORDER BY id`,
);
return new Set((rows as { migration: string }[]).map((r) => r.migration));
} catch {
return new Set();
} finally {
await conn.end();
}
}
function loadMigrations(): MigrationFile[] {
const entries = readdirSync(MIGRATIONS_DIR, { withFileTypes: true });
const files = entries
.filter((e) => e.isFile() && e.name.endsWith(".sql"))
.sort((a, b) => a.name.localeCompare(b.name));
return files.map((f) => {
const id = f.name.replace(/\.sql$/, "");
const sql = readFileSync(resolve(MIGRATIONS_DIR, f.name), "utf-8");
return { id, name: f.name, sql };
});
}
async function apply(migration: MigrationFile): Promise<void> {
const { url } = getDbConfig();
const mysql = await import("mysql2/promise");
const conn = await mysql.createConnection(url);
try {
const statements = migration.sql
.split(";")
.map((s) => s.trim())
.filter((s) => s.length > 0 && !s.startsWith("--"));
for (const stmt of statements) {
await conn.execute(stmt);
}
await conn.execute(
`INSERT INTO \`${TRACKING_TABLE}\` (migration) VALUES (?)`,
[migration.id],
);
console.log(`[migrate] Applied: ${migration.name}`);
} finally {
await conn.end();
}
}
async function main() {
const flag = process.argv[2];
if (flag === "--status") {
await ensureConnection();
const applied = await getApplied();
const all = loadMigrations();
console.log("\nMigration status:\n");
for (const m of all) {
const done = applied.has(m.id);
console.log(` ${done ? "✓" : " "} ${m.name}${done ? "" : " [PENDING]"}`);
}
const pending = all.filter((m) => !applied.has(m.id));
const total = all.length;
const done = total - pending.length;
console.log(`\n${done}/${total} applied, ${pending.length} pending\n`);
return;
}
await ensureConnection();
const applied = await getApplied();
const pending = loadMigrations().filter((m) => !applied.has(m.id));
if (pending.length === 0) {
console.log("[migrate] All migrations already applied.");
return;
}
console.log(`[migrate] Applying ${pending.length} migration(s)...\n`);
for (const m of pending) {
try {
await apply(m);
} catch (err) {
console.error(`[migrate] FAILED: ${m.name}`, err);
process.exit(1);
}
}
console.log("\n[migrate] Done.");
}
main().catch((err) => {
console.error("[migrate] Fatal:", err);
process.exit(1);
});