mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
96 lines
3.6 KiB
JavaScript
96 lines
3.6 KiB
JavaScript
/**
|
|
* Boot-time notice before `prisma migrate deploy`: how many migrations this
|
|
* image is about to apply, and a reminder to have a backup first.
|
|
*
|
|
* Run by `docker-entrypoint.sh` as `node scripts/pending-migrations.mjs`.
|
|
* It only reports. A migration that changes data cannot be undone by pulling
|
|
* the previous image, so an operator who reads the count in the log still has
|
|
* a moment to stop the container and take a `pg_dump`; the entrypoint does
|
|
* not wait for that.
|
|
*
|
|
* Best effort by design: any failure to count (no `_prisma_migrations` table
|
|
* yet is not a failure, it means every migration is pending) prints one line
|
|
* saying so and exits 0, because a notice is never a reason to keep the app
|
|
* from starting. `migrate deploy` right after it is what decides.
|
|
*/
|
|
import { readdirSync, existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
/** Migration directory names that carry a `migration.sql`, sorted. */
|
|
export function shippedMigrations(migrationsDir) {
|
|
return readdirSync(migrationsDir, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.filter((name) => existsSync(join(migrationsDir, name, "migration.sql")))
|
|
.sort();
|
|
}
|
|
|
|
/**
|
|
* The shipped migrations the database has not finished applying. Reads
|
|
* `_prisma_migrations` in the schema the connection string names (Prisma's
|
|
* `?schema=` parameter, `public` when absent); a database without that table
|
|
* has applied nothing.
|
|
*/
|
|
export async function pendingMigrations({ connectionString, migrationsDir }) {
|
|
const shipped = shippedMigrations(migrationsDir);
|
|
const { default: pg } = await import("pg");
|
|
const url = new URL(connectionString);
|
|
const schema = url.searchParams.get("schema") || "public";
|
|
url.searchParams.delete("schema");
|
|
const client = new pg.Client({
|
|
connectionString: url.toString(),
|
|
connectionTimeoutMillis: 5_000,
|
|
statement_timeout: 5_000,
|
|
});
|
|
await client.connect();
|
|
try {
|
|
const table = await client.query(
|
|
"SELECT to_regclass(format('%I._prisma_migrations', $1::text)) AS t",
|
|
[schema],
|
|
);
|
|
if (!table.rows[0]?.t) return shipped;
|
|
const applied = await client.query(
|
|
`SELECT migration_name FROM ${table.rows[0].t}
|
|
WHERE finished_at IS NOT NULL AND rolled_back_at IS NULL`,
|
|
);
|
|
const done = new Set(applied.rows.map((row) => row.migration_name));
|
|
return shipped.filter((name) => !done.has(name));
|
|
} finally {
|
|
await client.end().catch(() => {});
|
|
}
|
|
}
|
|
|
|
/** The log line for a count, as the entrypoint prints it. */
|
|
export function pendingNotice(count) {
|
|
if (count === 0) return "HealthLog: Database schema is up to date.";
|
|
const noun = count === 1 ? "migration" : "migrations";
|
|
return (
|
|
`HealthLog: ${count} database ${noun} pending. ` +
|
|
"Back up the database before updating (pg_dump, see the Updates section " +
|
|
"of docs/self-hosting/getting-started.md); a migration is not undone by " +
|
|
"going back to the previous image."
|
|
);
|
|
}
|
|
|
|
async function main() {
|
|
const connectionString = process.env.DATABASE_URL;
|
|
if (!connectionString) return;
|
|
try {
|
|
const pending = await pendingMigrations({
|
|
connectionString,
|
|
migrationsDir: join(process.cwd(), "prisma", "migrations"),
|
|
});
|
|
console.log(pendingNotice(pending.length));
|
|
} catch (error) {
|
|
const reason = error instanceof Error ? error.message : String(error);
|
|
console.log(
|
|
`HealthLog: Could not count pending migrations (${reason}); continuing.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
await main();
|
|
}
|