diff --git a/lib/index.js b/lib/index.js new file mode 100755 index 0000000..8f136b3 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,281 @@ +#!/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 envPath = envpath ?? path_1.default.join(process.cwd(), '.env'); + if ((0, fs_1.existsSync)(envPath)) { + const content = await promises_1.default.readFile(envPath, 'utf-8'); + content.split('\n').forEach(line => { + const [key, value] = line.split('='); + if (key && value) + process.env[key.trim()] = value.trim(); + }); + console.log('[i] Archivo .env cargado desde ' + envpath); + } +} +/** + * 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; + 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 + }); + } + 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"); + } + 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); +}); diff --git a/package.json b/package.json index 68c574a..22b0ba6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Herramienta de migracion de bdd", "main": "src/index.ts", "bin": { - "db-migrate": "./dist/index.js" + "db-migrate": "./lib/index.js" }, "scripts": { "test": "node --test",