import { spawn } from "node:child_process"; import { randomBytes } from "node:crypto"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { parseEnv } from "node:util"; import { createConnection } from "../../apps/server/node_modules/mysql2/promise"; import { readMigrationConfig } from "../../apps/server/src/infrastructure/database/migration-config"; import type { DatabaseCommand } from "../../modules/core/src/api/infrastructure/database/migration-runner"; import { DATABASE_COMMANDS, runDatabaseCommand, } from "../../modules/core/src/api/infrastructure/database/migration-runner"; import { CURRENT_SCHEMA_STATEMENTS, tableNameFromCreate, } from "../../modules/core/src/api/infrastructure/database/schema-contract"; const environmentFile = "infrastructure/database/.env"; const migrationCommands = new Map( DATABASE_COMMANDS.map((command) => [command, command]) ); async function main(): Promise { if (!existsSync(environmentFile)) { writeFileSync( environmentFile, [ "DB_HOST=127.0.0.1", "DB_PORT=3307", "DB_NAME=factuges_erp_development", "DB_USER=factuges_app", `DB_PASS=${randomBytes(24).toString("hex")}`, `DB_ROOT_PASS=${randomBytes(24).toString("hex")}`, "DB_SYNC_MODE=none", "", ].join("\n"), { mode: 0o600, flag: "wx" } ); console.info(`Created local database configuration: ${environmentFile}`); } // Solo se usa este fichero local; las variables del shell no cambian el destino. const environment = parseEnv(readFileSync(environmentFile, "utf8")); if (environment.DB_HOST !== "127.0.0.1" || environment.DB_NAME !== "factuges_erp_development") { throw new Error("Development commands require the local factuges_erp_development database."); } const action = process.argv[2]; const migrationCommand = action ? migrationCommands.get(action) : undefined; if (migrationCommand) { const config = readMigrationConfig({ ...environment, DB_USER: "root", DB_PASS: environment.DB_ROOT_PASS, }); const result = await runDatabaseCommand(config, migrationCommand); if (["migrate", "adopt", "recover"].includes(migrationCommand)) { await grantDevelopmentAccess(config, environment.DB_USER); } console.info(JSON.stringify(result, null, 2)); return; } const actions: Record = { up: ["up", "--detach", "--wait", "--wait-timeout", "120"], down: ["down"], logs: ["logs", "--tail", "100"], reset: ["down", "--volumes"], }; const args = action ? actions[action] : undefined; if (!args) { throw new Error("Unknown development database command."); } if (action === "reset" && !process.argv.includes("--confirm-reset")) { throw new Error("Reset deletes local development data. Use pnpm db:dev:reset --confirm-reset."); } const code = await new Promise((resolve, reject) => { const child = spawn( "docker", [ "compose", "--env-file", environmentFile, "--project-name", "factuges-development", "--file", "infrastructure/database/compose.development.yaml", ...args, ], { env: { ...process.env, ...environment }, stdio: "inherit", } ); child.once("error", reject); child.once("close", (exitCode) => resolve(exitCode ?? 1)); }); if (code !== 0) { throw new Error("Development database Compose command failed."); } } main().catch((error: unknown) => { console.error( error instanceof Error && error.constructor === Error && !error.cause && !("sql" in error) && !("code" in error) ? error.message : "Database command failed. Inspect Docker and db:status." ); process.exitCode = 1; }); async function grantDevelopmentAccess( config: ReturnType, applicationUser: string | undefined ): Promise { if (!applicationUser || applicationUser === "root" || !/^[a-zA-Z0-9_]+$/.test(applicationUser)) { throw new Error("Invalid development application user."); } const connection = await createConnection(config); try { await connection.query("REVOKE ALL PRIVILEGES, GRANT OPTION FROM ?@'%'", [applicationUser]); await connection.query(`GRANT SELECT ON \`${config.database}\`.* TO ?@'%'`, [applicationUser]); for (const statement of CURRENT_SCHEMA_STATEMENTS) { const table = tableNameFromCreate(statement); await connection.query( `GRANT INSERT, UPDATE, DELETE ON \`${config.database}\`.\`${table}\` TO ?@'%'`, [applicationUser] ); } } finally { await connection.end(); } }