288 lines
9.7 KiB
JavaScript
288 lines
9.7 KiB
JavaScript
#!/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 <version>
|
|
*/
|
|
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);
|
|
});
|