costco-grocery-list/scripts/db-migrate-common.js
Nico 77ae5be445
All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 1m10s
Build & Deploy Costco Grocery List / verify-images (push) Successful in 3s
Build & Deploy Costco Grocery List / deploy (push) Successful in 11s
Build & Deploy Costco Grocery List / notify (push) Successful in 1s
refactor
2026-02-22 01:27:03 -08:00

154 lines
3.7 KiB
JavaScript

"use strict";
const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");
const migrationsDir = path.resolve(
__dirname,
"..",
"packages",
"db",
"migrations"
);
const staleConfigPath = path.join(migrationsDir, "stale-files.json");
function readStaleConfigFile() {
if (!fs.existsSync(staleConfigPath)) {
return new Set();
}
const raw = fs.readFileSync(staleConfigPath, "utf8");
let parsed;
try {
parsed = JSON.parse(raw);
} catch (error) {
throw new Error(`Invalid JSON in ${staleConfigPath}`);
}
const values = Array.isArray(parsed?.stale_files) ? parsed.stale_files : [];
return new Set(
values
.map((value) => String(value || "").trim())
.filter(Boolean)
);
}
function getSkippedMigrations() {
const includeStale = String(process.env.DB_MIGRATE_INCLUDE_STALE || "")
.trim()
.toLowerCase();
const skipFromConfig =
includeStale === "1" || includeStale === "true" || includeStale === "yes"
? new Set()
: readStaleConfigFile();
const raw = process.env.DB_MIGRATE_SKIP_FILES || "";
const skipFromEnv = new Set(
raw
.split(",")
.map((value) => value.trim())
.filter(Boolean)
);
return new Set([...skipFromConfig, ...skipFromEnv]);
}
function ensureDatabaseUrl() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("DATABASE_URL is required.");
}
return databaseUrl;
}
function ensurePsql() {
const result = spawnSync("psql", ["--version"], { stdio: "pipe" });
if (result.error || result.status !== 0) {
throw new Error("psql executable was not found in PATH.");
}
}
function ensureMigrationsDir() {
if (!fs.existsSync(migrationsDir)) {
throw new Error(`Migrations directory not found: ${migrationsDir}`);
}
}
function getMigrationFiles() {
ensureMigrationsDir();
const skipped = getSkippedMigrations();
return fs
.readdirSync(migrationsDir)
.filter((file) => file.endsWith(".sql"))
.filter((file) => !skipped.has(file))
.sort((a, b) => a.localeCompare(b));
}
function runPsql(databaseUrl, args) {
const result = spawnSync("psql", [databaseUrl, ...args], {
stdio: "pipe",
encoding: "utf8",
});
if (result.status !== 0) {
const stderr = (result.stderr || "").trim();
const stdout = (result.stdout || "").trim();
const details = [stderr, stdout].filter(Boolean).join("\n");
throw new Error(details || "psql command failed");
}
return result.stdout || "";
}
function escapeSqlLiteral(value) {
return value.replace(/'/g, "''");
}
function ensureSchemaMigrationsTable(databaseUrl) {
runPsql(databaseUrl, [
"-v",
"ON_ERROR_STOP=1",
"-c",
"CREATE TABLE IF NOT EXISTS schema_migrations (filename TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW());",
]);
}
function getAppliedMigrations(databaseUrl) {
const output = runPsql(databaseUrl, [
"-At",
"-v",
"ON_ERROR_STOP=1",
"-c",
"SELECT filename FROM schema_migrations ORDER BY filename ASC;",
]);
return new Set(
output
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
);
}
function applyMigration(databaseUrl, filename) {
const fullPath = path.join(migrationsDir, filename);
runPsql(databaseUrl, ["-v", "ON_ERROR_STOP=1", "-f", fullPath]);
runPsql(databaseUrl, [
"-v",
"ON_ERROR_STOP=1",
"-c",
`INSERT INTO schema_migrations (filename) VALUES ('${escapeSqlLiteral(
filename
)}') ON CONFLICT DO NOTHING;`,
]);
}
module.exports = {
applyMigration,
ensureDatabaseUrl,
ensurePsql,
ensureSchemaMigrationsTable,
getAppliedMigrations,
getMigrationFiles,
getSkippedMigrations,
migrationsDir,
};