@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.
Files changed (2) hide show
  1. package/dist/db.js +41 -8
  2. 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, each inside a transaction, guarded by
371
- // an advisory lock so concurrent boots can't race. Applied at service boot.
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
- const { rows } = await client.query('select name from schema_migrations');
386
- const done = new Set(rows.map((r) => r.name));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve31415/baselib",
3
- "version": "3.48.3",
3
+ "version": "3.49.0",
4
4
  "description": "Plasticine new-world shared platform library: logging, auth, service-to-service auth, db, HTTP, sync, app updates",
5
5
  "type": "module",
6
6
  "license": "MIT",