MigrationRunner applies pending migrations through the migration ledger
exactly once; when a migration fails it throws a MigrationFailedError
whose diagnostic is a structured object identifying the failing migration
(version), the failure phase (apply/record), the underlying cause, and the
applied/pending ledger state, serializable via toJSON. Re-exported from
the driver boundary so no other package needs the pg driver to run
migrations. Advisory lock (T04) and ready gate (T06) remain out of scope.
release() previously returned the connection to the pool in a finally even
when the pg_advisory_unlock statement failed, so a pooled connection could be
reused while its session still held the migration advisory lock - the next
borrower would block every other runner (reviewer finding F4). On unlock
failure the connection is now destroyed (client.release(error) removes the
client from the pool, ending the session and its lock); the plain
client.release() is kept only on the success path. Locked in by a static
assertion, a mutation probe, and a deterministic stub-pool behavioral probe
of the committed release() control flow.
Security-review finding F2: two concurrent acquire() calls on the same
MigrationLock instance could each check out a connection; the second
pg_advisory_lock would overwrite this.client, leaking the first locked
connection until session end.
acquire() now memoizes the in-flight acquire in acquireInFlight and
returns it on re-entry, so exactly one connection is checked out and no
locked connection leaks. The memo is cleared once the acquire settles.
tryAcquire()/release() paths unchanged.
Locked in by:
- static criterion test: acquireInFlight field, re-entry guard returns
the in-flight acquire, memo cleared on settle
- mutation probe: removing the re-entry guard fails the criterion
- real-stack probe: two concurrent acquire() calls on one instance leave
pool.totalCount at 1 (exactly one connection), the lock granted once,
nothing left after release; probe fails (hangs) on the pre-fix code
- CI job comment updated to reflect the re-entrancy criterion
MigrationLedger over the package-owned pg Pool: ensure() creates the
schema_migrations table (version text PRIMARY KEY, applied_at timestamptz
NOT NULL DEFAULT now()) with idempotent DDL; record() inserts an applied
migration with a parameterized, idempotent statement (ON CONFLICT DO
NOTHING — a rerun never double-applies); has()/applied() read the ledger
back in apply order. Re-exported from the driver boundary (src/index.ts)
so no other package needs the pg driver to touch migration state.
Review finding on PR #391: packages/database-postgres pinned pg@8.23.0 and
kysely@0.29.5, but the architecture doc's golden tuple (Technology-Stack
section 5.2 / section 7) pins pg@8.22.0 and Kysely@0.29.4. Reproducibility
requires the exact documented versions.
- packages/database-postgres/package.json: pg 8.23.0 -> 8.22.0,
kysely 0.29.5 -> 0.29.4, @types/pg 8.23.1 -> 8.21.0 (no 8.22.x of
@types/pg is published; 8.21.0 is the closest matching release, types
for the immediately preceding pg minor)
- pnpm-lock.yaml: regenerated with pnpm 11.23.0 (Node 24); the resolved
pg dependency tree is unchanged apart from the driver version itself
- tests/database-postgres-imports.test.mjs: exact-pin assertions updated
to the corrected versions, with a comment noting the @types/pg choice
- packages/database-postgres (@personal-blog/database-postgres): the single
workspace package allowed to import the PostgreSQL driver — declares pg and
kysely as exact dependencies (@types/pg for types) and its src/index.ts
imports and re-exports the driver pieces (Pool, Kysely, PostgresDialect) so
the isolation is real, not a placeholder
- pnpm-lock.yaml: importer for packages/database-postgres plus the resolved
pg/kysely dependency tree (frozen-lockfile install keeps working)
- .gitea/workflows/ci.yml: new database-postgres-imports job runs
tests/database-postgres-imports.test.mjs on every PR so the isolation
criterion gates merges
Adds root scripts so build, test and typecheck run from the workspace root:
- root package.json gains scripts: build (pnpm -r run build), test
(node --test on tests/**/*.test.mjs), typecheck (pnpm -r run typecheck)
- every workspace package (apps/server, packages/core,
extensions/example) gains build (tsc -p tsconfig.json) and typecheck
(tsc -p tsconfig.json --noEmit) scripts
- typescript 6.0.3 pinned as an exact root devDependency so the commands
run from a clean checkout; lockfile regenerated with pnpm 11.23.0
Verified from a clean state: pnpm install --frozen-lockfile passes,
pnpm build emits dist for all 3 packages, pnpm typecheck passes for all 3,
pnpm test runs tests/architecture-import.test.mjs 10/10 green, and CI's
pnpm -r list --depth -1 still lists all 4 workspace projects.
Closes#158
Adds ESM boundary declarations to every workspace package manifest
(apps/server, packages/core, extensions/example): "type": "module",
"main"/"types" entry points and an "exports" map (types + import
conditions) so each package exposes only its public root; adds
"type": "module" to the workspace root package.json so the workspace is
uniformly ESM. Placeholder src/index.ts files stay as empty ESM modules.
Verified: each package compiles under tsconfig.base.json (NodeNext) with
TypeScript 6.0.3 and emits ESM dist/index.js + dist/index.d.ts; Node
imports each package by name through its exports map and rejects deep
subpath imports (ERR_PACKAGE_PATH_NOT_EXPORTED); pnpm 11.23.0
install --frozen-lockfile passes with the lockfile unchanged.
Closes#156
Adds a strict base TypeScript config at the workspace root (ES2023 / NodeNext
/ strict family incl. noUncheckedIndexedAccess, exactOptionalPropertyTypes,
noImplicitOverride, useUnknownInCatchVariables, verbatimModuleSyntax per
Engineering-Standards) and gives every workspace package
(apps/server, packages/core, extensions/example) a tsconfig.json that extends
it. Each package gets a minimal src/index.ts placeholder so it compiles under
the base config (CJS-safe `export {}` until ESM boundaries land in T03).
Verified: every package typechecks and emits (js + d.ts + sourcemap) with the
pinned TypeScript 6.0.3; a strictness probe confirms the strict family fires.
Closes#155