@smartmonkeysinc/node-red-contrib-postgres-migrations
Version:
A Node-RED node to run PostgreSQL migrations from a msg.migrations array using Knex.
148 lines (132 loc) • 4.6 kB
JavaScript
module.exports = function (RED) {
// We need to require these here to make them available to the node's logic
const knex = require("knex");
function PostgresMigrationRunnerNode(config) {
RED.nodes.createNode(this, config);
const node = this;
const MIGRATIONS_TABLE_NAME = "_nodered_migrations";
node.on("input", async function (msg, send, done) {
// For Node-RED 1.0+
send =
send ||
function () {
node.send.apply(node, arguments);
};
done =
done ||
function (err) {
if (err) {
node.error(err, msg);
}
};
// --- 1. Input Validation ---
if (!msg.migrations || !Array.isArray(msg.migrations)) {
node.status({
fill: "red",
shape: "dot",
text: "msg.migrations is not an array",
});
return done(new Error("Input msg.migrations must be an array."));
}
// --- 2. Setup Knex Connection ---
const knexConfig = {
client: "pg",
connection: {
host: config.host,
port: config.port,
user: config.user,
password: config.password,
database: config.database,
},
// Suppress the warning about not specifying a pool size
pool: { min: 0, max: 1 },
};
let db;
try {
db = knex(knexConfig);
node.status({ fill: "blue", shape: "dot", text: "Connecting..." });
// --- 3. Ensure Migrations Table Exists ---
const hasTable = await db.schema.hasTable(MIGRATIONS_TABLE_NAME);
if (!hasTable) {
node.log(`Creating migrations table: ${MIGRATIONS_TABLE_NAME}`);
await db.schema.createTable(MIGRATIONS_TABLE_NAME, (table) => {
table.string("name").primary();
table.integer("batch");
table.timestamp("migration_time");
});
}
// --- 4. Get Executed Migrations and Batch Number ---
const executed = await db(MIGRATIONS_TABLE_NAME).select("name");
const executedNames = new Set(executed.map((m) => m.name));
const maxBatchResult = await db(MIGRATIONS_TABLE_NAME)
.max("batch as maxBatch")
.first();
const batch = (maxBatchResult.maxBatch || 0) + 1;
// --- 5. Run Pending Migrations ---
const appliedMigrations = [];
let skippedCount = 0;
for (const migration of msg.migrations) {
if (!migration.name || !migration.up) {
throw new Error(
"Migration object is missing 'name' or 'up' property."
);
}
if (!executedNames.has(migration.name)) {
node.log(`Applying migration: ${migration.name}`);
node.status({
fill: "blue",
shape: "dot",
text: `Applying: ${migration.name.substring(0, 20)}...`,
});
// Execute the raw SQL from the 'up' property
try {
await db.raw(migration.up);
} catch (err) {
err.message = `${migration.name} - ${err.message}`;
throw err;
}
// Record the migration in our tracking table
await db(MIGRATIONS_TABLE_NAME).insert({
name: migration.name,
batch: batch,
migration_time: new Date(),
});
appliedMigrations.push(migration.name);
} else {
skippedCount++;
}
}
// --- 6. Send Success Output ---
const summary = `Applied ${appliedMigrations.length}, skipped ${skippedCount}.`;
node.status({ fill: "green", shape: "dot", text: summary });
msg.payload = {
summary: summary,
applied: appliedMigrations,
skipped: skippedCount,
};
send(msg);
done();
} catch (err) {
// --- 7. Handle Errors ---
node.status({ fill: "red", shape: "dot", text: "Error" });
done(err); // Pass the error to the catch node
} finally {
// --- 8. Cleanup ---
if (db) {
await db.destroy();
node.log("Database connection closed.");
}
}
});
node.on("close", function (done) {
// This node doesn't hold a persistent connection, so cleanup is minimal.
// The connection is created and destroyed per-message.
node.status({});
done();
});
}
RED.nodes.registerType(
"postgres-migration-runner",
PostgresMigrationRunnerNode
);
};