refactor(server): provide MaintenanceRepository through DI

MaintenanceRepository was built with new over an injected EntityManager
in the readiness probe and in UserCleanupService, outside the container.
MaintenanceModule now provides it, as a factory over the same
context-resolving EntityManager Nest injects everywhere, and both inject
it. The class itself stays free of Nest decorators, like the rest of
src/db, so tests keep building one over their own EntityManager.

No behaviour changes: the probe still pings and the erasure still runs
inside the account deletion's transaction.
This commit is contained in:
Maurice
2026-10-08 15:23:20 +02:00
parent 0a4c6fbace
commit 8ede0dab9e
19 changed files with 81 additions and 41 deletions
@@ -15,15 +15,13 @@ import type { EntityManager } from '@mikro-orm/core';
* all — so `connection.execute()` (the last tier in the program's query-API
* order) is the correct tool, not a shortcut around it.
*
* Constructed directly (`new MaintenanceRepository(em)`), never through Nest
* DI: its only two callers — `backup.impl.ts#createBackup` and
* `demo/demo-reset.ts` (`resetDemoUser`/`saveBaseline`) — are plain function
* modules, not Nest providers, and obtain the `EntityManager` via
* `RequestContext.getEntityManager()` (the same static accessor MikroORM's
* own per-request middleware and `CronRegistrarService`'s `wrappedTick`
* populate) rather than constructor injection. No module registers this
* class as a provider — nothing today asks Nest's DI container to resolve
* it.
* Provided through Nest DI by `MaintenanceModule` (`nest/database/`), a
* factory over the context-resolving global `EntityManager`; the readiness
* probe and `UserCleanupService` inject it. `backup.impl.ts` and
* `demo/demo-reset.ts` still build one over the request's EntityManager until
* they move behind an injected provider of their own. The class stays free of
* Nest decorators like every other file under `db/`, so a test can still
* build one over its own `EntityManager`.
*/
export class MaintenanceRepository {
constructor(private readonly em: EntityManager) {}
+3
View File
@@ -34,6 +34,7 @@ import { UserCleanupService } from './user-cleanup.service';
import { WebauthnConfigService } from './webauthn-config.service';
import { AppConfigModule } from '../app-config/app-config.module';
import { BudgetModule } from '../budget/budget.module';
import { MaintenanceModule } from '../database/maintenance.module';
import { AuditModule } from '../audit/audit.module';
import { MailerModule } from '../notifications/mailer/mailer.module';
import { PermissionsModule } from '../permissions/permissions.module';
@@ -84,6 +85,8 @@ import { AllowedFileTypesModule } from '../files/allowed-file-types.module';
StorageModule,
AllowedFileTypesModule,
EphemeralTokenModule, RateLimitModule, AuditModule, PermissionsModule, TripMembershipModule, MailerModule, AppConfigModule, TokensModule, BudgetModule,
// MaintenanceRepository: UserCleanupService's UC1 plugin-table erasure.
MaintenanceModule,
// AppSettings/Users: AuthService/UserProfileService each pass their own
// AppSettingsRepository/UsersRepository to instance-api-keys.ts's
// resolveApiKey/readInstanceApiKey/writeInstanceApiKey now (Plan 3a Task
+8 -8
View File
@@ -1,7 +1,6 @@
import fs from 'node:fs';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@mikro-orm/nestjs';
import { EntityManager } from '@mikro-orm/core';
import { MaintenanceRepository } from '../../db/repositories/MaintenanceRepository';
// Injected since BudgetModule dropped its AuthModule import (BudgetMcp's demo
// guard reads RuntimeEnvService + the users table now), which un-closed the
@@ -48,11 +47,11 @@ import { enqueueHookUserDataErasures } from '../plugins/user-erasure-enqueue';
@Injectable()
export class UserCleanupService {
constructor(
// Plan 4 Task 4: `DatabaseService` dropped — UC1 (below) now goes
// through `MaintenanceRepository`, the rule-4-permitted raw-connection
// home, constructed from this directly-injected `EntityManager`
// Plan 4 Task 4: `DatabaseService` dropped. UC1 (below) goes through
// `MaintenanceRepository`, the rule-4-permitted raw-connection home,
// injected over the context-resolving EntityManager
// (`uow.transactional` below always leaves a request context active).
private readonly em: EntityManager,
private readonly maintenance: MaintenanceRepository,
private readonly budget: BudgetService,
private readonly uow: UnitOfWork,
@InjectRepository(Users) private readonly usersRepo: UsersRepository,
@@ -92,8 +91,9 @@ export class UserCleanupService {
* deleted `DatabaseService` onto `MaintenanceRepository.deletePluginUserData`
* (rule 4's `connection.execute()` escape hatch), not onto a repository
* this domain would otherwise own. This method always runs inside
* `deleteUserCompletely`'s `uow.transactional(...)` — `this.em` is safe to
* pass as-is (MikroORM resolves the active transactional fork through
* `deleteUserCompletely`'s `uow.transactional(...)`. The injected
* repository's EntityManager is safe as-is (MikroORM resolves the active
* transactional fork through
* `EntityManager#getContext()`/`#getTransactionContext()` regardless of
* which EM instance you call them on, via its own `TransactionContext`
* async-local storage); `MaintenanceRepository.deletePluginUserData` is the
@@ -116,7 +116,7 @@ export class UserCleanupService {
* here, now via the same repository.
*/
async erasePluginUserData(userId: number): Promise<void> {
await new MaintenanceRepository(this.em).deletePluginUserData(userId); // UC1 — Plan 4 Task 4
await this.maintenance.deletePluginUserData(userId); // UC1, Plan 4 Task 4
try {
const rows = await this.pluginsRepo.listIdsAndPermissions(); // UC2 — Plan 4 Task 8a
const installed = new Set(rows.map((r) => r.id));
@@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { EntityManager } from '@mikro-orm/core';
import { MaintenanceRepository } from '../../db/repositories/MaintenanceRepository';
/**
* Provides `MaintenanceRepository`, the home of the few whole-database
* statements no entity can express (the readiness ping, the WAL checkpoint,
* `VACUUM INTO`, the plugin-table erasure).
*
* A factory over the context-resolving global `EntityManager`, the same one
* Nest injects into every service, so the repository resolves the request or
* transaction fork that is active when a method runs. The class itself stays
* free of Nest, like every other file under `db/`.
*/
@Module({
providers: [
{
provide: MaintenanceRepository,
useFactory: (em: EntityManager) => new MaintenanceRepository(em),
inject: [EntityManager],
},
],
exports: [MaintenanceRepository],
})
export class MaintenanceModule {}
@@ -1,5 +1,4 @@
import { Controller, Get, Res } from '@nestjs/common';
import { EntityManager } from '@mikro-orm/core';
import type { Response } from 'express';
import { MaintenanceRepository } from '../../db/repositories/MaintenanceRepository';
import { KitineraryExtractorService } from '../booking-import/kitinerary-extractor.service';
@@ -15,7 +14,7 @@ export class FeaturesController {
constructor(
private readonly extractor: KitineraryExtractorService,
private readonly addons: AddonsService,
private readonly em: EntityManager,
private readonly maintenance: MaintenanceRepository,
) {}
/** The container/uptime probe. The forced-HTTPS redirect and HSTS exempt this
@@ -37,7 +36,7 @@ export class FeaturesController {
async ready(@Res() res: Response): Promise<void> {
res.setHeader('Cache-Control', 'no-store, must-revalidate');
try {
await new MaintenanceRepository(this.em).ping();
await this.maintenance.ping();
res.json({ status: 'ready' });
} catch {
res.status(503).json({ status: 'unavailable' });
+2 -1
View File
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { FeaturesController } from './features.controller';
import { KitineraryExtractorModule } from '../booking-import/kitinerary-extractor.module';
import { AddonsModule } from '../addons/addons.module';
import { MaintenanceModule } from '../database/maintenance.module';
/** Server capability reporting. `GET /api/health/features` tells the client which
* optional server-side features are usable, so it can hide the affordances it
@@ -9,7 +10,7 @@ import { AddonsModule } from '../addons/addons.module';
* redirect and HSTS exempt it by path inside globalMiddleware, so it answers
* plain-HTTP probes no matter where the route itself is registered. */
@Module({
imports: [KitineraryExtractorModule, AddonsModule],
imports: [KitineraryExtractorModule, AddonsModule, MaintenanceModule],
controllers: [FeaturesController],
})
export class HealthModule {}
+3 -2
View File
@@ -188,6 +188,7 @@ import { noGoogleQuota } from './google-quota';
import { createTestPushSubscriptionsRepo } from './notifications-repos';
import { createTestToursRepo } from './tours-repos';
import { createTestBudgetSettlementsRepo } from './budget-repos';
import { MaintenanceRepository } from '../../src/db/repositories/MaintenanceRepository';
/**
* Hand-wired counterpart of the boot-time discovery in McpRegistryService,
@@ -232,7 +233,7 @@ export async function createMcpTestRegistry(): Promise<McpRegistry> {
permissionsService,
new TripMembershipService(await createTestTripsRepo(db), await createTestTripMembersRepo(db)),
new WebauthnConfigService(appSettings),
new UserCleanupService(mcpOrm.em, budgetService, await createTestUnitOfWork(db), usersRepo, await createTestTripMembersRepo(db), await createTestBudgetItemsRepo(db), await createTestBudgetSettlementsRepo(db), await createTestJourneyShareTokensRepo(db), await createTestJourneysRepo(db), await createTestJourneyEntriesRepo(db), await createTestJourneyContributorsRepo(db), await createTestShareTokensRepo(db), await createTestPluginsRepo(db), await createTestPluginUserErasureQueueRepo(db)),
new UserCleanupService(new MaintenanceRepository(mcpOrm.em), budgetService, await createTestUnitOfWork(db), usersRepo, await createTestTripMembersRepo(db), await createTestBudgetItemsRepo(db), await createTestBudgetSettlementsRepo(db), await createTestJourneyShareTokensRepo(db), await createTestJourneysRepo(db), await createTestJourneyEntriesRepo(db), await createTestJourneyContributorsRepo(db), await createTestShareTokensRepo(db), await createTestPluginsRepo(db), await createTestPluginUserErasureQueueRepo(db)),
new MailerService(usersRepo, settingsRepo, appSettings),
new EphemeralTokenService(),
new AllowedFileTypesService(appSettings), await createTestUnitOfWork(db),
@@ -340,7 +341,7 @@ export async function createMcpTestRegistry(): Promise<McpRegistry> {
daysService, accommodationsService, assignmentsService, await createTestUnitOfWork(db),
await createTestDaysRepo(db), await createTestDayAccommodationsRepo(db), await createTestRoadtripDayBoundariesRepo(db), await createTestTripsRepo(db),
);
const membersService = new TripMembersService(budgetService, new UserCleanupService(mcpOrm.em, budgetService, await createTestUnitOfWork(db), usersRepo, await createTestTripMembersRepo(db), await createTestBudgetItemsRepo(db), await createTestBudgetSettlementsRepo(db), await createTestJourneyShareTokensRepo(db), await createTestJourneysRepo(db), await createTestJourneyEntriesRepo(db), await createTestJourneyContributorsRepo(db), await createTestShareTokensRepo(db), await createTestPluginsRepo(db), await createTestPluginUserErasureQueueRepo(db)), permissionsService, realtimeService, notificationsStub(), await createTestUnitOfWork(db), await createTestTripsRepo(db), await createTestTripMembersRepo(db), usersRepo);
const membersService = new TripMembersService(budgetService, new UserCleanupService(new MaintenanceRepository(mcpOrm.em), budgetService, await createTestUnitOfWork(db), usersRepo, await createTestTripMembersRepo(db), await createTestBudgetItemsRepo(db), await createTestBudgetSettlementsRepo(db), await createTestJourneyShareTokensRepo(db), await createTestJourneysRepo(db), await createTestJourneyEntriesRepo(db), await createTestJourneyContributorsRepo(db), await createTestShareTokensRepo(db), await createTestPluginsRepo(db), await createTestPluginUserErasureQueueRepo(db)), permissionsService, realtimeService, notificationsStub(), await createTestUnitOfWork(db), await createTestTripsRepo(db), await createTestTripMembersRepo(db), usersRepo);
const tripsService = new TripsService(
reservationsService,
daysService,
+2 -1
View File
@@ -142,6 +142,7 @@ import { createTestJourneyShareTokensRepo } from './journey-share-repos';
import { noGoogleQuota } from './google-quota';
import { createTestToursRepo } from './tours-repos';
import { createTestBudgetSettlementsRepo } from './budget-repos';
import { MaintenanceRepository } from '../../src/db/repositories/MaintenanceRepository';
/**
* Hand-wired counterpart of the PluginsModule DI graph for no-Nest tests
@@ -302,7 +303,7 @@ export async function createPluginRpcHostFactory(db: Database.Database): Promise
const trips = new TripsService(reservations, days, permissions, budget, vacay, realtime, unsplash, generalStorage, await createTestUnitOfWork(db), (await sharedTestOrm(db)).em, new SettingsService(await createTestUnitOfWork(db), appSettings, await createTestSettingsRepo(db)));
// Plan 4 Task 4: UserCleanupService's UC1 now goes through a directly-injected
// EntityManager (MaintenanceRepository.deletePluginUserData), not DatabaseService.
const members = new TripMembersService(budget, new UserCleanupService((await sharedTestOrm(db)).em, budget, await createTestUnitOfWork(db), usersRepo, await createTestTripMembersRepo(db), await createTestBudgetItemsRepo(db), await createTestBudgetSettlementsRepo(db), await createTestJourneyShareTokensRepo(db), await createTestJourneysRepo(db), await createTestJourneyEntriesRepo(db), await createTestJourneyContributorsRepo(db), await createTestShareTokensRepo(db), await createTestPluginsRepo(db), await createTestPluginUserErasureQueueRepo(db)), permissions, realtime, notificationsStub(), await createTestUnitOfWork(db), await createTestTripsRepo(db), await createTestTripMembersRepo(db), usersRepo);
const members = new TripMembersService(budget, new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(db)).em), budget, await createTestUnitOfWork(db), usersRepo, await createTestTripMembersRepo(db), await createTestBudgetItemsRepo(db), await createTestBudgetSettlementsRepo(db), await createTestJourneyShareTokensRepo(db), await createTestJourneysRepo(db), await createTestJourneyEntriesRepo(db), await createTestJourneyContributorsRepo(db), await createTestShareTokensRepo(db), await createTestPluginsRepo(db), await createTestPluginUserErasureQueueRepo(db)), permissions, realtime, notificationsStub(), await createTestUnitOfWork(db), await createTestTripsRepo(db), await createTestTripMembersRepo(db), usersRepo);
// Plan 3j Task 1 — PluginGuards' own role lookup (PG3/PG4) now goes
// through UsersRepository.getRole; `usersRepo` above is the same
// sharedTestOrm-backed repository every other service in this file uses.
+2 -1
View File
@@ -111,6 +111,7 @@ import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourney
import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos';
import { createTestPushSubscriptionsRepo } from '../../helpers/notifications-repos';
import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos';
import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository';
const realtime = new RealtimeService();
@@ -130,7 +131,7 @@ let auditLogRepo: AuditLogRepository;
beforeAll(async () => {
webauthn = new WebauthnConfigService(await createTestAppSettingsRepo(testDb));
permissions = new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb));
userCleanup = new UserCleanupService((await sharedTestOrm(testDb)).em, new BudgetService(permissions, new ExchangeRatesService(), realtime, await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb));
userCleanup = new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(testDb)).em), new BudgetService(permissions, new ExchangeRatesService(), realtime, await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb));
auth = new AuthService(
permissions, new TripMembershipService(await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb)), webauthn, userCleanup, new MailerService(await createTestUsersRepo(testDb), await createTestSettingsRepo(testDb), await createTestAppSettingsRepo(testDb)), new EphemeralTokenService(), new AllowedFileTypesService(await createTestAppSettingsRepo(testDb)), await createTestUnitOfWork(testDb),
await createTestAppSettingsRepo(testDb), await createTestUsersRepo(testDb), await createTestInviteTokensRepo(testDb), await createTestMcpTokensRepo(testDb),
@@ -87,6 +87,7 @@ import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourney
import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos';
import { createTestPushSubscriptionsRepo } from '../../helpers/notifications-repos';
import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos';
import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository';
const realtime = new RealtimeService();
@@ -104,7 +105,7 @@ let svc: AdminService;
beforeAll(async () => {
webauthn = new WebauthnConfigService(await createTestAppSettingsRepo(testDb));
permissions = new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb));
userCleanup = new UserCleanupService((await sharedTestOrm(testDb)).em, new BudgetService(permissions, new ExchangeRatesService(), realtime, await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb));
userCleanup = new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(testDb)).em), new BudgetService(permissions, new ExchangeRatesService(), realtime, await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb));
auth = new AuthService(
permissions, new TripMembershipService(await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb)), webauthn, userCleanup, new MailerService(await createTestUsersRepo(testDb), await createTestSettingsRepo(testDb), await createTestAppSettingsRepo(testDb)), new EphemeralTokenService(), new AllowedFileTypesService(await createTestAppSettingsRepo(testDb)), await createTestUnitOfWork(testDb),
await createTestAppSettingsRepo(testDb), await createTestUsersRepo(testDb), await createTestInviteTokensRepo(testDb), await createTestMcpTokensRepo(testDb),
+2 -1
View File
@@ -100,6 +100,7 @@ import { createTestBudgetItemsRepo } from '../../helpers/files-repos';
import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourneyContributorsRepo } from '../../helpers/journey-repos';
import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos';
import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos';
import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository';
// MailerService is injected since the notifications fold — a stub instead of a
// module mock. sendPasswordResetEmail is the only thing auth reaches for.
@@ -120,7 +121,7 @@ beforeAll(async () => {
new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)),
membershipStub,
new WebauthnConfigService(await createTestAppSettingsRepo(testDb)),
new UserCleanupService((await sharedTestOrm(testDb)).em, new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)),
new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(testDb)).em), new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)),
mailerStub,
new EphemeralTokenService(),
new AllowedFileTypesService(await createTestAppSettingsRepo(testDb)), await createTestUnitOfWork(testDb),
@@ -78,6 +78,7 @@ import { createTestBudgetItemsRepo } from '../../helpers/files-repos';
import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourneyContributorsRepo } from '../../helpers/journey-repos';
import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos';
import { BudgetItemMembers } from '../../../src/db/entities/BudgetItemMembers.entity';
import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository';
// Guest fixtures come from TripMembersService since the trip split (they were on
// TripsService before, and on the deleted services/tripService before that);
@@ -114,7 +115,7 @@ beforeAll(async () => {
);
membersSvc = new TripMembersService(
budget,
new UserCleanupService(dbsEm!, budget, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)),
new UserCleanupService(new MaintenanceRepository(dbsEm!), budget, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)),
new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)),
new RealtimeService(),
notificationsStub(),
@@ -6,6 +6,8 @@ import { KitineraryExtractorService } from '../../../src/nest/booking-import/kit
import { BookingImportModule } from '../../../src/nest/booking-import/booking-import.module';
import { expectRegisteredController, expectRegisteredProvider } from '../../helpers/module-providers';
import { ADDON_IDS } from '../../../src/addons';
import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository';
import { MaintenanceModule } from '../../../src/nest/database/maintenance.module';
type Extractor = Pick<KitineraryExtractorService, 'isAvailable'>;
type Addons = { isAddonEnabled: (id: string) => boolean };
@@ -62,7 +64,7 @@ describe('FeaturesController (GET /api/health/features)', () => {
expectRegisteredProvider(KitineraryExtractorModule, KitineraryExtractorService);
const healthImports = Reflect.getMetadata('imports', HealthModule) as unknown[];
expect(Array.isArray(healthImports)).toBe(true);
expect(healthImports).toEqual(expect.arrayContaining([KitineraryExtractorModule]));
expect(healthImports).toEqual(expect.arrayContaining([KitineraryExtractorModule, MaintenanceModule]));
expect(healthImports).not.toContain(BookingImportModule);
});
@@ -95,7 +97,7 @@ describe('FeaturesController (GET /api/health/features)', () => {
const execute = vi.fn().mockResolvedValue([]);
const em = { getConnection: () => ({ execute }), getContext: () => em };
const extractor = { isAvailable: vi.fn(() => true) };
const controller = new FeaturesController(extractor as Extractor as KitineraryExtractorService, {} as never, em as never);
const controller = new FeaturesController(extractor as Extractor as KitineraryExtractorService, {} as never, new MaintenanceRepository(em as never));
const ok = response();
await controller.ready(ok as never);
+2 -1
View File
@@ -121,6 +121,7 @@ import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourney
import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos';
import { createTestPushSubscriptionsRepo } from '../../helpers/notifications-repos';
import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos';
import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository';
// MailerService is injected since the notifications fold — a stub instead of a
// module mock. sendPasswordResetEmail is the only thing auth reaches for.
@@ -140,7 +141,7 @@ beforeAll(async () => {
new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)),
membership,
new WebauthnConfigService(await createTestAppSettingsRepo(testDb)),
new UserCleanupService((await sharedTestOrm(testDb)).em, new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)),
new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(testDb)).em), new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)),
mailerStub,
new EphemeralTokenService(),
new AllowedFileTypesService(await createTestAppSettingsRepo(testDb)), await createTestUnitOfWork(testDb),
@@ -106,6 +106,7 @@ import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourney
import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos';
import { createTestPushSubscriptionsRepo } from '../../helpers/notifications-repos';
import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos';
import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository';
// MailerService is injected since the notifications fold — a stub instead of a
// module mock. sendPasswordResetEmail is the only thing auth reaches for.
@@ -134,7 +135,7 @@ beforeAll(async () => {
new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)),
new TripMembershipService(await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb)),
new WebauthnConfigService(await createTestAppSettingsRepo(testDb)),
new UserCleanupService((await sharedTestOrm(testDb)).em, new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)),
new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(testDb)).em), new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))), await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)),
mailerStub,
new EphemeralTokenService(),
new AllowedFileTypesService(await createTestAppSettingsRepo(testDb)), await createTestUnitOfWork(testDb),
@@ -71,6 +71,7 @@ import type { TripsRepository } from '../../../src/db/repositories/Trips.reposit
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
import type { TripMembersRepository } from '../../../src/db/repositories/TripMembers.repository';
import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos';
import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository';
// Plan 3c Task 0b: `dbsEm` is resolved once, at the top of the `beforeAll`
// below — `canAccessTrip`/`isOwner`/`rosterUserIds`/`getPlaceWithTags`
@@ -97,7 +98,7 @@ beforeAll(async () => {
budgetSvc = new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb)));
roster = new TripMembersService(
budgetSvc,
new UserCleanupService(dbsEm!, budgetSvc, await createTestUnitOfWork(testDb), usersRepo, await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)),
new UserCleanupService(new MaintenanceRepository(dbsEm!), budgetSvc, await createTestUnitOfWork(testDb), usersRepo, await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)),
new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)),
new RealtimeService(), notificationsStub(notifySend), await createTestUnitOfWork(testDb),
tripsRepo, tripMembersRepo, usersRepo,
@@ -111,6 +111,7 @@ import { createTestShareTokensRepo, createTestPluginsRepo, createTestPluginUserE
import { createTestCollectionPlacesRepo } from '../../helpers/test-uow';
import { noGoogleQuota } from '../../helpers/google-quota';
import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos';
import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository';
// Real sibling services over the same in-memory DB — the aggregation runs the
// actual SQL of every domain it fans out to, so a shape change downstream shows
@@ -176,7 +177,7 @@ beforeAll(async () => {
await createTestBudgetItemsRepo(testDb),
await createTestCollectionPlacesRepo(testDb),
);
membersSvc = new TripMembersService(budgetSvc, new UserCleanupService((await sharedTestOrm(testDb)).em, budgetSvc, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), notificationsStub(), await createTestUnitOfWork(testDb), await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb), await createTestUsersRepo(testDb));
membersSvc = new TripMembersService(budgetSvc, new UserCleanupService(new MaintenanceRepository((await sharedTestOrm(testDb)).em), budgetSvc, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), notificationsStub(), await createTestUnitOfWork(testDb), await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb), await createTestUsersRepo(testDb));
});
const buildReadModel = async (tripsRepo: TripsRepository, roster: TripMembersService = membersSvc) =>
+2 -1
View File
@@ -131,6 +131,7 @@ import { createTestCollectionPlacesRepo } from '../../helpers/test-uow';
import { SettingsService } from '../../../src/nest/settings/settings.service';
import { noGoogleQuota } from '../../helpers/google-quota';
import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos';
import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository';
// Real sibling services over the same in-memory DB — updateTrip's date-shift
// resyncs and the summary/bundle aggregation run their actual SQL.
@@ -238,7 +239,7 @@ beforeAll(async () => {
(await sharedTestOrm(testDb)).em,
new SettingsService(await createTestUnitOfWork(testDb), await createTestAppSettingsRepo(testDb), await createTestSettingsRepo(testDb)),
);
membersSvc = new TripMembersService(budgetSvc, new UserCleanupService(dbsEm!, budgetSvc, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), notificationsStub(), await createTestUnitOfWork(testDb), await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb), await createTestUsersRepo(testDb));
membersSvc = new TripMembersService(budgetSvc, new UserCleanupService(new MaintenanceRepository(dbsEm!), budgetSvc, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb), await createTestTripMembersRepo(testDb), await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb), await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestJourneyContributorsRepo(testDb), await createTestShareTokensRepo(testDb), await createTestPluginsRepo(testDb), await createTestPluginUserErasureQueueRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), notificationsStub(), await createTestUnitOfWork(testDb), await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb), await createTestUsersRepo(testDb));
readModelSvc = new TripReadModelService(
await createTestTripsRepo(testDb), membersSvc, daysSvc, accommodationsSvc, budgetSvc,
new PackingService(
@@ -58,18 +58,19 @@ import { createTestJourneysRepo, createTestJourneyEntriesRepo, createTestJourney
import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos';
import { createTestShareTokensRepo, createTestPluginsRepo, createTestPluginUserErasureQueueRepo } from '../../helpers/share-repos';
import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos';
import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository';
let em: EntityManager;
let budget: BudgetService;
let svc: UserCleanupService;
beforeAll(async () => {
// Plan 4 Task 4: UserCleanupService's own DatabaseService param is gone —
// UC1 goes through MaintenanceRepository, built from a directly-injected
// EntityManager instead.
// Plan 4 Task 4: UserCleanupService's own DatabaseService param is gone.
// UC1 goes through MaintenanceRepository, which Nest injects (built here
// over the test ORM's EntityManager, as MaintenanceModule's factory does).
em = (await sharedTestOrm(testDb)).em;
budget = new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb)));
svc = new UserCleanupService(
em, budget, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb),
new MaintenanceRepository(em), budget, await createTestUnitOfWork(testDb), await createTestUsersRepo(testDb),
// Plan 4 Task 1 constructor-ripple: UC4's repository.
await createTestTripMembersRepo(testDb),
await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb),
@@ -181,7 +182,7 @@ describe('erasePluginUserData', () => {
// journey repositories (UC7-10) still never reach their tables from this
// call — stubs are enough for those.
const slimSvc = new UserCleanupService(
(await sharedTestOrm(slim)).em, budget, await createTestUnitOfWork(slim), await createTestUsersRepo(slim),
new MaintenanceRepository((await sharedTestOrm(slim)).em), budget, await createTestUnitOfWork(slim), await createTestUsersRepo(slim),
{} as unknown as TripMembersRepository, {} as unknown as BudgetItemsRepository, {} as unknown as BudgetSettlementsRepository,
{} as unknown as JourneyShareTokensRepository, {} as unknown as JourneysRepository,
{} as unknown as JourneyEntriesRepository, {} as unknown as JourneyContributorsRepository,
@@ -342,7 +343,7 @@ describe('deleteUserCompletely', () => {
const deleteByIdSpy = vi.spyOn(usersRepo, 'deleteById').mockRejectedValue(new Error('boom'));
try {
await expect(new UserCleanupService(
em, budget, await createTestUnitOfWork(testDb), usersRepo,
new MaintenanceRepository(em), budget, await createTestUnitOfWork(testDb), usersRepo,
await createTestTripMembersRepo(testDb),
await createTestBudgetItemsRepo(testDb), await createTestBudgetSettlementsRepo(testDb),
await createTestJourneyShareTokensRepo(testDb), await createTestJourneysRepo(testDb),