Files
db-migrate/lib/index.js

288 lines
9.7 KiB
JavaScript
Raw Normal View History

2026-05-06 11:05:24 +02:00
#!/usr/bin/env node
2026-02-20 12:19:51 +01:00
"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) {
2026-04-09 12:24:55 +02:00
const env = process.env;
const envPath = envpath;
if (envPath != undefined && (0, fs_1.existsSync)(envPath)) {
process.loadEnvFile(envpath);
2026-02-20 12:19:51 +01:00
console.log('[i] Archivo .env cargado desde ' + envpath);
2026-04-09 12:24:55 +02:00
console.log("ENV:", env);
}
else {
console.log("[i] Variables de entorno locales");
2026-02-20 12:19:51 +01:00
}
}
/**
* 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;
2026-02-23 11:07:02 +01:00
console.log(`
[i] Lanzado migraciones\n
-> Servidor: ${process_1.env.POSTGRES_HOST}:${process_1.env.POSTGRES_PORT}
2026-05-06 10:58:25 +02:00
-> BDD: ${process_1.env.POSTGRES_DATABASE}
2026-02-23 11:07:02 +01:00
-> Objetivo: ${args.targetVersion}
`);
2026-02-20 12:19:51 +01:00
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,
2026-05-06 10:58:25 +02:00
database: process_1.env.POSTGRES_DATABASE || process_1.env.POSTGRES_DB
2026-02-20 12:19:51 +01:00
});
}
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) {
2026-05-06 10:58:25 +02:00
console.log("[x] Error buscando la ultima version de la base de datos ", process_1.env.POSTGRES_DATABASE);
2026-02-20 12:19:51 +01:00
}
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);
});