From 0a4c6fbace67a99ff036d1bfd9c63475d644b037 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 8 Oct 2026 15:21:05 +0200 Subject: [PATCH] refactor(db): open the database through a DatabaseLifecycle provider db/database.ts opened the connection as a side effect of being imported, so the moment anything reached it the database was open. index.ts had to defer loading the app until a first-boot restore was done, and nothing in the container decided when the connection opened or closed. The module now opens the connection on first use and opens nothing at import. A connection closed on purpose (a restore, the shutdown) stays closed until it is reopened, as before. The new DatabaseLifecycle provider owns the decisions: buildApp() opens through it, which also binds the ORM to later swaps (the hook attachOrm used to register) and runs the schema bootstrap in the same order as before, and the shutdown closes through it. Its file property is the one resolved database path. The module keeps every export, so the test suites that replace it with vi.mock keep working; buildDbMock gains the new openDb. --- server/CLAUDE.md | 2 +- server/src/app-config/boot-validate.ts | 2 +- server/src/bootstrap.ts | 22 +++- server/src/db/database.ts | 93 +++++++------ server/src/db/orm.ts | 28 +--- server/src/db/schema-snapshot.ts | 8 +- server/src/index.ts | 22 +++- server/src/nest/app.module.ts | 3 + .../database/database-lifecycle.module.ts | 10 ++ .../database/database-lifecycle.service.ts | 76 +++++++++++ server/tests/build-schema-snapshot.ts | 12 +- server/tests/global-setup.ts | 2 +- server/tests/helpers/db-mock.ts | 3 +- .../integration/database-lifecycle.test.ts | 55 ++++++++ .../demo-reset-connection-swap.spike.test.ts | 8 +- .../demo-seed-request-context.test.ts | 2 +- .../tests/unit/db/database-lazy-open.test.ts | 80 +++++++++++ .../database-lifecycle.service.test.ts | 124 ++++++++++++++++++ 18 files changed, 456 insertions(+), 96 deletions(-) create mode 100644 server/src/nest/database/database-lifecycle.module.ts create mode 100644 server/src/nest/database/database-lifecycle.service.ts create mode 100644 server/tests/integration/database-lifecycle.test.ts create mode 100644 server/tests/unit/db/database-lazy-open.test.ts create mode 100644 server/tests/unit/nest/database/database-lifecycle.service.test.ts diff --git a/server/CLAUDE.md b/server/CLAUDE.md index a6d88afef..0259f516e 100644 --- a/server/CLAUDE.md +++ b/server/CLAUDE.md @@ -42,7 +42,7 @@ Nest owns everything. Every domain is a DI module under `src/nest//` (`c - **Scheduling**: `src/nest/scheduling/` (`CronRegistrarService`) is the one way to schedule a cron. Jobs are `*.job.ts` providers in their owning domain. The registrar refuses to schedule under `NODE_ENV=test` (an integration test pins it). **Never use a `@Cron` decorator** — it bypasses that gate. - **Plugins** (`src/nest/plugins/`): the child-process runtime (`supervisor/`, `runtime/plugin-host-entry.ts`, `protocol/envelope.ts`) is **intentionally not Nest** — it is the security boundary. `host/rpc-host.ts` registers an RPC method at spawn only if the plugin holds the unlocking permission (registration = authorization). Install-time gates live in `install/`; the manifest's `trek` version range fails closed, and `TREK_PLUGINS_IGNORE_TREK_RANGE` downgrades that to a warning. The protocol tables in `protocol/envelope.ts` (+ `src/plugin-event-sink.ts`) are **generated** into `plugin-sdk/` and `shared/` — run `gen:plugin-facts` after touching them and commit the output. `runtime/egress-policy.ts` is a hand-kept mirror guarded by a parity test in `plugin-sdk/`; change both sides together. - **Storage**: `src/nest/storage/` is the upload driver abstraction (local / S3 / mirror behind one interface, with an admin surface and a usage job). Domain services never touch the filesystem or the S3 SDK directly. -- **DB** (`src/db/`): `better-sqlite3`, WAL, FK on. `database.ts` only opens the connection and applies pragmas — it does not build a schema. MikroORM's migrations (`db/migrations/`) are the only schema source, **including for tests**: the hand-kept `schema.ts`/`migrations.ts`/`seeds.ts` builders and the parity test that guarded them are gone. Seeding is the MikroORM seeders in `db/seeders/`, applied by `db/orm.ts` from `buildApp()` before anything reads. An install the retired positional runner migrated (a `schema_version` row, no `mikro_orm_migrations` rows) is baselined by `db/legacy-baseline.ts`: the migrations whose `Legacy migration step N` docstring is ≤ its version are recorded as executed (never replayed) in the same transaction as the migrator run, while the baseline migration itself still runs first, as the legacy runner's `createTables()` did on every boot; anything it cannot map refuses the boot. Before a boot applies pending migrations to a file database that holds anything, `db/pre-migrate-snapshot.ts` writes a `VACUUM INTO` copy next to it (`pre-migrate--