@steve31415/baselib 3.48.3 → 3.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db.js +41 -8
- package/package.json +1 -1
package/dist/db.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// (per-checkout instance — see TESTING.md). Cloud Run: the Cloud SQL Node
|
|
3
3
|
// connector with IAM authentication (CLOUD_SQL_INSTANCE + DB_IAM_USER +
|
|
4
4
|
// DB_NAME env vars) — no database passwords anywhere in the serving path.
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
import { readdir, readFile } from 'node:fs/promises';
|
|
6
7
|
import { join } from 'node:path';
|
|
7
8
|
import pg from 'pg';
|
|
@@ -367,8 +368,10 @@ export async function endPool(pool) {
|
|
|
367
368
|
await allClosed;
|
|
368
369
|
}
|
|
369
370
|
// Plain-SQL migrations: numbered files (001-*.sql, 002-*.sql, …) applied in
|
|
370
|
-
// order, recorded in schema_migrations
|
|
371
|
-
// an advisory lock so concurrent boots can't
|
|
371
|
+
// order, recorded in schema_migrations with their content's sha256, each
|
|
372
|
+
// inside a transaction, guarded by an advisory lock so concurrent boots can't
|
|
373
|
+
// race. Applied at service boot; an applied file whose content has since
|
|
374
|
+
// changed fails the boot.
|
|
372
375
|
// Lock loss (see logPoolErrors): all the work runs on the lock's own client,
|
|
373
376
|
// so a dead connection fails the next statement and the boot aborts — no
|
|
374
377
|
// window in which a migration runs unlocked.
|
|
@@ -380,18 +383,48 @@ export async function migrate(pool, migrationsDir, logger) {
|
|
|
380
383
|
await client.query('select pg_advisory_lock(hashtext($1))', ['baselib_migrate']);
|
|
381
384
|
await client.query(`create table if not exists schema_migrations (
|
|
382
385
|
name text primary key,
|
|
383
|
-
applied_at timestamptz not null default now()
|
|
386
|
+
applied_at timestamptz not null default now(),
|
|
387
|
+
sha256 text
|
|
384
388
|
)`);
|
|
385
|
-
|
|
386
|
-
const
|
|
389
|
+
await client.query('alter table schema_migrations add column if not exists sha256 text');
|
|
390
|
+
const { rows } = await client.query('select name, sha256 from schema_migrations');
|
|
391
|
+
const recorded = new Map(rows.map((r) => [r.name, r.sha256]));
|
|
392
|
+
const contents = new Map();
|
|
387
393
|
for (const file of files) {
|
|
388
|
-
if (done.has(file))
|
|
389
|
-
continue;
|
|
390
394
|
const sql = await readFile(join(migrationsDir, file), 'utf8');
|
|
395
|
+
contents.set(file, { sql, sha256: createHash('sha256').update(sql).digest('hex') });
|
|
396
|
+
}
|
|
397
|
+
// A shipped migration file is never edited (plasticine-way docs/DATABASE.md): the
|
|
398
|
+
// runner skips a recorded name, so an edit would never reach the database while the
|
|
399
|
+
// code relying on it ships. Refuse to boot instead, before applying anything, so the
|
|
400
|
+
// new revision fails its start and the old one keeps serving (people, 2026-11-02).
|
|
401
|
+
// Rows recorded before hashes were kept get the file's current hash, once.
|
|
402
|
+
const edited = [];
|
|
403
|
+
for (const file of files) {
|
|
404
|
+
if (!recorded.has(file))
|
|
405
|
+
continue;
|
|
406
|
+
const { sha256 } = contents.get(file);
|
|
407
|
+
const was = recorded.get(file);
|
|
408
|
+
if (was == null) {
|
|
409
|
+
await client.query('update schema_migrations set sha256 = $2 where name = $1', [file, sha256]);
|
|
410
|
+
}
|
|
411
|
+
else if (was !== sha256) {
|
|
412
|
+
edited.push(file);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (edited.length > 0) {
|
|
416
|
+
throw new Error(`migration ${edited.join(', ')} was edited after it was applied; a shipped migration is never ` +
|
|
417
|
+
'edited: restore the file and put the change in a new migration (a local database holding an ' +
|
|
418
|
+
'unshipped draft: drop it and re-run)');
|
|
419
|
+
}
|
|
420
|
+
for (const file of files) {
|
|
421
|
+
if (recorded.has(file))
|
|
422
|
+
continue;
|
|
423
|
+
const { sql, sha256 } = contents.get(file);
|
|
391
424
|
try {
|
|
392
425
|
await client.query('begin');
|
|
393
426
|
await client.query(sql);
|
|
394
|
-
await client.query('insert into schema_migrations (name) values ($1)', [file]);
|
|
427
|
+
await client.query('insert into schema_migrations (name, sha256) values ($1, $2)', [file, sha256]);
|
|
395
428
|
await client.query('commit');
|
|
396
429
|
}
|
|
397
430
|
catch (err) {
|
package/package.json
CHANGED