FactuGES_ERP/scripts/dev/database.ts

135 lines
4.6 KiB
TypeScript

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<string, DatabaseCommand>(
DATABASE_COMMANDS.map((command) => [command, command])
);
async function main(): Promise<void> {
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<string, string[]> = {
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<number>((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<typeof readMigrationConfig>,
applicationUser: string | undefined
): Promise<void> {
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();
}
}