From 5c054863b9ef190522eeaad879aa07b44ed40d11 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 8 Oct 2026 15:03:23 +0200 Subject: [PATCH] refactor(server): resolve the data directory in one place The data and uploads directories were derived independently in about ten files, each counting `..` hops from its own `__dirname`, and the file logger used `process.cwd()` instead. Moving a file or starting the server from another directory quietly moved where secrets, backups or logs went, and the backup code named `data/travel.db` directly, so with TREK_DB_FILE set a backup archived and a restore replaced a file the connection never opened. `resolveDataPaths()` in app-config now anchors data/, uploads/, backups, tmp, logs, the JWT secret and encryption key files and the database file (TREK_DB_FILE when set, else data/travel.db) on the server package root. Code that runs before the container calls it directly; Nest classes inject the new `DataPathsService`. Every previous anchor uses it, and the resolved paths are unchanged for the image and for `npm run dev`. --- server/CLAUDE.md | 2 +- server/src/app-config/README.md | 12 ++++ server/src/app-config/data-paths.ts | 62 ++++++++++++++++ server/src/app-config/index.ts | 1 + server/src/config.ts | 6 +- server/src/db/db-path.ts | 19 ++--- server/src/demo/demo-reset.ts | 4 +- server/src/index.ts | 13 ++-- server/src/nest/admin/admin.service.ts | 7 +- .../src/nest/app-config/app-config.module.ts | 5 +- .../src/nest/app-config/data-paths.service.ts | 36 ++++++++++ server/src/nest/app-config/index.ts | 1 + server/src/nest/audit/audit-log.logger.ts | 5 +- .../src/nest/backup/auto-backup.settings.ts | 3 +- server/src/nest/backup/backup.impl.ts | 7 +- server/src/nest/files/files.constants.ts | 7 +- server/src/nest/plugins/paths.ts | 3 +- server/src/nest/storage/storage-paths.ts | 27 ++++--- .../tests/unit/app-config/data-paths.test.ts | 72 +++++++++++++++++++ server/tests/unit/nest/admin.service.test.ts | 2 + .../nest/admin.version-notification.test.ts | 2 + 21 files changed, 239 insertions(+), 57 deletions(-) create mode 100644 server/src/app-config/data-paths.ts create mode 100644 server/src/nest/app-config/data-paths.service.ts create mode 100644 server/tests/unit/app-config/data-paths.test.ts diff --git a/server/CLAUDE.md b/server/CLAUDE.md index c2fed5065..c3eaf546d 100644 --- a/server/CLAUDE.md +++ b/server/CLAUDE.md @@ -49,7 +49,7 @@ Nest owns everything. Every domain is a DI module under `src/nest//` (`c - **MCP**: `src/nest-mcp/` is the decorator/registry layer (`@McpController`/`@Tool`/`@Resource`/`@Prompt`; see its `README.md`); tools live in `.mcp.ts` files beside each module; the HTTP transport is `src/nest/mcp-transport/`; `src/mcp/` holds process-wide state only (scopes, session manager, boot policy). Every tool invocation writes an audit row through the registry's `onInvoke` seam. MCP tools are adapters over the same services and permission checks as REST — a change on one side lands on the other in the same change. - **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. +- **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. Where the data lives (`data/`, `uploads/`, backups, logs, key files, the DB file) is resolved in one place, `src/app-config/data-paths.ts`; Nest classes inject `DataPathsService`, and nothing anchors a data path on its own `__dirname` or on `process.cwd()`. - **DB** (`src/db/`): `better-sqlite3`, WAL, FK on. `database.ts` only holds the connection and applies pragmas. It does not build a schema, and importing it opens nothing (the first reader does). The connection's lifecycle belongs to the `DatabaseLifecycle` provider (`nest/database/database-lifecycle.service.ts`): `buildApp()` opens it through `open()` (which binds the ORM to later swaps and runs the schema bootstrap below), the shutdown closes it through `close()`, and a restore or the demo reset swaps the file only through the database backup port, `DATABASE_BACKUP` (`nest/database/database-backup.interface.ts`; the SQLite implementation is `nest/backup/sqlite-database-backup.ts`: `VACUUM INTO` snapshot, close, atomic file swap, `reopen()`). The port works on `DatabaseLifecycle.file`, the one resolved path (`TREK_DB_FILE` or `data/travel.db`), and a snapshot that fails fails the backup rather than archiving the live file. `MaintenanceRepository` is provided only by `MaintenanceModule`'s factory and injected everywhere else; no consumer constructs it. A provider that has to follow every reopen (the demo reset job saves the first demo baseline after a restore's re-bootstrap seeded the example trips) registers with `DatabaseLifecycle.onReopened()`, which runs after the reopen's schema bootstrap; a listener contains its own failures, since one that throws fails the reopen. 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--