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
154 lines
3.7 KiB
JavaScript
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,
|
|
};
|