#!/usr/bin/node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fs_1 = require("fs"); const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const pg_1 = require("pg"); const process_1 = require("process"); const yargs_1 = __importDefault(require("yargs")); const helpers_1 = require("yargs/helpers"); /** * Carga de variables de entorno manual (para evitar dependencias como dotenv) * Busca el .env en el directorio actual (CWD) */ async function loadEnv(envpath) { const env = process.env; const envPath = envpath; if (envPath != undefined && (0, fs_1.existsSync)(envPath)) { process.loadEnvFile(envpath); console.log('[i] Archivo .env cargado desde ' + envpath); console.log("ENV:", env); } else { console.log("[i] Variables de entorno locales"); } } /** * Parseo de argumentos manual: --target */ function getArgs() { const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)) .option("target", { alias: "t", type: 'string', description: "Versión objetivo de la migracion", requiresArg: false }) .option("env", { alias: "e", type: 'string', description: "Path del archivo .env con los datos de la BDD" }) .option("migrations", { alias: "m", type: "string", description: "Path del directorio de migrations" }) .option("versionTable", { alias: "v", type: "string", description: "Nombre de la tabla donde se almacenan las versiones de la BDD, por defecto 'db_versions'", default: "db_versions" }) .option("baseVersion", { alias: "b", type: "string", description: "Versión a partir de la cual se aplican las migraciones independientemente de la almacenada en BDD" }) .parse(); //console.log("args", argv) return { target: argv.target, env: argv.env, migrations: argv.migrations, versionTable: argv.versionTable, baseVersion: argv.baseVersion }; } function versionToValue(version) { return version.split("_")[0] .split(".") .slice(0, 3) .map(e => parseInt(e)); } /** * Para poder ordenar las verisones con 1 - 3 valores */ function compareVersions(va, vb) { const { max } = Math; const partesa = versionToValue(va); const partesb = versionToValue(vb); const maxLen = max(partesa.length, partesb.length); for (let i = 0; i < maxLen; i++) { const partea = partesa[i] ?? 0; const parteb = partesb[i] ?? 0; if (partea == parteb) continue; else if (partea > parteb) return 1; else return -1; } return 0; } async function getCurrentVersion(db, versionTable) { try { const lastVersion = await db.query(` SELECT * FROM ${versionTable} ORDER BY creation_date DESC LIMIT 1 `); return lastVersion.rows[0]; } catch (e) { console.error("[x] Error leyendo la tabla de versiones ", versionTable, e); } } /** * Si se estuviese lanzando el script sin que exista una tabla con el regstro * de versiones se crea una nueva con 0.0.0 como version inicial */ async function initVersionTable(db, versionTable) { const client = await db.connect(); const ddlCreateVersionTable = ` CREATE TABLE IF NOT EXISTS ${versionTable} ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, version TEXT, -- version semantica x.x.x, notes TEXT, creation_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), stable BOOLEAN DEFAULT FALSE -- Si la version ha sido testada y se puede desplegar ); `; const insertFistValue = ` INSERT INTO ${versionTable} ( version, notes ) VALUES ( '0.0.0', 'Versión base' ); `; try { await client.query("BEGIN"); await client.query(ddlCreateVersionTable); await client.query(insertFistValue); await client.query("COMMIT"); return 0; } catch (e) { client.query("ROLLBACK"); console.error("[x] No se ha podido crear la tabla de versiones de la BDD"); console.error(e); } finally { client.release(); } } async function writeAppliedVersion(db, versionTable, version) { const client = await db.connect(); const insertVersion = ` INSERT INTO ${versionTable} ( version, notes ) VALUES ( $1, $2 ); `; const parts = version.version.split("_"); try { await client.query("BEGIN"); await client.query(insertVersion, parts); await client.query("COMMIT"); return 0; } catch (e) { client.query("ROLLBACK"); console.error("[x] No se ha podido insertar la nueva version de la BDD"); console.error(e); } finally { client.release(); } } /** * Lógica principal de ejecución de migraciones * @param targetVersion Versión objetivo pasada por el usuario * @param currentVersion Versión actual obtenida de la DB */ async function runMigrations(args) { let db; console.log(` [i] Lanzado migraciones\n -> Servidor: ${process_1.env.POSTGRES_HOST}:${process_1.env.POSTGRES_PORT} -> BDD: ${process_1.env.POSTGRES_DATABASE} -> Objetivo: ${args.targetVersion} `); try { db = new pg_1.Pool({ host: process_1.env.POSTGRES_HOST, user: process_1.env.POSTGRES_USER, port: Number(process_1.env.POSTGRES_PORT), password: process_1.env.POSTGRES_PASSWORD, database: process_1.env.POSTGRES_DATABASE || process_1.env.POSTGRES_DB }); } catch (e) { console.error("[x] Error conectando a la base datos. Host: ", process_1.env.POSTGRES_HOST, " DB: ", process_1.env.POSTGRES_DATABASE); console.error(e); return; } const dbClient = await db.connect(); try { const versionBdd = (await getCurrentVersion(db, args.versionTable))?.version; if (versionBdd == undefined) { await initVersionTable(db, args.versionTable); } // 1º La version explicita 2º La versión almacenada en BDD 3º 0.0.0 como version base if (versionBdd == undefined) { console.log("[x] Error buscando la ultima version de la base de datos ", process_1.env.POSTGRES_DATABASE); } const baseVersion = args.baseVersion ?? versionBdd ?? "0.0.0"; console.log("[i] Migrando desde la version " + baseVersion + " a la version " + args.targetVersion); const files = await promises_1.default.readdir(args.migrationDir); const pendingMigrations = files .map(f => ({ version: path_1.default.parse(f).name, fileName: f, fullPath: path_1.default.join(args.migrationDir, f) })) // 1. Filtrar las migraciones > que la actual para volver a aplicar la actual // file > base .filter(file => compareVersions(file.version, baseVersion) == 1) // 2. Filtra las migraciones <= que la objetivo // file <= objetivo .filter(file => compareVersions(file.version, args.targetVersion) <= 0) .sort((a, b) => compareVersions(a.version, b.version)); if (pendingMigrations.length === 0) { console.log("[o] La base de datos ya está actualizada."); return; } console.log("[i] Migraciones pendietes", pendingMigrations.map(e => e.version)); console.log(`[i] Aplicando ${pendingMigrations.length} migraciones...`); // Iniciamos Transacción (Ejemplo conceptual con un cliente genérico) await dbClient.query("BEGIN"); for (const migration of pendingMigrations) { const sql = await promises_1.default.readFile(migration.fullPath, 'utf8'); console.log(` -> Aplicando: ${migration.fileName}`); // Ejecutar SQL await dbClient.query(sql); console.log(` -> Aplicado correctamente: ${migration.fileName}`); } await dbClient.query("COMMIT"); const ultimaVersion = pendingMigrations[pendingMigrations.length - 1]; await writeAppliedVersion(db, args.versionTable, ultimaVersion); console.log("[o] Migraciones completadas con éxito."); console.log("[o] Última version aplicada: ", ultimaVersion.version); } catch (error) { console.error("[x] Error durante la migración. Se ha realizado un rollback automático."); console.error(error); await dbClient.query("ROLLBACK"); process.exit(1); } finally { dbClient.release(); } } /** * ******************************************************************* * MAIN * ******************************************************************* */ async function main() { const args = getArgs(); await loadEnv(args.env); await runMigrations({ targetVersion: args.target, migrationDir: args.migrations, versionTable: args.versionTable, baseVersion: args.baseVersion }); return 0; } main() .then(e => { process.exit(0); }) .catch(e => { console.error(e); process.exit(1); });