mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
462 lines
25 KiB
TypeScript
462 lines
25 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';
|
|
import { WebauthnCredentials } from '../../src/db/entities/WebauthnCredentials.entity';
|
|
import type { WebauthnCredentialsRepository } from '../../src/db/repositories/WebauthnCredentials.repository';
|
|
import { WebauthnChallenges } from '../../src/db/entities/WebauthnChallenges.entity';
|
|
import type { WebauthnChallengesRepository } from '../../src/db/repositories/WebauthnChallenges.repository';
|
|
import { InviteTokens } from '../../src/db/entities/InviteTokens.entity';
|
|
import type { InviteTokensRepository } from '../../src/db/repositories/InviteTokens.repository';
|
|
import { McpTokens } from '../../src/db/entities/McpTokens.entity';
|
|
import type { McpTokensRepository } from '../../src/db/repositories/McpTokens.repository';
|
|
import { OauthTokens } from '../../src/db/entities/OauthTokens.entity';
|
|
import type { OauthTokensRepository } from '../../src/db/repositories/OauthTokens.repository';
|
|
import { PasswordResetTokens } from '../../src/db/entities/PasswordResetTokens.entity';
|
|
import type { PasswordResetTokensRepository } from '../../src/db/repositories/PasswordResetTokens.repository';
|
|
import { Trips } from '../../src/db/entities/Trips.entity';
|
|
import type { TripsRepository } from '../../src/db/repositories/Trips.repository';
|
|
import { TripMembers } from '../../src/db/entities/TripMembers.entity';
|
|
import type { TripMembersRepository } from '../../src/db/repositories/TripMembers.repository';
|
|
import { Places } from '../../src/db/entities/Places.entity';
|
|
import type { PlacesRepository } from '../../src/db/repositories/Places.repository';
|
|
import { DatabaseService } from '../../src/nest/database/database.service';
|
|
import { Days } from '../../src/db/entities/Days.entity';
|
|
import type { DaysRepository } from '../../src/db/repositories/Days.repository';
|
|
import { DayAssignments } from '../../src/db/entities/DayAssignments.entity';
|
|
import type { DayAssignmentsRepository } from '../../src/db/repositories/DayAssignments.repository';
|
|
import { DayNotes } from '../../src/db/entities/DayNotes.entity';
|
|
import type { DayNotesRepository } from '../../src/db/repositories/DayNotes.repository';
|
|
import { PlaceRatings } from '../../src/db/entities/PlaceRatings.entity';
|
|
import type { PlaceRatingsRepository } from '../../src/db/repositories/PlaceRatings.repository';
|
|
import { AssignmentParticipants } from '../../src/db/entities/AssignmentParticipants.entity';
|
|
import type { AssignmentParticipantsRepository } from '../../src/db/repositories/AssignmentParticipants.repository';
|
|
import { PlaceShadowPicks } from '../../src/db/entities/PlaceShadowPicks.entity';
|
|
import type { PlaceShadowPicksRepository } from '../../src/db/repositories/PlaceShadowPicks.repository';
|
|
import { GooglePlacePhotoMeta } from '../../src/db/entities/GooglePlacePhotoMeta.entity';
|
|
import type { GooglePlacePhotoMetaRepository } from '../../src/db/repositories/GooglePlacePhotoMeta.repository';
|
|
import { PlaceDetailsCache } from '../../src/db/entities/PlaceDetailsCache.entity';
|
|
import type { PlaceDetailsCacheRepository } from '../../src/db/repositories/PlaceDetailsCache.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));
|
|
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));
|
|
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));
|
|
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));
|
|
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));
|
|
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],
|
|
};
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Plan 3b Task 2 (TokenService) — appended after every Task 0/1/3a helper
|
|
// above, before Task 3's own block (which appends after this one, per each
|
|
// task's file-ownership rule). Same memoisation-per-handle pattern as every
|
|
// helper above.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const mcpTokensPerHandle = new WeakMap<Database.Database, Promise<McpTokensRepository>>();
|
|
|
|
/** The `McpTokensRepository` a hand-constructed `TokenService` needs (Plan 3b Task 2). */
|
|
export function createTestMcpTokensRepo(db: Database.Database): Promise<McpTokensRepository> {
|
|
const existing = mcpTokensPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(McpTokens));
|
|
mcpTokensPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Plan 3b Task 3 (PasskeyService / RegistrationInvitesService) — appended at
|
|
// the end per the task's own file-ownership rule (Task 2 owns everything
|
|
// above this point in this file). Same memoisation-per-handle pattern as
|
|
// every helper above: one ORM per test file's better-sqlite3 handle, shared
|
|
// with whatever `UnitOfWork`/other repository that same file also builds.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const webauthnCredentialsPerHandle = new WeakMap<Database.Database, Promise<WebauthnCredentialsRepository>>();
|
|
const webauthnChallengesPerHandle = new WeakMap<Database.Database, Promise<WebauthnChallengesRepository>>();
|
|
const inviteTokensPerHandle = new WeakMap<Database.Database, Promise<InviteTokensRepository>>();
|
|
|
|
/** The `WebauthnCredentialsRepository` a hand-constructed `PasskeyService` needs. */
|
|
export function createTestWebauthnCredentialsRepo(db: Database.Database): Promise<WebauthnCredentialsRepository> {
|
|
const existing = webauthnCredentialsPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(WebauthnCredentials));
|
|
webauthnCredentialsPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
/** The `WebauthnChallengesRepository` a hand-constructed `PasskeyService` needs. */
|
|
export function createTestWebauthnChallengesRepo(db: Database.Database): Promise<WebauthnChallengesRepository> {
|
|
const existing = webauthnChallengesPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(WebauthnChallenges));
|
|
webauthnChallengesPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
/**
|
|
* The `InviteTokensRepository` a hand-constructed `RegistrationInvitesService`
|
|
* (or `AuthService`) needs — Task 0 built the class, this is the first test
|
|
* helper for it.
|
|
*/
|
|
export function createTestInviteTokensRepo(db: Database.Database): Promise<InviteTokensRepository> {
|
|
const existing = inviteTokensPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(InviteTokens));
|
|
inviteTokensPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Plan 3b Task 5 (AuthService / UserCleanupService) — appended at the end per
|
|
// the task's own file-ownership rule. Same memoisation-per-handle pattern as
|
|
// every helper above: one ORM per test file's better-sqlite3 handle, shared
|
|
// with whatever `UnitOfWork`/other repository that same file also builds —
|
|
// required per the Task 4 review ("append createTestOauthTokensRepo to
|
|
// test-uow.ts through the existing sharedTestOrm memoisation — a repository
|
|
// from a different ORM instance resolves a different fork and the
|
|
// transaction test lies").
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const oauthTokensPerHandle = new WeakMap<Database.Database, Promise<OauthTokensRepository>>();
|
|
const passwordResetTokensPerHandle = new WeakMap<Database.Database, Promise<PasswordResetTokensRepository>>();
|
|
|
|
/** The `OauthTokensRepository` a hand-constructed `AuthService` needs (Plan 3b Task 4's `revokeAllForUser`, consumed by Task 5). */
|
|
export function createTestOauthTokensRepo(db: Database.Database): Promise<OauthTokensRepository> {
|
|
const existing = oauthTokensPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(OauthTokens));
|
|
oauthTokensPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
/** The `PasswordResetTokensRepository` a hand-constructed `AuthService` needs (Plan 3b Task 5, new repository). */
|
|
export function createTestPasswordResetTokensRepo(db: Database.Database): Promise<PasswordResetTokensRepository> {
|
|
const existing = passwordResetTokensPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(PasswordResetTokens));
|
|
passwordResetTokensPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Plan 3c Task 0b (trip-access primitives onto TripsRepository/
|
|
// TripMembersRepository/PlacesRepository) — appended at the end per the
|
|
// task's own file-ownership rule. Same memoisation-per-handle pattern as
|
|
// every helper above.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const tripsRepoPerHandle = new WeakMap<Database.Database, Promise<TripsRepository>>();
|
|
const tripMembersRepoPerHandle = new WeakMap<Database.Database, Promise<TripMembersRepository>>();
|
|
const placesRepoPerHandle = new WeakMap<Database.Database, Promise<PlacesRepository>>();
|
|
|
|
/** The `TripsRepository` a hand-constructed `DatabaseService`/`TripAccessGuard` double needs. */
|
|
export function createTestTripsRepo(db: Database.Database): Promise<TripsRepository> {
|
|
const existing = tripsRepoPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(Trips));
|
|
tripsRepoPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
/** The `TripMembersRepository` `DatabaseService.rosterUserIds` needs. */
|
|
export function createTestTripMembersRepo(db: Database.Database): Promise<TripMembersRepository> {
|
|
const existing = tripMembersRepoPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(TripMembers));
|
|
tripMembersRepoPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
/** The `PlacesRepository` `DatabaseService.getPlaceWithTags` needs. */
|
|
export function createTestPlacesRepo(db: Database.Database): Promise<PlacesRepository> {
|
|
const existing = placesRepoPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(Places));
|
|
placesRepoPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
/**
|
|
* A `DatabaseService` bound to the suite's own better-sqlite3 handle, with
|
|
* its `EntityManager` wired through `sharedTestOrm` — so `canAccessTrip`/
|
|
* `isOwner`/`rosterUserIds`/`getPlaceWithTags` (Plan 3c Task 0b, now
|
|
* repository-backed rather than delegating to `db/database.ts`'s deleted
|
|
* free functions) work on a directly-constructed instance the same way they
|
|
* do through Nest DI in production. `sharedTestOrm`'s ORM is built with
|
|
* `allowGlobalContext: true` (`test-orm.ts`), so these calls need no
|
|
* `withRequestContext` wrapper here, unlike production.
|
|
*
|
|
* A file that only needs `new DatabaseService(db)`'s other methods
|
|
* (`prepare`/`get`/`all`/`run`) does not need this helper — the
|
|
* `EntityManager` constructor argument is `@Optional()`, and only these four
|
|
* methods use it.
|
|
*/
|
|
export async function createTestDatabaseService(db: Database.Database): Promise<DatabaseService> {
|
|
const t = await sharedTestOrm(db);
|
|
return new DatabaseService(db, t.em);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Plan 3c Task 2 (`DaysService` + day notes onto `DaysRepository`/
|
|
// `DayAssignmentsRepository`/`DayNotesRepository`/`TripsRepository.setEndDate`)
|
|
// — appended at the end per the task's own file-ownership rule (append only).
|
|
// Same memoisation-per-handle pattern as every helper above.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const daysRepoPerHandle = new WeakMap<Database.Database, Promise<DaysRepository>>();
|
|
const dayAssignmentsRepoPerHandle = new WeakMap<Database.Database, Promise<DayAssignmentsRepository>>();
|
|
const dayNotesRepoPerHandle = new WeakMap<Database.Database, Promise<DayNotesRepository>>();
|
|
|
|
/** The `DaysRepository` a hand-constructed `DaysService` needs. */
|
|
export function createTestDaysRepo(db: Database.Database): Promise<DaysRepository> {
|
|
const existing = daysRepoPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(Days));
|
|
daysRepoPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
/** The `DayAssignmentsRepository` a hand-constructed `DaysService` needs. */
|
|
export function createTestDayAssignmentsRepo(db: Database.Database): Promise<DayAssignmentsRepository> {
|
|
const existing = dayAssignmentsRepoPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(DayAssignments));
|
|
dayAssignmentsRepoPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
/** The `DayNotesRepository` a hand-constructed `DaysService` needs (DY4). */
|
|
export function createTestDayNotesRepo(db: Database.Database): Promise<DayNotesRepository> {
|
|
const existing = dayNotesRepoPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(DayNotes));
|
|
dayNotesRepoPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Plan 3c Task 1 (`QueryHelpersService`/`TripMembershipService`/
|
|
// `PlaceShadowService`/`PlaceEnrichmentService`/`PlacePhotoCacheService` onto
|
|
// repositories) — appended at the end per the task's own file-ownership rule
|
|
// (append only). Same memoisation-per-handle pattern as every helper above.
|
|
// `TagsRepository` already has `createTestTagsRepo` (Plan 3a Task 3) — a
|
|
// hand-constructed `QueryHelpersService` reuses that one rather than a second
|
|
// copy. `TripsRepository`/`TripMembersRepository` already have
|
|
// `createTestTripsRepo`/`createTestTripMembersRepo` (Plan 3c Task 0b) — a
|
|
// hand-constructed `TripMembershipService` reuses those too.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const placeRatingsRepoPerHandle = new WeakMap<Database.Database, Promise<PlaceRatingsRepository>>();
|
|
const assignmentParticipantsRepoPerHandle = new WeakMap<Database.Database, Promise<AssignmentParticipantsRepository>>();
|
|
const placeShadowPicksRepoPerHandle = new WeakMap<Database.Database, Promise<PlaceShadowPicksRepository>>();
|
|
const googlePlacePhotoMetaRepoPerHandle = new WeakMap<Database.Database, Promise<GooglePlacePhotoMetaRepository>>();
|
|
const placeDetailsCacheRepoPerHandle = new WeakMap<Database.Database, Promise<PlaceDetailsCacheRepository>>();
|
|
|
|
/** The `PlaceRatingsRepository` a hand-constructed `QueryHelpersService` needs (QH2). */
|
|
export function createTestPlaceRatingsRepo(db: Database.Database): Promise<PlaceRatingsRepository> {
|
|
const existing = placeRatingsRepoPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(PlaceRatings));
|
|
placeRatingsRepoPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
/** The `AssignmentParticipantsRepository` a hand-constructed `QueryHelpersService` needs (QH3). */
|
|
export function createTestAssignmentParticipantsRepo(db: Database.Database): Promise<AssignmentParticipantsRepository> {
|
|
const existing = assignmentParticipantsRepoPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(AssignmentParticipants));
|
|
assignmentParticipantsRepoPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
/** The `PlaceShadowPicksRepository` a hand-constructed `PlaceShadowService` needs. */
|
|
export function createTestPlaceShadowPicksRepo(db: Database.Database): Promise<PlaceShadowPicksRepository> {
|
|
const existing = placeShadowPicksRepoPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(PlaceShadowPicks));
|
|
placeShadowPicksRepoPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
/** The `GooglePlacePhotoMetaRepository` a hand-constructed `PlacePhotoCacheService` needs. */
|
|
export function createTestGooglePlacePhotoMetaRepo(db: Database.Database): Promise<GooglePlacePhotoMetaRepository> {
|
|
const existing = googlePlacePhotoMetaRepoPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(GooglePlacePhotoMeta));
|
|
googlePlacePhotoMetaRepoPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|
|
|
|
/** The `PlaceDetailsCacheRepository` a hand-constructed `PlaceEnrichmentService` needs. */
|
|
export function createTestPlaceDetailsCacheRepo(db: Database.Database): Promise<PlaceDetailsCacheRepository> {
|
|
const existing = placeDetailsCacheRepoPerHandle.get(db);
|
|
if (existing !== undefined) return existing;
|
|
const pending = sharedTestOrm(db).then((t) => t.repo(PlaceDetailsCache));
|
|
placeDetailsCacheRepoPerHandle.set(db, pending);
|
|
return pending;
|
|
}
|