135 lines
4.6 KiB
TypeScript
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();
|
||
|
|
}
|
||
|
|
}
|