mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
175 lines
8.8 KiB
TypeScript
175 lines
8.8 KiB
TypeScript
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<Database.Database, Promise<UnitOfWork>>();
|
|
const appSettingsPerHandle = new WeakMap<Database.Database, Promise<AppSettingsRepository>>();
|
|
const categoriesPerHandle = new WeakMap<Database.Database, Promise<CategoriesRepository>>();
|
|
const tagsPerHandle = new WeakMap<Database.Database, Promise<TagsRepository>>();
|
|
const settingsPerHandle = new WeakMap<Database.Database, Promise<SettingsRepository>>();
|
|
const usersPerHandle = new WeakMap<Database.Database, Promise<UsersRepository>>();
|
|
// 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<Database.Database, Promise<TestOrm>>();
|
|
/**
|
|
* 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<TestOrm> {
|
|
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<UnitOfWork> {
|
|
// `=== 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<AppSettingsRepository> {
|
|
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<CategoriesRepository> {
|
|
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<TagsRepository> {
|
|
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<SettingsRepository> {
|
|
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<UsersRepository> {
|
|
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<DynamicModule> {
|
|
const uow = await createTestUnitOfWork(db);
|
|
return {
|
|
module: TestUnitOfWorkModule,
|
|
global: true,
|
|
providers: [{ provide: UnitOfWork, useValue: uow }],
|
|
exports: [UnitOfWork],
|
|
};
|
|
}
|
|
}
|