import { Module, type DynamicModule } from '@nestjs/common'; import type Database from 'better-sqlite3'; import { UnitOfWork } from '../../src/nest/database/unit-of-work'; import { createTestOrm, type TestOrm } from './test-orm'; import { AppSettings } from '../../src/db/entities/AppSettings.entity'; import type { AppSettingsRepository } from '../../src/db/repositories/AppSettings.repository'; import { Categories } from '../../src/db/entities/Categories.entity'; import type { CategoriesRepository } from '../../src/db/repositories/Categories.repository'; import { Tags } from '../../src/db/entities/Tags.entity'; import type { TagsRepository } from '../../src/db/repositories/Tags.repository'; import { Settings } from '../../src/db/entities/Settings.entity'; import type { SettingsRepository } from '../../src/db/repositories/Settings.repository'; import { Users } from '../../src/db/entities/Users.entity'; import type { UsersRepository } from '../../src/db/repositories/Users.repository'; const perHandle = new WeakMap>(); const appSettingsPerHandle = new WeakMap>(); const categoriesPerHandle = new WeakMap>(); const tagsPerHandle = new WeakMap>(); const settingsPerHandle = new WeakMap>(); const usersPerHandle = new WeakMap>(); // ONE MikroORM per handle, shared by createTestUnitOfWork and // createTestAppSettingsRepo (task-2-review.md I2; reworded per // task-1-2-rereview.md's I-A — the original wording here claimed a // functional bug that measurement disproved): each used to call // createTestOrm(db) independently, which opened a SECOND MikroORM.init over // the same better-sqlite3 handle — two identity maps and two Kysely clients, // neither ever closed. That is NOT the same as a repository write silently // running outside a transaction: `TransactionContext.getEntityManager(name)` // keys on the EntityManager's CONTEXT NAME ('default'), not on which ORM // instance created it, so a repository built from ORM B still resolved ORM // A's transactional fork correctly even under the old split wiring — // transaction resolution worked either way. The reason to share one ORM per // handle is simpler: it is the same EntityManager Nest's real DI graph would // inject (one MikroORM per app, not one per consumer), and it stops leaking // a second identity map and Kysely client per test file. Both functions below // now derive from this single `t.em`, so a repository resolved from // `t.repo(X)` and a `UnitOfWork` built from `t.em` share the same // context-resolving EntityManager, exactly like Nest's real DI graph. const ormPerHandle = new WeakMap>(); /** * Exported so a caller that needs the raw `MikroORM` instance itself — not a * repository or a `UnitOfWork` derived from it — can still share it: e.g. * `plugin-host.ts`'s `createPluginRuntime` passes `(await sharedTestOrm(db)).orm` * to `PluginRuntimeService` so `PluginSupervisor`'s D6 request-context wrapper * (task-2-review.md C3) forks from the SAME `em` `PermissionsService`'s * repository resolves through — a wrapper built from a different ORM instance * would fork a context a repository bound to this one never sees. */ export function sharedTestOrm(db: Database.Database): Promise { const existing = ormPerHandle.get(db); if (existing !== undefined) return existing; const pending = createTestOrm(db); ormPerHandle.set(db, pending); return pending; } /** * The `UnitOfWork` a hand-constructed service needs, bound to the suite's own * better-sqlite3 handle. * * `UnitOfWork.transactional` opens the transaction through MikroORM on that same * handle, so the legacy raw statements a service still issues inside the * callback run inside it — the arrangement the Phase 1 sweep relies on. Memoised * per handle via `sharedTestOrm`: one ORM per test file, however many services * the file builds — and the SAME one `createTestAppSettingsRepo` below draws * from, so a repository and this `UnitOfWork` resolve the same transaction. */ export function createTestUnitOfWork(db: Database.Database): Promise { // `=== undefined` rather than a truthiness test: a Promise is always truthy, // which is exactly what no-misused-promises/checksConditionals rejects. const existing = perHandle.get(db); if (existing !== undefined) return existing; const pending = sharedTestOrm(db).then((t) => new UnitOfWork(t.em)); perHandle.set(db, pending); return pending; } /** * The `AppSettingsRepository` a hand-constructed service needs (`PermissionsService`, * mirroring `SettingsService`'s own `t.repo(AppSettings)` in its unit test), bound to * the suite's own better-sqlite3 handle the same way `createTestUnitOfWork` is. * * Memoised per handle via the same `sharedTestOrm` `createTestUnitOfWork` uses — * not a second `createTestOrm` call — so this repository and that `UnitOfWork` * resolve through the identical `EntityManager` and a `uow.transactional(...)` * write this repository makes is actually inside the open transaction. */ export function createTestAppSettingsRepo(db: Database.Database): Promise { const existing = appSettingsPerHandle.get(db); if (existing !== undefined) return existing; const pending = sharedTestOrm(db).then((t) => t.repo(AppSettings) as AppSettingsRepository); appSettingsPerHandle.set(db, pending); return pending; } /** * The `CategoriesRepository` a hand-constructed `CategoriesService` needs * (Plan 3a Task 3), bound to the suite's own better-sqlite3 handle and * memoised via the same `sharedTestOrm` the other helpers here use — not a * second `createTestOrm` call (task-2-review.md I2's one-ORM-per-handle * ruling). */ export function createTestCategoriesRepo(db: Database.Database): Promise { const existing = categoriesPerHandle.get(db); if (existing !== undefined) return existing; const pending = sharedTestOrm(db).then((t) => t.repo(Categories) as CategoriesRepository); categoriesPerHandle.set(db, pending); return pending; } /** * The `TagsRepository` a hand-constructed `TagsService` needs (Plan 3a Task * 3), same memoisation as `createTestCategoriesRepo`. */ export function createTestTagsRepo(db: Database.Database): Promise { const existing = tagsPerHandle.get(db); if (existing !== undefined) return existing; const pending = sharedTestOrm(db).then((t) => t.repo(Tags) as TagsRepository); tagsPerHandle.set(db, pending); return pending; } /** * The `SettingsRepository` a hand-constructed `SettingsService` needs (Plan * 3a Task 5 — the per-user `settings` table repository), same memoisation as * `createTestAppSettingsRepo`/`createTestCategoriesRepo`. */ export function createTestSettingsRepo(db: Database.Database): Promise { const existing = settingsPerHandle.get(db); if (existing !== undefined) return existing; const pending = sharedTestOrm(db).then((t) => t.repo(Settings) as SettingsRepository); settingsPerHandle.set(db, pending); return pending; } /** * The `UsersRepository` a hand-constructed service passes into * `instance-api-keys.ts`'s `resolveApiKey` (and the other functions there * that take one) — an explicit parameter, not resolved off an ambient * MikroORM request context (Plan 3a Task 5; an earlier version of that file * tried the request-context approach and broke 185 unit tests across 12 * files that build a service with no request context around it — see that * file's own docstring). Same memoisation as the others in this file. */ export function createTestUsersRepo(db: Database.Database): Promise { const existing = usersPerHandle.get(db); if (existing !== undefined) return existing; const pending = sharedTestOrm(db).then((t) => t.repo(Users) as UsersRepository); usersPerHandle.set(db, pending); return pending; } /** * The same `UnitOfWork`, as a **global** Nest module. * * A `Test.createTestingModule({ providers: [...] })` entry only reaches the root * testing module, so a service resolved inside its own feature module (the * `PermissionsService` in `PermissionsModule`, say) still finds no `UnitOfWork`. * `OrmModule` is `@Global()` in the app graph for exactly that reason, and the * partial containers the e2e suites build need the same shape. */ @Module({}) export class TestUnitOfWorkModule { static async forRoot(db: Database.Database): Promise { const uow = await createTestUnitOfWork(db); return { module: TestUnitOfWorkModule, global: true, providers: [{ provide: UnitOfWork, useValue: uow }], exports: [UnitOfWork], }; } }