From ea7b93de5d371f6ad35c8d7c96bb4a638ea234fe Mon Sep 17 00:00:00 2001 From: jubnl Date: Thu, 24 Sep 2026 05:47:29 +0200 Subject: [PATCH] =?UTF-8?q?refactor(server):=20plan=204=20=E2=80=94=20inli?= =?UTF-8?q?ne=20canAccessTrip/isOwner=20off=20DatabaseService=20(plugins?= =?UTF-8?q?=20+=20MCP=20surfaces)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/nest/budget/costs.rpc.ts | 10 +++++-- server/src/nest/feeds/feeds.mcp.ts | 11 +++++-- .../contributions/day-schedule.controller.ts | 7 +++-- .../contributions/day-tints.controller.ts | 7 +++-- .../contributions/map-layers.controller.ts | 8 +++-- .../contributions/map-markers.controller.ts | 8 +++-- .../contributions/pdf-sections.controller.ts | 8 +++-- .../contributions/place-details.controller.ts | 7 +++-- .../plugin-contributions.module.ts | 3 +- .../contributions/plugin-routes.controller.ts | 7 +++-- .../trip-card-contributions.controller.ts | 8 +++-- .../contributions/trip-warnings.controller.ts | 8 +++-- .../contributions/trip-warnings.mcp.ts | 8 +++-- .../view-contributions.controller.ts | 8 +++-- .../nest/plugins/host/plugin-guards.module.ts | 3 +- .../plugins/host/plugin-guards.service.ts | 14 +++++---- .../nest/plugins/host/rpc/host-surface.rpc.ts | 8 ++--- server/src/nest/plugins/host/rpc/meta.rpc.ts | 6 ++-- .../reservation-import.mcp.ts | 11 +++++-- .../reservation-import.module.ts | 4 ++- server/src/nest/transit/transit.mcp.ts | 11 +++++-- server/src/nest/transit/transit.module.ts | 10 +++---- server/tests/helpers/mcp-test-controllers.ts | 28 +++++++++--------- server/tests/helpers/plugin-host.ts | 29 +++++++++++-------- server/tests/helpers/rpc-host-deps.ts | 6 ++-- server/tests/unit/budget/costs.rpc.test.ts | 6 ++-- server/tests/unit/days/day-notes.rpc.test.ts | 6 ++-- .../unit/days/days-itinerary.rpc.test.ts | 6 ++-- server/tests/unit/files/files.rpc.test.ts | 10 +++---- server/tests/unit/packing/packing.rpc.test.ts | 6 ++-- server/tests/unit/places/places.rpc.test.ts | 6 ++-- .../plugins/addon-domains-errors.rpc.test.ts | 6 ++-- .../unit/plugins/addon-gates.rpc.test.ts | 6 ++-- .../plugins/day-schedule.controller.test.ts | 4 +-- .../unit/plugins/day-tints.controller.test.ts | 4 +-- server/tests/unit/plugins/host-rpc.test.ts | 28 +++++++++--------- .../plugins/map-layers.controller.test.ts | 4 +-- .../plugins/map-markers.controller.test.ts | 4 +-- .../plugins/pdf-sections.controller.test.ts | 4 +-- .../plugins/place-details.controller.test.ts | 4 +-- .../tests/unit/plugins/plugin-guards.test.ts | 14 ++++----- .../plugins/plugin-routes.controller.test.ts | 4 +-- .../unit/plugins/request-context.test.ts | 5 +++- .../tests/unit/plugins/rpc-fallbacks.test.ts | 6 ++-- .../unit/plugins/rpc-host-inv-strip.test.ts | 6 ++-- ...trip-card-contributions.controller.test.ts | 4 +-- .../plugins/trip-warnings.controller.test.ts | 4 +-- .../view-contributions.controller.test.ts | 4 +-- .../unit/reservations/bookings.rpc.test.ts | 6 ++-- server/tests/unit/todo/todo.rpc.test.ts | 6 ++-- server/tests/unit/trips/trips.rpc.test.ts | 10 +++---- 51 files changed, 232 insertions(+), 179 deletions(-) diff --git a/server/src/nest/budget/costs.rpc.ts b/server/src/nest/budget/costs.rpc.ts index ea256373c..ba6e07e00 100644 --- a/server/src/nest/budget/costs.rpc.ts +++ b/server/src/nest/budget/costs.rpc.ts @@ -4,8 +4,10 @@ import { PluginGuards } from '../plugins/host/plugin-guards.service'; import { BadParams, ForbiddenResource } from '../plugins/host/rpc-errors'; import { num, schemaMessage } from '../plugins/host/rpc-params'; import type { PluginRpcContext } from '../plugins/host/rpc-kit/types'; +import { InjectRepository } from '@mikro-orm/nestjs'; import { RealtimeService } from '../realtime/realtime.service'; -import { DatabaseService } from '../database/database.service'; +import { Trips } from '../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { TripMembershipService } from '../trip-membership/trip-membership.service'; import { ADDON_IDS } from '../../addons'; import { BudgetService } from './budget.service'; @@ -30,7 +32,9 @@ const BUDGET_EDIT_ACTION = 'budget_edit'; export class CostsRpc { constructor( private readonly budget: BudgetService, - private readonly db: DatabaseService, + // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: + // this injects TripsRepository directly and calls findAccessible. + @InjectRepository(Trips) private readonly trips: TripsRepository, private readonly realtime: RealtimeService, private readonly guards: PluginGuards, private readonly membership: TripMembershipService, @@ -120,7 +124,7 @@ export class CostsRpc { /** Trip access plus budget_edit, with the cost-specific refusal message. */ private async requireCostEdit(tripId: number, userId: number): Promise { - if (!(await this.db.canAccessTrip(tripId, userId))) throw new ForbiddenResource(`no access to trip ${tripId}`); + if (!(await this.trips.findAccessible(tripId, userId))) throw new ForbiddenResource(`no access to trip ${tripId}`); if (!(await this.guards.canEditAs(BUDGET_EDIT_ACTION, tripId, userId))) { throw new ForbiddenResource(`no permission to edit costs on trip ${tripId}`); } diff --git a/server/src/nest/feeds/feeds.mcp.ts b/server/src/nest/feeds/feeds.mcp.ts index 7f6262d23..4bb580a88 100644 --- a/server/src/nest/feeds/feeds.mcp.ts +++ b/server/src/nest/feeds/feeds.mcp.ts @@ -5,7 +5,9 @@ import { } from '../../nest-mcp'; import { z } from 'zod'; import { getAppUrl } from '../../app-config'; -import { DatabaseService } from '../database/database.service'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { Trips } from '../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { RuntimeEnvService } from '../app-config/runtime-env.service'; import { DemoService } from '../common/demo.service'; import { McpToolGuardsService } from '../mcp-shared/mcp-tool-guards.service'; @@ -35,7 +37,10 @@ import { FeedsService } from './feeds.service'; export class FeedsMcp { constructor( private readonly feeds: FeedsService, - private readonly db: DatabaseService, + // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: + // this injects TripsRepository directly (same constructor slot) and + // calls findAccessible. + @InjectRepository(Trips) private readonly trips: TripsRepository, private readonly env: RuntimeEnvService, private readonly guards: McpToolGuardsService, private readonly demo: DemoService, @@ -59,7 +64,7 @@ export class FeedsMcp { /** Trip access first (404-equivalent), then share_manage, exactly as TripFeedTokenController is gated. */ private async denyTripFeed(tripId: number, userId: number) { - if (!(await this.db.canAccessTrip(tripId, userId))) return noAccess(); + if (!(await this.trips.findAccessible(tripId, userId))) return noAccess(); if (!(await this.guards.hasTripPermission('share_manage', tripId, userId))) return permissionDenied(); return null; } diff --git a/server/src/nest/plugins/contributions/day-schedule.controller.ts b/server/src/nest/plugins/contributions/day-schedule.controller.ts index 305cea28c..b4991c9ed 100644 --- a/server/src/nest/plugins/contributions/day-schedule.controller.ts +++ b/server/src/nest/plugins/contributions/day-schedule.controller.ts @@ -1,7 +1,8 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { DatabaseService } from '../../database/database.service'; +import { Trips } from '../../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { Days } from '../../../db/entities/Days.entity'; import type { DaysRepository } from '../../../db/repositories/Days.repository'; import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; @@ -83,7 +84,7 @@ function normalize(pluginId: string, tripDayIds: ReadonlySet, raw: unkno export class DayScheduleController { constructor( private readonly hooks: PluginHooks, - private readonly dbs: DatabaseService, + @InjectRepository(Trips) private readonly trips: TripsRepository, // CT1 (Plan 3j Task 5) — the trip's day-id set, converted onto Days.repository.ts. @InjectRepository(Days) private readonly days: DaysRepository, ) {} @@ -96,7 +97,7 @@ export class DayScheduleController { if (!pluginsEnabled()) return { items: [] }; const tripId = Number(tripIdRaw); const userId = req.user?.id; - if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { items: [] }; + if (!Number.isFinite(tripId) || userId == null || !(await this.trips.findAccessible(tripId, userId))) return { items: [] }; const ids = this.hooks.providersOf('dayScheduleProvider'); if (ids.length === 0) return { items: [] }; diff --git a/server/src/nest/plugins/contributions/day-tints.controller.ts b/server/src/nest/plugins/contributions/day-tints.controller.ts index b9000e238..de0da14d8 100644 --- a/server/src/nest/plugins/contributions/day-tints.controller.ts +++ b/server/src/nest/plugins/contributions/day-tints.controller.ts @@ -1,7 +1,8 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { DatabaseService } from '../../database/database.service'; +import { Trips } from '../../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { Days } from '../../../db/entities/Days.entity'; import type { DaysRepository } from '../../../db/repositories/Days.repository'; import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; @@ -160,7 +161,7 @@ function normalize(pluginId: string, tripDayIds: ReadonlySet, raw: unkno export class DayTintsController { constructor( private readonly hooks: PluginHooks, - private readonly dbs: DatabaseService, + @InjectRepository(Trips) private readonly trips: TripsRepository, // CT2 (Plan 3j Task 5) — the trip's day-id set, converted onto Days.repository.ts. @InjectRepository(Days) private readonly days: DaysRepository, ) {} @@ -173,7 +174,7 @@ export class DayTintsController { if (!pluginsEnabled()) return { tints: [] }; const tripId = Number(tripIdRaw); const userId = req.user?.id; - if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { tints: [] }; + if (!Number.isFinite(tripId) || userId == null || !(await this.trips.findAccessible(tripId, userId))) return { tints: [] }; const ids = this.hooks.providersOf('dayTintProvider'); if (ids.length === 0) return { tints: [] }; diff --git a/server/src/nest/plugins/contributions/map-layers.controller.ts b/server/src/nest/plugins/contributions/map-layers.controller.ts index f5b026150..8bcfd5c9f 100644 --- a/server/src/nest/plugins/contributions/map-layers.controller.ts +++ b/server/src/nest/plugins/contributions/map-layers.controller.ts @@ -1,6 +1,8 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { DatabaseService } from '../../database/database.service'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { Trips } from '../../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; @@ -147,7 +149,7 @@ function normalize(pluginId: string, raw: unknown): MapLayer[] { export class MapLayersController { constructor( private readonly hooks: PluginHooks, - private readonly dbs: DatabaseService, + @InjectRepository(Trips) private readonly trips: TripsRepository, ) {} @Get(':tripId') @@ -158,7 +160,7 @@ export class MapLayersController { if (!pluginsEnabled()) return { layers: [] }; const tripId = Number(tripIdRaw); const userId = req.user?.id; - if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { layers: [] }; + if (!Number.isFinite(tripId) || userId == null || !(await this.trips.findAccessible(tripId, userId))) return { layers: [] }; const ids = this.hooks.providersOf('mapLayerProvider'); const perProvider = await Promise.all( diff --git a/server/src/nest/plugins/contributions/map-markers.controller.ts b/server/src/nest/plugins/contributions/map-markers.controller.ts index f5a676de5..7e6eb59c0 100644 --- a/server/src/nest/plugins/contributions/map-markers.controller.ts +++ b/server/src/nest/plugins/contributions/map-markers.controller.ts @@ -1,6 +1,8 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { DatabaseService } from '../../database/database.service'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { Trips } from '../../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; @@ -80,7 +82,7 @@ function normalize(pluginId: string, raw: unknown): MapMarker[] { export class MapMarkersController { constructor( private readonly hooks: PluginHooks, - private readonly dbs: DatabaseService, + @InjectRepository(Trips) private readonly trips: TripsRepository, ) {} @Get(':tripId') @@ -91,7 +93,7 @@ export class MapMarkersController { if (!pluginsEnabled()) return { markers: [] }; const tripId = Number(tripIdRaw); const userId = req.user?.id; - if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { markers: [] }; + if (!Number.isFinite(tripId) || userId == null || !(await this.trips.findAccessible(tripId, userId))) return { markers: [] }; const ids = this.hooks.providersOf('mapMarkerProvider'); const perProvider = await Promise.all( diff --git a/server/src/nest/plugins/contributions/pdf-sections.controller.ts b/server/src/nest/plugins/contributions/pdf-sections.controller.ts index ec2f3f0ce..ec93ae578 100644 --- a/server/src/nest/plugins/contributions/pdf-sections.controller.ts +++ b/server/src/nest/plugins/contributions/pdf-sections.controller.ts @@ -1,6 +1,8 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { DatabaseService } from '../../database/database.service'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { Trips } from '../../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; @@ -76,7 +78,7 @@ function normalize(pluginId: string, raw: unknown): PdfSection[] { export class PdfSectionsController { constructor( private readonly hooks: PluginHooks, - private readonly dbs: DatabaseService, + @InjectRepository(Trips) private readonly trips: TripsRepository, ) {} @Get(':tripId') @@ -87,7 +89,7 @@ export class PdfSectionsController { if (!pluginsEnabled()) return { sections: [] }; const tripId = Number(tripIdRaw); const userId = req.user?.id; - if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { sections: [] }; + if (!Number.isFinite(tripId) || userId == null || !(await this.trips.findAccessible(tripId, userId))) return { sections: [] }; const ids = this.hooks.providersOf('pdfSectionProvider'); const perProvider = await Promise.all( diff --git a/server/src/nest/plugins/contributions/place-details.controller.ts b/server/src/nest/plugins/contributions/place-details.controller.ts index 9680d3aed..e3251cfbd 100644 --- a/server/src/nest/plugins/contributions/place-details.controller.ts +++ b/server/src/nest/plugins/contributions/place-details.controller.ts @@ -1,7 +1,8 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { DatabaseService } from '../../database/database.service'; +import { Trips } from '../../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { Places } from '../../../db/entities/Places.entity'; import type { PlacesRepository } from '../../../db/repositories/Places.repository'; import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; @@ -66,7 +67,7 @@ function normalize(raw: unknown): DetailItem[] { export class PlaceDetailsController { constructor( private readonly hooks: PluginHooks, - private readonly dbs: DatabaseService, + @InjectRepository(Trips) private readonly trips: TripsRepository, // CT7 (Plan 3j Task 5) — the place's owning trip id, converted onto Places.repository.ts. @InjectRepository(Places) private readonly places: PlacesRepository, ) {} @@ -83,7 +84,7 @@ export class PlaceDetailsController { // The place must belong to a trip the caller can access — same gate as a read. const tripId = await this.places.findTripId(placeId); // CT7 — Plan 3j - if (tripId === undefined || !(await this.dbs.canAccessTrip(tripId, userId))) return { providers: [] }; + if (tripId === undefined || !(await this.trips.findAccessible(tripId, userId))) return { providers: [] }; const ids = this.hooks.providersOf('placeDetailProvider'); const results = await Promise.all( diff --git a/server/src/nest/plugins/contributions/plugin-contributions.module.ts b/server/src/nest/plugins/contributions/plugin-contributions.module.ts index bfdd83dc2..e993f4717 100644 --- a/server/src/nest/plugins/contributions/plugin-contributions.module.ts +++ b/server/src/nest/plugins/contributions/plugin-contributions.module.ts @@ -7,6 +7,7 @@ import { JourneyEntries } from '../../../db/entities/JourneyEntries.entity'; import { Plugins } from '../../../db/entities/Plugins.entity'; import { Days } from '../../../db/entities/Days.entity'; import { Places } from '../../../db/entities/Places.entity'; +import { Trips } from '../../../db/entities/Trips.entity'; import { PlaceDetailsController } from './place-details.controller'; import { PluginSearchController } from './plugin-search.controller'; import { TripWarningsController } from './trip-warnings.controller'; @@ -59,7 +60,7 @@ import { DemoModule } from '../../common/demo.module'; * dependency unresolved. */ @Module({ - imports: [PluginsRuntimeModule, AddonsModule, JourneyDomainModule, DemoModule, MikroOrmModule.forFeature([JourneyEntries, Plugins, Days, Places])], + imports: [PluginsRuntimeModule, AddonsModule, JourneyDomainModule, DemoModule, MikroOrmModule.forFeature([JourneyEntries, Plugins, Days, Places, Trips])], controllers: [ PlaceDetailsController, PluginSearchController, diff --git a/server/src/nest/plugins/contributions/plugin-routes.controller.ts b/server/src/nest/plugins/contributions/plugin-routes.controller.ts index 13b37730f..1833d51de 100644 --- a/server/src/nest/plugins/contributions/plugin-routes.controller.ts +++ b/server/src/nest/plugins/contributions/plugin-routes.controller.ts @@ -1,7 +1,8 @@ import { Body, Controller, Param, Post, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { DatabaseService } from '../../database/database.service'; +import { Trips } from '../../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { Plugins } from '../../../db/entities/Plugins.entity'; import type { PluginsRepository } from '../../../db/repositories/Plugins.repository'; import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; @@ -35,7 +36,7 @@ export type { PluginRouteOut } from './plugin-route-normalize'; export class PluginRoutesController { constructor( private readonly hooks: PluginHooks, - private readonly dbs: DatabaseService, + @InjectRepository(Trips) private readonly trips: TripsRepository, @InjectRepository(Plugins) private readonly plugins: PluginsRepository, ) {} @@ -49,7 +50,7 @@ export class PluginRoutesController { if (!pluginsEnabled()) return { route: null }; const userId = req.user?.id; const tripId = Number(body?.tripId); - if (userId == null || !Number.isFinite(tripId) || !(await this.dbs.canAccessTrip(tripId, userId))) return { route: null }; + if (userId == null || !Number.isFinite(tripId) || !(await this.trips.findAccessible(tripId, userId))) return { route: null }; if (!PROFILE_RE.test(profileId)) return { route: null }; const waypoints = readWaypoints(body?.waypoints); if (!waypoints) return { route: null }; diff --git a/server/src/nest/plugins/contributions/trip-card-contributions.controller.ts b/server/src/nest/plugins/contributions/trip-card-contributions.controller.ts index 060957bfd..175866fcf 100644 --- a/server/src/nest/plugins/contributions/trip-card-contributions.controller.ts +++ b/server/src/nest/plugins/contributions/trip-card-contributions.controller.ts @@ -1,6 +1,8 @@ import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { DatabaseService } from '../../database/database.service'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { Trips } from '../../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; @@ -84,7 +86,7 @@ function normalize(pluginId: string, raw: unknown, allowed: Set): TripCa export class TripCardContributionsController { constructor( private readonly hooks: PluginHooks, - private readonly dbs: DatabaseService, + @InjectRepository(Trips) private readonly trips: TripsRepository, ) {} @Get() @@ -114,7 +116,7 @@ export class TripCardContributionsController { // produces (the filtered `uniqueRequested` order) is load-bearing. const accessible: number[] = []; for (const id of uniqueRequested) { - if (await this.dbs.canAccessTrip(id, userId)) accessible.push(id); + if (await this.trips.findAccessible(id, userId)) accessible.push(id); } if (accessible.length === 0) return { contributions: [] }; const allowed = new Set(accessible); diff --git a/server/src/nest/plugins/contributions/trip-warnings.controller.ts b/server/src/nest/plugins/contributions/trip-warnings.controller.ts index 99cef1439..8c1b12673 100644 --- a/server/src/nest/plugins/contributions/trip-warnings.controller.ts +++ b/server/src/nest/plugins/contributions/trip-warnings.controller.ts @@ -1,6 +1,8 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { DatabaseService } from '../../database/database.service'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { Trips } from '../../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; @@ -29,7 +31,7 @@ const MESSAGE_MAX = 300; export class TripWarningsController { constructor( private readonly hooks: PluginHooks, - private readonly dbs: DatabaseService, + @InjectRepository(Trips) private readonly trips: TripsRepository, ) {} @Get(':tripId') @@ -40,7 +42,7 @@ export class TripWarningsController { if (!pluginsEnabled()) return { warnings: [] }; const tripId = Number(tripIdRaw); const userId = req.user?.id; - if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { warnings: [] }; + if (!Number.isFinite(tripId) || userId == null || !(await this.trips.findAccessible(tripId, userId))) return { warnings: [] }; const ids = this.hooks.providersOf('warningProvider'); const perProvider = await Promise.all( diff --git a/server/src/nest/plugins/contributions/trip-warnings.mcp.ts b/server/src/nest/plugins/contributions/trip-warnings.mcp.ts index fd33b41b3..651362da6 100644 --- a/server/src/nest/plugins/contributions/trip-warnings.mcp.ts +++ b/server/src/nest/plugins/contributions/trip-warnings.mcp.ts @@ -1,7 +1,9 @@ import { z } from 'zod'; import { McpController, Tool, TOOL_ANNOTATIONS_READONLY, ok, type McpContext, type McpTextResult } from '../../../nest-mcp'; import { noAccess } from '../../../mcp/tools/_shared'; -import { DatabaseService } from '../../database/database.service'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { Trips } from '../../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; import { stripEmoji } from '../text-sanitize'; @@ -40,7 +42,7 @@ const MESSAGE_MAX = 300; export class TripWarningsMcp { constructor( private readonly hooks: PluginHooks, - private readonly dbs: DatabaseService, + @InjectRepository(Trips) private readonly trips: TripsRepository, ) {} @Tool({ @@ -55,7 +57,7 @@ export class TripWarningsMcp { async getTripWarnings({ tripId }: { tripId: number }, ctx: McpContext): Promise { // Access first, so the answer to "may I look at this trip" does not depend on // whether an admin has the plugin system switched on. - if (!(await this.dbs.canAccessTrip(tripId, ctx.userId))) return noAccess(); + if (!(await this.trips.findAccessible(tripId, ctx.userId))) return noAccess(); if (!pluginsEnabled()) return ok({ warnings: [] }); const ids = this.hooks.providersOf('warningProvider'); diff --git a/server/src/nest/plugins/contributions/view-contributions.controller.ts b/server/src/nest/plugins/contributions/view-contributions.controller.ts index 80de4f770..433719693 100644 --- a/server/src/nest/plugins/contributions/view-contributions.controller.ts +++ b/server/src/nest/plugins/contributions/view-contributions.controller.ts @@ -1,6 +1,8 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { DatabaseService } from '../../database/database.service'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { Trips } from '../../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; @@ -104,7 +106,7 @@ function normalize(pluginId: string, raw: unknown): Contribution[] { export class ViewContributionsController { constructor( private readonly hooks: PluginHooks, - private readonly dbs: DatabaseService, + @InjectRepository(Trips) private readonly trips: TripsRepository, ) {} @Get(':view/:tripId') @@ -116,7 +118,7 @@ export class ViewContributionsController { if (!pluginsEnabled() || !VIEWS.has(view)) return { contributions: [] }; const tripId = Number(tripIdRaw); const userId = req.user?.id; - if (!Number.isFinite(tripId) || userId == null || !(await this.dbs.canAccessTrip(tripId, userId))) return { contributions: [] }; + if (!Number.isFinite(tripId) || userId == null || !(await this.trips.findAccessible(tripId, userId))) return { contributions: [] }; const ids = this.hooks.providersOf('tableContributor'); const perProvider = await Promise.all( diff --git a/server/src/nest/plugins/host/plugin-guards.module.ts b/server/src/nest/plugins/host/plugin-guards.module.ts index ae17808fd..450cb6a19 100644 --- a/server/src/nest/plugins/host/plugin-guards.module.ts +++ b/server/src/nest/plugins/host/plugin-guards.module.ts @@ -3,6 +3,7 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { PermissionsModule } from '../../permissions/permissions.module'; import { AddonsModule } from '../../addons/addons.module'; import { Users } from '../../../db/entities/Users.entity'; +import { Trips } from '../../../db/entities/Trips.entity'; import { PluginGuards } from './plugin-guards.service'; /** @@ -23,7 +24,7 @@ import { PluginGuards } from './plugin-guards.service'; * AuthModule <-> NotificationsModule. */ @Module({ - imports: [PermissionsModule, AddonsModule, MikroOrmModule.forFeature([Users])], + imports: [PermissionsModule, AddonsModule, MikroOrmModule.forFeature([Users, Trips])], providers: [PluginGuards], exports: [PluginGuards], }) diff --git a/server/src/nest/plugins/host/plugin-guards.service.ts b/server/src/nest/plugins/host/plugin-guards.service.ts index 26eca1bb7..edb095646 100644 --- a/server/src/nest/plugins/host/plugin-guards.service.ts +++ b/server/src/nest/plugins/host/plugin-guards.service.ts @@ -1,10 +1,11 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { DatabaseService } from '../../database/database.service'; import { PermissionsService } from '../../permissions/permissions.service'; import { AddonsService } from '../../addons/addons.service'; import { Users } from '../../../db/entities/Users.entity'; import type { UsersRepository } from '../../../db/repositories/Users.repository'; +import { Trips } from '../../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { BadParams, ForbiddenResource } from './rpc-errors'; import { num } from './rpc-params'; import type { PluginRpcContext } from './rpc-kit/types'; @@ -23,7 +24,10 @@ import type { PluginRpcContext } from './rpc-kit/types'; @Injectable() export class PluginGuards { constructor( - private readonly db: DatabaseService, + // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: + // this injects TripsRepository directly (same constructor slot) and + // calls findAccessible. + @InjectRepository(Trips) private readonly trips: TripsRepository, private readonly permissions: PermissionsService, private readonly addons: AddonsService, @InjectRepository(Users) private readonly users: UsersRepository, @@ -40,7 +44,7 @@ export class PluginGuards { if (ctx.actingUserId === undefined) { throw new ForbiddenResource('trip reads require an authenticated user context'); } - if (!(await this.db.canAccessTrip(tripId, ctx.actingUserId))) { + if (!(await this.trips.findAccessible(tripId, ctx.actingUserId))) { throw new ForbiddenResource(`no access to trip ${tripId}`); } // The read runs only for a bound, membership-checked user, so hand the id through @@ -61,7 +65,7 @@ export class PluginGuards { /** A write is allowed only if the acting user can access AND edit the trip. */ async requireTripEdit(tripId: number, userId: number, action: string): Promise { - if (!(await this.db.canAccessTrip(tripId, userId))) throw new ForbiddenResource(`no access to trip ${tripId}`); + if (!(await this.trips.findAccessible(tripId, userId))) throw new ForbiddenResource(`no access to trip ${tripId}`); if (!(await this.canEditAs(action, tripId, userId))) throw new ForbiddenResource(`no permission to edit trip ${tripId}`); } @@ -71,7 +75,7 @@ export class PluginGuards { * message the refusal carries. */ async canEditAs(action: string, tripId: number, userId: number): Promise { - const trip = await this.db.canAccessTrip(tripId, userId); + const trip = await this.trips.findAccessible(tripId, userId); if (!trip) return false; // UsersRepository.getRole (PG3, Plan 3j Task 1) — `SELECT role FROM users // WHERE id = ?`, converted. Its `string | null` return already folds "no diff --git a/server/src/nest/plugins/host/rpc/host-surface.rpc.ts b/server/src/nest/plugins/host/rpc/host-surface.rpc.ts index 1eaf98ee7..b8f530038 100644 --- a/server/src/nest/plugins/host/rpc/host-surface.rpc.ts +++ b/server/src/nest/plugins/host/rpc/host-surface.rpc.ts @@ -13,7 +13,6 @@ import { Trips } from '../../../../db/entities/Trips.entity'; import type { TripsRepository } from '../../../../db/repositories/Trips.repository'; import { PluginScheduledTasks } from '../../../../db/entities/PluginScheduledTasks.entity'; import type { PluginScheduledTasksRepository } from '../../../../db/repositories/PluginScheduledTasks.repository'; -import { DatabaseService } from '../../../database/database.service'; import { RealtimeService } from '../../../realtime/realtime.service'; import { NotificationsService } from '../../../notifications/notifications.service'; import { LlmConfigResolver } from '../../../llm-parse/llm-config.resolver'; @@ -45,7 +44,8 @@ const AI_TEXT_MAX = 20_000; @PluginController() export class HostSurfaceRpc { constructor( - private readonly db: DatabaseService, + // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: + // this reuses the `trips: TripsRepository` param below (findAccessible). private readonly realtime: RealtimeService, private readonly notifications: NotificationsService, private readonly llmConfig: LlmConfigResolver, @@ -82,7 +82,7 @@ export class HostSurfaceRpc { // Namespacing the event type alone does not cross the membership boundary. const tripId = num(params.tripId, 'tripId'); if (ctx.actingUserId === undefined) throw new ForbiddenResource('broadcasts require an authenticated user context'); - if (!(await this.db.canAccessTrip(tripId, ctx.actingUserId))) throw new ForbiddenResource(`no access to trip ${tripId}`); + if (!(await this.trips.findAccessible(tripId, ctx.actingUserId))) throw new ForbiddenResource(`no access to trip ${tripId}`); // The host forces the plugin:{id}:{event} namespace, so a plugin cannot forge a // core event. this.realtime.broadcast(tripId, `plugin:${ctx.pluginId}:${str(params.event, 'event')}`, asPayload(params.data)); @@ -121,7 +121,7 @@ export class HostSurfaceRpc { if (scope !== 'user' && scope !== 'trip') throw new BadParams("scope must be 'user' or 'trip'"); const targetId = num(input.targetId, 'targetId'); if (scope === 'user' && targetId !== actor) throw new ForbiddenResource('a plugin may only notify the acting user'); - if (scope === 'trip' && !(await this.db.canAccessTrip(targetId, actor))) { + if (scope === 'trip' && !(await this.trips.findAccessible(targetId, actor))) { throw new ForbiddenResource('the acting user is not a member of that trip'); } const link = this.safeLink(input.link); diff --git a/server/src/nest/plugins/host/rpc/meta.rpc.ts b/server/src/nest/plugins/host/rpc/meta.rpc.ts index 7d8bb3cf7..164527cff 100644 --- a/server/src/nest/plugins/host/rpc/meta.rpc.ts +++ b/server/src/nest/plugins/host/rpc/meta.rpc.ts @@ -4,7 +4,6 @@ import { PluginGuards } from '../plugin-guards.service'; import { BadParams, ForbiddenResource } from '../rpc-errors'; import { num, str } from '../rpc-params'; import type { PluginRpcContext } from '../rpc-kit/types'; -import { DatabaseService } from '../../../database/database.service'; import { PluginEntityMetadata } from '../../../../db/entities/PluginEntityMetadata.entity'; import type { PluginEntityMetadataRepository } from '../../../../db/repositories/PluginEntityMetadata.repository'; import { Trips } from '../../../../db/entities/Trips.entity'; @@ -49,7 +48,8 @@ const EDIT_ACTION: Record = { @PluginController() export class MetaRpc { constructor( - private readonly db: DatabaseService, + // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: + // this reuses the `trips: TripsRepository` param below (findAccessible). private readonly guards: PluginGuards, @InjectRepository(PluginEntityMetadata) private readonly meta: PluginEntityMetadataRepository, @InjectRepository(Trips) private readonly trips: TripsRepository, @@ -124,7 +124,7 @@ export class MetaRpc { const entityId = num(params.entityId, 'entityId'); if (ctx.actingUserId === undefined) throw new ForbiddenResource('metadata requires an authenticated user context'); const tripId = await this.entityTrip(entityType, entityId); - if (tripId === undefined || !(await this.db.canAccessTrip(tripId, ctx.actingUserId))) { + if (tripId === undefined || !(await this.trips.findAccessible(tripId, ctx.actingUserId))) { throw new ForbiddenResource(`no access to ${entityType} ${entityId}`); } if (write && !(await this.guards.canEditAs(EDIT_ACTION[entityType], tripId, ctx.actingUserId))) { diff --git a/server/src/nest/reservation-import/reservation-import.mcp.ts b/server/src/nest/reservation-import/reservation-import.mcp.ts index 9712556fe..c49f7b7c4 100644 --- a/server/src/nest/reservation-import/reservation-import.mcp.ts +++ b/server/src/nest/reservation-import/reservation-import.mcp.ts @@ -9,7 +9,9 @@ import { ADDON_IDS } from '../../addons'; import { addonGate } from '../addons/addon-gate'; import { AddonsService } from '../addons/addons.service'; import { AuthService } from '../auth/auth.service'; -import { DatabaseService } from '../database/database.service'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { Trips } from '../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { McpToolGuardsService } from '../mcp-shared/mcp-tool-guards.service'; import { noAccess, permissionDenied } from '../../mcp/tools/_shared'; import { AirtrailImportService } from '../integrations/airtrail-import.service'; @@ -42,7 +44,10 @@ const MAX_MCP_AIRTRAIL_FLIGHTS = 50; export class ReservationImportMcp { constructor( private readonly airtrailImport: AirtrailImportService, - private readonly db: DatabaseService, + // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: + // this injects TripsRepository directly (same constructor slot) and + // calls findAccessible. + @InjectRepository(Trips) private readonly trips: TripsRepository, private readonly auth: AuthService, private readonly guards: McpToolGuardsService, readonly addons: AddonsService, @@ -70,7 +75,7 @@ export class ReservationImportMcp { ctx: McpContext, ) { if (await this.auth.isDemoUser(ctx.userId)) return demoDenied(); - if (!(await this.db.canAccessTrip(tripId, ctx.userId))) return noAccess(); + if (!(await this.trips.findAccessible(tripId, ctx.userId))) return noAccess(); if (!(await this.guards.hasTripPermission('reservation_edit', tripId, ctx.userId))) return permissionDenied(); if (flightIds.length > MAX_MCP_AIRTRAIL_FLIGHTS) { diff --git a/server/src/nest/reservation-import/reservation-import.module.ts b/server/src/nest/reservation-import/reservation-import.module.ts index 2fc52d1d3..cbb57ad38 100644 --- a/server/src/nest/reservation-import/reservation-import.module.ts +++ b/server/src/nest/reservation-import/reservation-import.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; import { ReservationImportController } from './reservation-import.controller'; import { ReservationImportMcp } from './reservation-import.mcp'; import { BookingImportModule } from '../booking-import/booking-import.module'; @@ -7,6 +8,7 @@ import { AddonsModule } from '../addons/addons.module'; import { PermissionsModule } from '../permissions/permissions.module'; import { AuthModule } from '../auth/auth.module'; import { McpSharedModule } from '../mcp-shared/mcp-shared.module'; +import { Trips } from '../../db/entities/Trips.entity'; /** * The one route prefix that turns something external into reservations. @@ -21,7 +23,7 @@ import { McpSharedModule } from '../mcp-shared/mcp-shared.module'; * permission/broadcast guards; neither is @Global, so both are named here. */ @Module({ - imports: [BookingImportModule, AirtrailModule, AddonsModule, PermissionsModule, AuthModule, McpSharedModule], + imports: [BookingImportModule, AirtrailModule, AddonsModule, PermissionsModule, AuthModule, McpSharedModule, MikroOrmModule.forFeature([Trips])], controllers: [ReservationImportController], providers: [ReservationImportMcp], }) diff --git a/server/src/nest/transit/transit.mcp.ts b/server/src/nest/transit/transit.mcp.ts index 215e5d06c..09eed85e0 100644 --- a/server/src/nest/transit/transit.mcp.ts +++ b/server/src/nest/transit/transit.mcp.ts @@ -16,7 +16,9 @@ import { } from './transit-itinerary.helpers'; import { noAccess, permissionDenied } from '../../mcp/tools/_shared'; import { RateLimitService } from '../common/rate-limit.service'; -import { DatabaseService } from '../database/database.service'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { Trips } from '../../db/entities/Trips.entity'; +import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { DaysService } from '../days/days.service'; import { ReservationsService } from '../reservations/reservations.service'; import { SCHEDULED_TRANSIT_MODES, type TransitItinerary } from './transit.helpers'; @@ -63,7 +65,10 @@ export class TransitMcp { private readonly transit: TransitService, private readonly days: DaysService, private readonly reservations: ReservationsService, - private readonly db: DatabaseService, + // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: + // this injects TripsRepository directly (same constructor slot) and + // calls findAccessible. + @InjectRepository(Trips) private readonly trips: TripsRepository, private readonly auth: AuthService, private readonly guards: McpToolGuardsService, ) {} @@ -184,7 +189,7 @@ export class TransitMcp { ctx: McpContext, ) { if (await this.auth.isDemoUser(ctx.userId)) return demoDenied(); - if (!(await this.db.canAccessTrip(tripId, ctx.userId))) return noAccess(); + if (!(await this.trips.findAccessible(tripId, ctx.userId))) return noAccess(); if (!(await this.guards.hasTripPermission('reservation_edit', tripId, ctx.userId))) return permissionDenied(); const day = await this.days.getDay(dayId, tripId); if (!day) { diff --git a/server/src/nest/transit/transit.module.ts b/server/src/nest/transit/transit.module.ts index ac06114cb..94aaf0835 100644 --- a/server/src/nest/transit/transit.module.ts +++ b/server/src/nest/transit/transit.module.ts @@ -11,6 +11,7 @@ import { AuthModule } from '../auth/auth.module'; import { McpSharedModule } from '../mcp-shared/mcp-shared.module'; import { AppSettings } from '../../db/entities/AppSettings.entity'; import { Users } from '../../db/entities/Users.entity'; +import { Trips } from '../../db/entities/Trips.entity'; /** * Transit domain (#1065) — the Transitous/MOTIS proxy, with the optional @@ -18,18 +19,17 @@ import { Users } from '../../db/entities/Users.entity'; * decorator-registered MCP tools; DaysModule/ReservationsModule feed * create_transit_journey. Exports TransitService for in-container consumers. * - * MikroOrmModule.forFeature([AppSettings, Users]): GoogleTransitProvider + * MikroOrmModule.forFeature([AppSettings, Users, Trips]): GoogleTransitProvider * passes its own AppSettingsRepository/UsersRepository to * instance-api-keys.ts's resolveApiKey (Plan 3a Task 5) and, since Plan 4 * Task 1, to `transit-provider.ts`'s `readTransitProvider`/ * `writeTransitProvider` too — the SAME `AppSettingsRepository` this module - * already registers. `transit.mcp.ts`'s remaining `DatabaseService` - * injection is unrelated: it is the `canAccessTrip` delegate (Plan 4 - * Task 2/3's facade-inline sweep, not this plan's). + * already registers. `transit.mcp.ts`'s own `canAccessTrip` delegate is now + * `TripsRepository.findAccessible` directly (Plan 4 Task 2). */ @Module({ // DaysModule + ReservationsModule: TransitMcp's create_transit_journey injects both. - imports: [McpSharedModule, RateLimitModule, DaysModule, ReservationsModule, AuthModule, MikroOrmModule.forFeature([AppSettings, Users])], + imports: [McpSharedModule, RateLimitModule, DaysModule, ReservationsModule, AuthModule, MikroOrmModule.forFeature([AppSettings, Users, Trips])], controllers: [TransitController], providers: [TransitService, TransitMcp, GoogleTransitProvider], exports: [TransitService], diff --git a/server/tests/helpers/mcp-test-controllers.ts b/server/tests/helpers/mcp-test-controllers.ts index a99045b59..9915b78a0 100644 --- a/server/tests/helpers/mcp-test-controllers.ts +++ b/server/tests/helpers/mcp-test-controllers.ts @@ -231,7 +231,6 @@ export async function createMcpTestRegistry(): Promise { ); const queryHelpersService = new QueryHelpersService(await createTestTagsRepo(dbService.connection), await createTestPlaceRatingsRepo(dbService.connection), await createTestAssignmentParticipantsRepo(dbService.connection)); const daysService = new DaysService( - dbService, permissionsService, realtimeService, queryHelpersService, @@ -244,7 +243,7 @@ export async function createMcpTestRegistry(): Promise { await createTestReservationEndpointsRepo(dbService.connection), await createTestDayAccommodationsRepo(dbService.connection), ); - const todoService = new TodoService(dbService, permissionsService, realtimeService, await createTestUnitOfWork(dbService.connection), await createTestTodoItemsRepo(dbService.connection), await createTestTodoCategoryAssigneesRepo(dbService.connection)); + const todoService = new TodoService(dbService, permissionsService, realtimeService, await createTestUnitOfWork(dbService.connection), await createTestTodoItemsRepo(dbService.connection), await createTestTodoCategoryAssigneesRepo(dbService.connection), await createTestTripsRepo(dbService.connection)); const packingService = new PackingService( dbService, permissionsService, realtimeService, notificationsStub(), await createTestUnitOfWork(dbService.connection), await createTestPackingItemsRepo(dbService.connection), await createTestPackingItemContributorsRepo(dbService.connection), await createTestPackingBagsRepo(dbService.connection), @@ -252,7 +251,7 @@ export async function createMcpTestRegistry(): Promise { await createTestPackingTemplateItemsRepo(dbService.connection), await createTestTripsRepo(dbService.connection), ); const collabService = new CollabService( - dbService, permissionsService, realtimeService, notificationsStub(), generalStorage, new RateLimitService(), await createTestUnitOfWork(dbService.connection), + permissionsService, realtimeService, notificationsStub(), generalStorage, new RateLimitService(), await createTestUnitOfWork(dbService.connection), await createTestCollabMessageReactionsRepo(dbService.connection), await createTestCollabNotesRepo(dbService.connection), await createTestCollabPollsRepo(dbService.connection), await createTestCollabPollVotesRepo(dbService.connection), await createTestCollabLinksRepo(dbService.connection), await createTestCollabMessagesRepo(dbService.connection), await createTestTripsRepo(dbService.connection), @@ -284,7 +283,7 @@ export async function createMcpTestRegistry(): Promise { // One instance, four consumers: AssignmentsMcp, ReservationsMcp, PlacesMcp and // AccommodationsService, which writes the day stop a booked night implies. const assignmentsService = new AssignmentsService( - dbService, permissionsService, realtimeService, queryHelpersService, journeyDomain, await createTestUnitOfWork(dbService.connection), + await createTestTripsRepo(dbService.connection), permissionsService, realtimeService, queryHelpersService, journeyDomain, await createTestUnitOfWork(dbService.connection), await createTestDayAssignmentsRepo(dbService.connection), await createTestAssignmentParticipantsRepo(dbService.connection), await createTestDaysRepo(dbService.connection), @@ -294,6 +293,7 @@ export async function createMcpTestRegistry(): Promise { ); const accommodationsService = new AccommodationsService( dbService, permissionsService, realtimeService, assignmentsService, await createTestUnitOfWork(dbService.connection), + await createTestTripsRepo(dbService.connection), await createTestDayAccommodationsRepo(dbService.connection), await createTestDayAssignmentsRepo(dbService.connection), await createTestPlacesRepo(dbService.connection), @@ -321,8 +321,8 @@ export async function createMcpTestRegistry(): Promise { await createTestCollectionPlacesRepo(dbService.connection), ); // Built after it: a hotel booking writes the stay's day stop through this one. - const reservationsService = new ReservationsService(dbService, permissionsService, budgetService, realtimeService, notificationsStub(), new ReservationsReadService(await createTestReservationsRepo(dbService.connection), await createTestReservationEndpointsRepo(dbService.connection), await createTestReservationTravelersRepo(dbService.connection)), accommodationsService, await createTestUnitOfWork(dbService.connection), await createTestReservationsRepo(dbService.connection), await createTestReservationEndpointsRepo(dbService.connection), await createTestReservationTravelersRepo(dbService.connection), await createTestReservationDayPositionsRepo(dbService.connection), await createTestDayAccommodationsRepo(dbService.connection), await createTestDaysRepo(dbService.connection), await createTestPlacesRepo(dbService.connection), await createTestDayAssignmentsRepo(dbService.connection), await createTestTripMembersRepo(dbService.connection), await createTestUsersRepo(dbService.connection), await createTestTripsRepo(dbService.connection), await createTestBudgetItemsRepo(dbService.connection)); - const membersService = new TripMembersService(dbService, budgetService, new UserCleanupService(dbService, budgetService, await createTestUnitOfWork(dbService.connection), usersRepo, await createTestTripMembersRepo(dbService.connection), await createTestBudgetItemsRepo(dbService.connection), await createTestJourneyShareTokensRepo(dbService.connection), await createTestJourneysRepo(dbService.connection), await createTestJourneyEntriesRepo(dbService.connection), await createTestJourneyContributorsRepo(dbService.connection), await createTestShareTokensRepo(dbService.connection)), permissionsService, realtimeService, notificationsStub(), await createTestUnitOfWork(dbService.connection), await createTestTripsRepo(dbService.connection), await createTestTripMembersRepo(dbService.connection), usersRepo); + const reservationsService = new ReservationsService(permissionsService, budgetService, realtimeService, notificationsStub(), new ReservationsReadService(await createTestReservationsRepo(dbService.connection), await createTestReservationEndpointsRepo(dbService.connection), await createTestReservationTravelersRepo(dbService.connection)), accommodationsService, await createTestUnitOfWork(dbService.connection), await createTestReservationsRepo(dbService.connection), await createTestReservationEndpointsRepo(dbService.connection), await createTestReservationTravelersRepo(dbService.connection), await createTestReservationDayPositionsRepo(dbService.connection), await createTestDayAccommodationsRepo(dbService.connection), await createTestDaysRepo(dbService.connection), await createTestPlacesRepo(dbService.connection), await createTestDayAssignmentsRepo(dbService.connection), await createTestTripMembersRepo(dbService.connection), await createTestUsersRepo(dbService.connection), await createTestTripsRepo(dbService.connection), await createTestBudgetItemsRepo(dbService.connection)); + const membersService = new TripMembersService(budgetService, new UserCleanupService(dbService, budgetService, await createTestUnitOfWork(dbService.connection), usersRepo, await createTestTripMembersRepo(dbService.connection), await createTestBudgetItemsRepo(dbService.connection), await createTestJourneyShareTokensRepo(dbService.connection), await createTestJourneysRepo(dbService.connection), await createTestJourneyEntriesRepo(dbService.connection), await createTestJourneyContributorsRepo(dbService.connection), await createTestShareTokensRepo(dbService.connection)), permissionsService, realtimeService, notificationsStub(), await createTestUnitOfWork(dbService.connection), await createTestTripsRepo(dbService.connection), await createTestTripMembersRepo(dbService.connection), usersRepo); const tripsService = new TripsService( dbService, reservationsService, @@ -349,7 +349,7 @@ export async function createMcpTestRegistry(): Promise { // once and shared by both consumers below (readModelService, FilesMcp) // rather than reconstructed twice with the same seven-repository tail. const filesService = new FilesService( - dbService, permissionsService, realtimeService, new EphemeralTokenService(), generalStorage, mcpOrm.em, + await createTestTripsRepo(dbService.connection), permissionsService, realtimeService, new EphemeralTokenService(), generalStorage, mcpOrm.em, await createTestUnitOfWork(dbService.connection), await createTestTripFilesRepo(dbService.connection), await createTestFileLinksRepo(dbService.connection), @@ -379,7 +379,7 @@ export async function createMcpTestRegistry(): Promise { // stubbed: an empty provider registry would make the backfill answer "unknown // provider" for every id and hide a wiring mistake behind a caught error. const memoriesAccess = new MemoriesAccessService( - dbService, mcpOrm.repo(TripPhotos), mcpOrm.repo(TrekPhotos), mcpOrm.repo(TripAlbumLinks), mcpOrm.repo(Trips), + mcpOrm.repo(TripPhotos), mcpOrm.repo(TrekPhotos), mcpOrm.repo(TripAlbumLinks), mcpOrm.repo(Trips), // Plan 3g Task 4 constructor-ripple: MA1/MA2/MA6's journey half. await createTestJourneysRepo(dbService.connection), await createTestJourneyContributorsRepo(dbService.connection), await createTestJourneyPhotosRepo(dbService.connection), ); @@ -399,7 +399,7 @@ export async function createMcpTestRegistry(): Promise { new PackingMcp(packingService, authService, addonsService, guards), new BudgetMcp(budgetService, exchangeRatesService, dbService, new RuntimeEnvService(), new TripMembershipService(await createTestTripsRepo(dbService.connection), await createTestTripMembersRepo(dbService.connection)), addonsService, guards, await createTestUnitOfWork(dbService.connection), await createTestPlacesRepo(dbService.connection), await createTestTripsRepo(dbService.connection), demoService), new ReservationsMcp(reservationsService, daysService, budgetService, authService, assignmentsService, guards), - new DayNotesMcp(new DayNotesService(dbService, permissionsService, realtimeService, await createTestDayNotesRepo(dbService.connection), await createTestDaysRepo(dbService.connection)), authService, guards), + new DayNotesMcp(new DayNotesService(await createTestTripsRepo(dbService.connection), permissionsService, realtimeService, await createTestDayNotesRepo(dbService.connection), await createTestDaysRepo(dbService.connection)), authService, guards), new DaysMcp(daysService, authService, guards), new RoadtripMcp( new RoadtripService( @@ -441,8 +441,8 @@ export async function createMcpTestRegistry(): Promise { new TripsMcp(tripsService, todoService, collabService, authService, calendarService, membersService, readModelService, addonsService, guards), new TripPromptsMcp(tripsService, readModelService, packingService, addonsService), new ShareMcp(new ShareService(new SettingsService(await createTestUnitOfWork(dbService.connection), appSettings, await createTestSettingsRepo(dbService.connection)), permissionsService, queryHelpersService, placePhotoCache, await createTestUnitOfWork(dbService.connection), ...(await shareServiceRepoArgs(dbService.connection))), authService, guards), - new FeedsMcp(new FeedsService(await createTestTripsRepo(dbService.connection), usersRepo, calendarService), dbService, new RuntimeEnvService(), guards, demoService), - new TripInviteMcp(new TripInviteService(dbService, permissionsService, new TripMembershipService(await createTestTripsRepo(dbService.connection), await createTestTripMembersRepo(dbService.connection)), await createTestUnitOfWork(dbService.connection), await createTestTripInviteTokensRepo(dbService.connection)), dbService, new RuntimeEnvService(), guards, new AuditService(auditLogRepo, usersRepo), demoService), + new FeedsMcp(new FeedsService(await createTestTripsRepo(dbService.connection), usersRepo, calendarService), await createTestTripsRepo(dbService.connection), new RuntimeEnvService(), guards, demoService), + new TripInviteMcp(new TripInviteService(await createTestTripsRepo(dbService.connection), permissionsService, new TripMembershipService(await createTestTripsRepo(dbService.connection), await createTestTripMembersRepo(dbService.connection)), await createTestUnitOfWork(dbService.connection), await createTestTripInviteTokensRepo(dbService.connection)), dbService, new RuntimeEnvService(), guards, new AuditService(auditLogRepo, usersRepo), demoService), new MapsMcp(mapsService), new PlacesMcp(placesService, mapsService, await createTestTripsRepo(dbService.connection), authService, journeyDomain, assignmentsService, guards, await createTestUnitOfWork(dbService.connection)), new CollectionsMcp( @@ -468,7 +468,7 @@ export async function createMcpTestRegistry(): Promise { // (`findUsernameEmail`, UM11's precedent) added in its place. usersRepo, authService, addonsService, ), - new TransitMcp(new TransitService(new GoogleTransitProvider(appSettings, usersRepo)), daysService, reservationsService, dbService, authService, guards), + new TransitMcp(new TransitService(new GoogleTransitProvider(appSettings, usersRepo)), daysService, reservationsService, await createTestTripsRepo(dbService.connection), authService, guards), new AtlasMcp(new AtlasService( await createTestBucketListRepo(dbService.connection), await createTestHiddenCountriesRepo(dbService.connection), await createTestHiddenRegionsRepo(dbService.connection), await createTestVisitedCountriesRepo(dbService.connection), @@ -491,10 +491,10 @@ export async function createMcpTestRegistry(): Promise { new ReservationImportMcp(new AirtrailImportService( await createTestReservationsRepo(dbService.connection), await createTestReservationEndpointsRepo(dbService.connection), await createTestDaysRepo(dbService.connection), realtimeService, reservationsService, new AirtrailClient(), new AirtrailService(usersRepo, new AuditService(auditLogRepo, usersRepo), new AirtrailClient()), - ), dbService, authService, guards, addonsService), + ), await createTestTripsRepo(dbService.connection), authService, guards, addonsService), new SettingsMcp(new SettingsService(await createTestUnitOfWork(dbService.connection), appSettings, await createTestSettingsRepo(dbService.connection)), authService), new HelpMcp(), new AddonsMcp(addonsService), - new TripWarningsMcp(new PluginHooks({ providersOf: () => [], invokeHook: async () => [] } as unknown as PluginRuntimeService), dbService), + new TripWarningsMcp(new PluginHooks({ providersOf: () => [], invokeHook: async () => [] } as unknown as PluginRuntimeService), await createTestTripsRepo(dbService.connection)), new PluginSearchMcp(new PluginHooks({ providersOf: () => [], invokeHook: async () => [] } as unknown as PluginRuntimeService)), ], { accessPolicy: trekMcpAccessPolicy, validateAccess: trekMcpValidateAccess }, diff --git a/server/tests/helpers/plugin-host.ts b/server/tests/helpers/plugin-host.ts index 32b3a380a..4e4f4ede1 100644 --- a/server/tests/helpers/plugin-host.ts +++ b/server/tests/helpers/plugin-host.ts @@ -161,7 +161,7 @@ export async function createPluginRpcHostFactory(dbs: DatabaseService): Promise< const budget = new BudgetService(dbs, permissions, exchangeRates, realtime, await createTestUnitOfWork(dbs.connection), ...(await budgetRepoArgs(dbs.connection))); const addons = await createTestAddonsService(dbs.connection, dbs); const queryHelpers = new QueryHelpersService(await createTestTagsRepo(dbs.connection), await createTestPlaceRatingsRepo(dbs.connection), await createTestAssignmentParticipantsRepo(dbs.connection)); - const todos = new TodoService(dbs, permissions, realtime, await createTestUnitOfWork(dbs.connection), await createTestTodoItemsRepo(dbs.connection), await createTestTodoCategoryAssigneesRepo(dbs.connection)); + const todos = new TodoService(dbs, permissions, realtime, await createTestUnitOfWork(dbs.connection), await createTestTodoItemsRepo(dbs.connection), await createTestTodoCategoryAssigneesRepo(dbs.connection), await createTestTripsRepo(dbs.connection)); const packing = new PackingService( dbs, permissions, realtime, notificationsStub(), await createTestUnitOfWork(dbs.connection), await createTestPackingItemsRepo(dbs.connection), await createTestPackingItemContributorsRepo(dbs.connection), await createTestPackingBagsRepo(dbs.connection), @@ -171,7 +171,7 @@ export async function createPluginRpcHostFactory(dbs: DatabaseService): Promise< // Plan 3e Task 1 (files): FilesService now also takes uow + the repositories // its R2 transactions and R12 cross-object trip-scoping guard need. const files = new FilesService( - dbs, permissions, realtime, new EphemeralTokenService(), generalStorage, (await sharedTestOrm(dbs.connection)).em, + await createTestTripsRepo(dbs.connection), permissions, realtime, new EphemeralTokenService(), generalStorage, (await sharedTestOrm(dbs.connection)).em, await createTestUnitOfWork(dbs.connection), await createTestTripFilesRepo(dbs.connection), await createTestFileLinksRepo(dbs.connection), @@ -181,7 +181,7 @@ export async function createPluginRpcHostFactory(dbs: DatabaseService): Promise< await createTestBudgetItemsRepo(dbs.connection), ); const collab = new CollabService( - dbs, permissions, realtime, notificationsStub(), generalStorage, new RateLimitService(), await createTestUnitOfWork(dbs.connection), + permissions, realtime, notificationsStub(), generalStorage, new RateLimitService(), await createTestUnitOfWork(dbs.connection), await createTestCollabMessageReactionsRepo(dbs.connection), await createTestCollabNotesRepo(dbs.connection), await createTestCollabPollsRepo(dbs.connection), await createTestCollabPollVotesRepo(dbs.connection), await createTestCollabLinksRepo(dbs.connection), await createTestCollabMessagesRepo(dbs.connection), await createTestTripsRepo(dbs.connection), @@ -196,7 +196,6 @@ export async function createPluginRpcHostFactory(dbs: DatabaseService): Promise< realtime, notificationsStub(), await createTestUnitOfWork(dbs.connection), ); const days = new DaysService( - dbs, permissions, realtime, queryHelpers, @@ -249,9 +248,9 @@ export async function createPluginRpcHostFactory(dbs: DatabaseService): Promise< await createTestTripsRepo(dbs.connection), await createTestPlacesRepo(dbs.connection), await createTestReservationEndpointsRepo(dbs.connection), await createTestUnitOfWork(dbs.connection), ); - const dayNotes = new DayNotesService(dbs, permissions, realtime, await createTestDayNotesRepo(dbs.connection), await createTestDaysRepo(dbs.connection)); + const dayNotes = new DayNotesService(await createTestTripsRepo(dbs.connection), permissions, realtime, await createTestDayNotesRepo(dbs.connection), await createTestDaysRepo(dbs.connection)); const assignments = new AssignmentsService( - dbs, permissions, realtime, queryHelpers, journey, await createTestUnitOfWork(dbs.connection), + await createTestTripsRepo(dbs.connection), permissions, realtime, queryHelpers, journey, await createTestUnitOfWork(dbs.connection), await createTestDayAssignmentsRepo(dbs.connection), await createTestAssignmentParticipantsRepo(dbs.connection), await createTestDaysRepo(dbs.connection), @@ -268,6 +267,7 @@ export async function createPluginRpcHostFactory(dbs: DatabaseService): Promise< const oauth = new PluginOAuthService(pluginOrm.repo(Plugins), pluginOrm.repo(PluginOauthTokens), pluginOrm.repo(PluginOauthState), pluginOrm.repo(PluginSettingsFields)); const accommodations = new AccommodationsService( dbs, permissions, realtime, assignments, await createTestUnitOfWork(dbs.connection), + await createTestTripsRepo(dbs.connection), await createTestDayAccommodationsRepo(dbs.connection), await createTestDayAssignmentsRepo(dbs.connection), await createTestPlacesRepo(dbs.connection), @@ -290,13 +290,15 @@ export async function createPluginRpcHostFactory(dbs: DatabaseService): Promise< await createTestCollectionPlacesRepo(dbs.connection), ); // After accommodations: a hotel booking writes the stay's day stop through it. - const reservations = new ReservationsService(dbs, permissions, budget, realtime, notificationsStub(), new ReservationsReadService(await createTestReservationsRepo(dbs.connection), await createTestReservationEndpointsRepo(dbs.connection), await createTestReservationTravelersRepo(dbs.connection)), accommodations, await createTestUnitOfWork(dbs.connection), await createTestReservationsRepo(dbs.connection), await createTestReservationEndpointsRepo(dbs.connection), await createTestReservationTravelersRepo(dbs.connection), await createTestReservationDayPositionsRepo(dbs.connection), await createTestDayAccommodationsRepo(dbs.connection), await createTestDaysRepo(dbs.connection), await createTestPlacesRepo(dbs.connection), await createTestDayAssignmentsRepo(dbs.connection), await createTestTripMembersRepo(dbs.connection), await createTestUsersRepo(dbs.connection), await createTestTripsRepo(dbs.connection), await createTestBudgetItemsRepo(dbs.connection)); + const reservations = new ReservationsService(permissions, budget, realtime, notificationsStub(), new ReservationsReadService(await createTestReservationsRepo(dbs.connection), await createTestReservationEndpointsRepo(dbs.connection), await createTestReservationTravelersRepo(dbs.connection)), accommodations, await createTestUnitOfWork(dbs.connection), await createTestReservationsRepo(dbs.connection), await createTestReservationEndpointsRepo(dbs.connection), await createTestReservationTravelersRepo(dbs.connection), await createTestReservationDayPositionsRepo(dbs.connection), await createTestDayAccommodationsRepo(dbs.connection), await createTestDaysRepo(dbs.connection), await createTestPlacesRepo(dbs.connection), await createTestDayAssignmentsRepo(dbs.connection), await createTestTripMembersRepo(dbs.connection), await createTestUsersRepo(dbs.connection), await createTestTripsRepo(dbs.connection), await createTestBudgetItemsRepo(dbs.connection)); const trips = new TripsService(dbs, reservations, days, permissions, budget, vacay, realtime, unsplash, generalStorage, await createTestUnitOfWork(dbs.connection), (await sharedTestOrm(dbs.connection)).em); - const members = new TripMembersService(dbs, budget, new UserCleanupService(dbs, budget, await createTestUnitOfWork(dbs.connection), usersRepo, await createTestTripMembersRepo(dbs.connection), await createTestBudgetItemsRepo(dbs.connection), await createTestJourneyShareTokensRepo(dbs.connection), await createTestJourneysRepo(dbs.connection), await createTestJourneyEntriesRepo(dbs.connection), await createTestJourneyContributorsRepo(dbs.connection), await createTestShareTokensRepo(dbs.connection)), permissions, realtime, notificationsStub(), await createTestUnitOfWork(dbs.connection), await createTestTripsRepo(dbs.connection), await createTestTripMembersRepo(dbs.connection), usersRepo); + const members = new TripMembersService(budget, new UserCleanupService(dbs, budget, await createTestUnitOfWork(dbs.connection), usersRepo, await createTestTripMembersRepo(dbs.connection), await createTestBudgetItemsRepo(dbs.connection), await createTestJourneyShareTokensRepo(dbs.connection), await createTestJourneysRepo(dbs.connection), await createTestJourneyEntriesRepo(dbs.connection), await createTestJourneyContributorsRepo(dbs.connection), await createTestShareTokensRepo(dbs.connection)), permissions, realtime, notificationsStub(), await createTestUnitOfWork(dbs.connection), await createTestTripsRepo(dbs.connection), await createTestTripMembersRepo(dbs.connection), 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. - const guards = new PluginGuards(dbs, permissions, addons, usersRepo); + // Plan 4 Task 2 — PluginGuards' own canAccessTrip delegate is now + // TripsRepository.findAccessible directly, in the same constructor slot. + const guards = new PluginGuards(await createTestTripsRepo(dbs.connection), permissions, addons, usersRepo); const registry = createTestPluginRegistry([ new TagsRpc(new TagsService(await createTestTagsRepo(dbs.connection))), @@ -312,7 +314,7 @@ export async function createPluginRpcHostFactory(dbs: DatabaseService): Promise< new AccommodationsRpc(accommodations, realtime, guards), new ItineraryRpc(assignments, realtime, guards), new TripsRpc(trips, reservations, days, membership, realtime, guards, accommodations, members, (await sharedTestOrm(dbs.connection)).em), - new CostsRpc(budget, dbs, realtime, guards, membership), + new CostsRpc(budget, await createTestTripsRepo(dbs.connection), realtime, guards, membership), new ReservationsRpc(reservations, realtime, guards), new CollabRpc(collab, realtime, guards), new AtlasRpc(atlas, guards), @@ -327,15 +329,18 @@ export async function createPluginRpcHostFactory(dbs: DatabaseService): Promise< new DbRpc(new PluginUserSettingsService(pluginOrm.repo(PluginSettingsFields), pluginOrm.repo(PluginUserConfig))), // Plan 3j Task 5 — MetaRpc's own MR1–MR9 conversion: PluginEntityMetadata plus // one typed trip-id read per entity table (R12's dispatch, no interpolation). + // Plan 4 Task 2 — MetaRpc's own DatabaseService param is gone: canAccessTrip + // now reads through the TripsRepository it already injects below. new MetaRpc( - dbs, guards, pluginOrm.repo(PluginEntityMetadata), await createTestTripsRepo(dbs.connection), + guards, pluginOrm.repo(PluginEntityMetadata), await createTestTripsRepo(dbs.connection), await createTestPlacesRepo(dbs.connection), await createTestDaysRepo(dbs.connection), await createTestReservationsRepo(dbs.connection), await createTestDayAccommodationsRepo(dbs.connection), ), // Plan 3j Task 5 — HostSurfaceRpc's own HR1/HR5–HR9 conversion: the plugin-visible // user row, the bilateral trip-sharing gate, and its own scheduler.set/cancel. + // Plan 4 Task 2 — its own DatabaseService param is gone the same way MetaRpc's is. new HostSurfaceRpc( - dbs, realtime, notifications, llmConfig, oauth, guards, pluginAuditRepo, + realtime, notifications, llmConfig, oauth, guards, pluginAuditRepo, usersRepo, await createTestTripsRepo(dbs.connection), pluginOrm.repo(PluginScheduledTasks), ), new PluginHooks(undefined as never), diff --git a/server/tests/helpers/rpc-host-deps.ts b/server/tests/helpers/rpc-host-deps.ts index 4601bca75..470c83cd8 100644 --- a/server/tests/helpers/rpc-host-deps.ts +++ b/server/tests/helpers/rpc-host-deps.ts @@ -82,9 +82,11 @@ export function allRpcControllers(): object[] { new CollectionsRpc(anyService(), anyService()), new DbRpc(anyService()), // Plan 3j Task 5 — MetaRpc/HostSurfaceRpc's own repository conversions (MR#/HR#). - new MetaRpc(anyService(), anyService(), anyService(), anyService(), anyService(), anyService(), anyService(), anyService()), + // Plan 4 Task 2 — both classes' own DatabaseService param is gone (canAccessTrip + // now reads through the TripsRepository they already inject), one fewer anyService(). + new MetaRpc(anyService(), anyService(), anyService(), anyService(), anyService(), anyService(), anyService()), new HostSurfaceRpc( - anyService(), anyService(), anyService(), anyService(), anyService(), anyService(), anyService(), + anyService(), anyService(), anyService(), anyService(), anyService(), anyService(), anyService(), anyService(), anyService(), ), // Owns no wire method, but declares all 18 hook contracts, which requireTotalCoverage checks too. diff --git a/server/tests/unit/budget/costs.rpc.test.ts b/server/tests/unit/budget/costs.rpc.test.ts index 56fc7abb1..12701c913 100644 --- a/server/tests/unit/budget/costs.rpc.test.ts +++ b/server/tests/unit/budget/costs.rpc.test.ts @@ -14,7 +14,7 @@ import { CostsRpc } from '../../../src/nest/budget/costs.rpc'; import { BudgetModule } from '../../../src/nest/budget/budget.module'; import type { BudgetService } from '../../../src/nest/budget/budget.service'; import type { RealtimeService } from '../../../src/nest/realtime/realtime.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; @@ -34,8 +34,8 @@ function build(opts: { addonOn?: boolean; canEdit?: boolean; missing?: boolean } } as unknown as BudgetService & Record>; const realtime = { broadcast: vi.fn() } as unknown as RealtimeService & { broadcast: ReturnType }; const db = { - canAccessTrip: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), - } as unknown as DatabaseService; + findAccessible: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), + } as unknown as TripsRepository; const guards = new PluginGuards( db, { checkPermission: vi.fn(() => opts.canEdit ?? true) } as unknown as PermissionsService, diff --git a/server/tests/unit/days/day-notes.rpc.test.ts b/server/tests/unit/days/day-notes.rpc.test.ts index c0d8748f8..9b47e5b89 100644 --- a/server/tests/unit/days/day-notes.rpc.test.ts +++ b/server/tests/unit/days/day-notes.rpc.test.ts @@ -13,7 +13,7 @@ import { DayNotesRpc } from '../../../src/nest/day-notes/day-notes.rpc'; import { DayNotesModule } from '../../../src/nest/day-notes/day-notes.module'; import type { DayNotesService } from '../../../src/nest/day-notes/day-notes.service'; import type { RealtimeService } from '../../../src/nest/realtime/realtime.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; @@ -36,8 +36,8 @@ function build(canEdit = true) { } as unknown as DayNotesService & Record>; const realtime = { broadcast: vi.fn() } as unknown as RealtimeService & { broadcast: ReturnType }; const db = { - canAccessTrip: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), - } as unknown as DatabaseService; + findAccessible: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), + } as unknown as TripsRepository; const guards = new PluginGuards( db, { checkPermission: vi.fn(() => canEdit) } as unknown as PermissionsService, diff --git a/server/tests/unit/days/days-itinerary.rpc.test.ts b/server/tests/unit/days/days-itinerary.rpc.test.ts index 29a45cacc..9303ed75b 100644 --- a/server/tests/unit/days/days-itinerary.rpc.test.ts +++ b/server/tests/unit/days/days-itinerary.rpc.test.ts @@ -17,7 +17,7 @@ import { AssignmentsModule } from '../../../src/nest/assignments/assignments.mod import type { DaysService } from '../../../src/nest/days/days.service'; import type { AssignmentsService } from '../../../src/nest/assignments/assignments.service'; import type { RealtimeService } from '../../../src/nest/realtime/realtime.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; @@ -45,8 +45,8 @@ function build(opts: { canEdit?: boolean } = {}) { const realtime = { broadcast: vi.fn() } as unknown as RealtimeService & { broadcast: ReturnType }; const guards = new PluginGuards( { - canAccessTrip: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), - } as unknown as DatabaseService, + findAccessible: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), + } as unknown as TripsRepository, { checkPermission: vi.fn(() => opts.canEdit ?? true) } as unknown as PermissionsService, { isAddonEnabled: vi.fn(() => true) } as unknown as AddonsService, { getRole: vi.fn(async () => 'user') } as unknown as UsersRepository, diff --git a/server/tests/unit/files/files.rpc.test.ts b/server/tests/unit/files/files.rpc.test.ts index f07431d9b..4e962f52d 100644 --- a/server/tests/unit/files/files.rpc.test.ts +++ b/server/tests/unit/files/files.rpc.test.ts @@ -19,7 +19,7 @@ import { FilesRpc } from '../../../src/nest/files/files.rpc'; import { FilesModule } from '../../../src/nest/files/files.module'; import { FilesService } from '../../../src/nest/files/files.service'; import type { RealtimeService } from '../../../src/nest/realtime/realtime.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; @@ -47,8 +47,8 @@ function build(opts: { file?: Record | undefined; foreign?: str softDeleteFile: vi.fn(), } as unknown as FilesService & Record>; const db = { - canAccessTrip: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), - } as unknown as DatabaseService; + findAccessible: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), + } as unknown as TripsRepository; // FL28 — `SELECT email FROM users WHERE id = ?`, now `UsersRepository.getEmail`. // getRole is PluginGuards' own role lookup (PG3/PG4, Plan 3j Task 1), on the // same double — both are UsersRepository methods now. @@ -291,8 +291,8 @@ describe('FilesRpc writes', () => { try { const f = build(); const db = { - canAccessTrip: vi.fn(async () => ({ id: 1, user_id: 42 })), - } as unknown as DatabaseService; + findAccessible: vi.fn(async () => ({ id: 1, user_id: 42 })), + } as unknown as TripsRepository; const guards = new PluginGuards( db, { checkPermission: vi.fn(() => true) } as unknown as PermissionsService, diff --git a/server/tests/unit/packing/packing.rpc.test.ts b/server/tests/unit/packing/packing.rpc.test.ts index bb9b1e756..4b81d6b3e 100644 --- a/server/tests/unit/packing/packing.rpc.test.ts +++ b/server/tests/unit/packing/packing.rpc.test.ts @@ -15,7 +15,7 @@ import { PackingRpc } from '../../../src/nest/packing/packing.rpc'; import { PackingModule } from '../../../src/nest/packing/packing.module'; import { PackingService } from '../../../src/nest/packing/packing.service'; import type { RealtimeService } from '../../../src/nest/realtime/realtime.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; @@ -55,8 +55,8 @@ function build(opts: { canEdit?: boolean; before?: Item | undefined; updated?: I Object.assign(packing, data); const guards = new PluginGuards( { - canAccessTrip: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), - } as unknown as DatabaseService, + findAccessible: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), + } as unknown as TripsRepository, { checkPermission: vi.fn(() => opts.canEdit ?? true) } as unknown as PermissionsService, { isAddonEnabled: vi.fn(() => true) } as unknown as AddonsService, { getRole: vi.fn(async () => 'user') } as unknown as UsersRepository, diff --git a/server/tests/unit/places/places.rpc.test.ts b/server/tests/unit/places/places.rpc.test.ts index ed80dd1c5..8b75d0a9c 100644 --- a/server/tests/unit/places/places.rpc.test.ts +++ b/server/tests/unit/places/places.rpc.test.ts @@ -16,7 +16,7 @@ import { PlacesModule } from '../../../src/nest/places/places.module'; import type { PlacesService } from '../../../src/nest/places/places.service'; import type { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service'; import type { RealtimeService } from '../../../src/nest/realtime/realtime.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; @@ -53,8 +53,8 @@ function build(opts: { canEdit?: boolean; journeyThrows?: boolean } = {}) { const realtime = { broadcast: vi.fn() }; const guards = new PluginGuards( { - canAccessTrip: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), - } as unknown as DatabaseService, + findAccessible: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), + } as unknown as TripsRepository, { checkPermission: vi.fn(() => opts.canEdit ?? true) } as unknown as PermissionsService, { isAddonEnabled: vi.fn(() => true) } as unknown as AddonsService, { getRole: vi.fn(async () => 'user') } as unknown as UsersRepository, diff --git a/server/tests/unit/plugins/addon-domains-errors.rpc.test.ts b/server/tests/unit/plugins/addon-domains-errors.rpc.test.ts index b2c3ba838..2cc31ffda 100644 --- a/server/tests/unit/plugins/addon-domains-errors.rpc.test.ts +++ b/server/tests/unit/plugins/addon-domains-errors.rpc.test.ts @@ -16,7 +16,7 @@ import { BucketItemExistsError } from '../../../src/nest/atlas/atlas.service'; import { VacayRpc } from '../../../src/nest/vacay/vacay.rpc'; import { JournalRpc } from '../../../src/nest/journey/journal.rpc'; import { CollectionsRpc } from '../../../src/nest/collections/collections.rpc'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; @@ -39,9 +39,9 @@ function build(overrides: { collections?: Record; } = {}) { const db = { - canAccessTrip: vi.fn(async () => ({ id: 1, user_id: 42 })), + findAccessible: vi.fn(async () => ({ id: 1, user_id: 42 })), prepare: vi.fn(() => ({ get: () => ({ role: 'user' }), all: () => [] })), - } as unknown as DatabaseService; + } as unknown as TripsRepository; const guards = new PluginGuards( db, { checkPermission: vi.fn(() => true) } as unknown as PermissionsService, diff --git a/server/tests/unit/plugins/addon-gates.rpc.test.ts b/server/tests/unit/plugins/addon-gates.rpc.test.ts index 44f382dc1..a9b587a3a 100644 --- a/server/tests/unit/plugins/addon-gates.rpc.test.ts +++ b/server/tests/unit/plugins/addon-gates.rpc.test.ts @@ -20,7 +20,7 @@ import { VacayRpc } from '../../../src/nest/vacay/vacay.rpc'; import { JournalRpc } from '../../../src/nest/journey/journal.rpc'; import { CollectionsRpc } from '../../../src/nest/collections/collections.rpc'; import { CostsRpc } from '../../../src/nest/budget/costs.rpc'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; @@ -57,9 +57,9 @@ function spyService(calls: string[], name: string) { function build(addonOn: boolean) { const calls: string[] = []; const db = { - canAccessTrip: vi.fn(async () => ({ id: 1, user_id: 42 })), + findAccessible: vi.fn(async () => ({ id: 1, user_id: 42 })), prepare: vi.fn(() => ({ get: () => ({ role: 'user' }), all: () => [] })), - } as unknown as DatabaseService; + } as unknown as TripsRepository; const guards = new PluginGuards( db, { checkPermission: vi.fn(() => true) } as unknown as PermissionsService, diff --git a/server/tests/unit/plugins/day-schedule.controller.test.ts b/server/tests/unit/plugins/day-schedule.controller.test.ts index 2e814fe9c..2e06a7fe6 100644 --- a/server/tests/unit/plugins/day-schedule.controller.test.ts +++ b/server/tests/unit/plugins/day-schedule.controller.test.ts @@ -16,7 +16,7 @@ vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ all: () => tripDays.value }) }, canAccessTrip, })); -import { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { DayScheduleController } from '../../../src/nest/plugins/contributions/day-schedule.controller'; @@ -32,7 +32,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { } as unknown as PluginHooks; // CT1 (Plan 3j Task 5) — the day-id-set read is now DaysRepository.listIdsByTrip. const days = { listIdsByTrip: vi.fn(async () => tripDays.value.map((d) => d.id)) } as unknown as DaysRepository; - return { c: new DayScheduleController(runtime, { canAccessTrip } as unknown as DatabaseService, days), runtime }; + return { c: new DayScheduleController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, days), runtime }; } const item = (over: Record = {}) => ({ id: 's1', dayId: 10, label: 'Charging', ...over }); diff --git a/server/tests/unit/plugins/day-tints.controller.test.ts b/server/tests/unit/plugins/day-tints.controller.test.ts index 880ba77d7..b03acc37c 100644 --- a/server/tests/unit/plugins/day-tints.controller.test.ts +++ b/server/tests/unit/plugins/day-tints.controller.test.ts @@ -16,7 +16,7 @@ vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ all: () => tripDays.value }) }, canAccessTrip, })); -import { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { DayTintsController } from '../../../src/nest/plugins/contributions/day-tints.controller'; @@ -32,7 +32,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { } as unknown as PluginHooks; // CT2 (Plan 3j Task 5) — the day-id-set read is now DaysRepository.listIdsByTrip. const days = { listIdsByTrip: vi.fn(async () => tripDays.value.map((d) => d.id)) } as unknown as DaysRepository; - return { c: new DayTintsController(runtime, { canAccessTrip } as unknown as DatabaseService, days), runtime }; + return { c: new DayTintsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, days), runtime }; } const tint = (over: Record = {}) => ({ dayId: 10, tone: 'success', ...over }); diff --git a/server/tests/unit/plugins/host-rpc.test.ts b/server/tests/unit/plugins/host-rpc.test.ts index 38c9aef0c..48b610bd3 100644 --- a/server/tests/unit/plugins/host-rpc.test.ts +++ b/server/tests/unit/plugins/host-rpc.test.ts @@ -65,7 +65,6 @@ import { UnreadableLlmResponse } from '../../../src/nest/llm-parse/clients/opena import { getPluginDataDb, closePluginDataDb } from '../../../src/nest/plugins/host/plugin-host-state'; import { verifyChain } from '../../../src/nest/plugins/host/plugin-audit'; import { db as mockDb } from '../../../src/db/database'; -import { DatabaseService } from '../../../src/nest/database/database.service'; import { RealtimeService } from '../../../src/nest/realtime/realtime.service'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; @@ -154,6 +153,13 @@ type RawDb = { const raw = mockDb as unknown as RawDb; const tripsRepo = { + // Plan 4 Task 2 — PluginGuards' own canAccessTrip delegate is now this + // repository's findAccessible directly (same access rule the old + // `db/database` mock factory above encoded: trip 1 belongs to user 5, + // user 6 is a member). + async findAccessible(tripId: number | string, userId: number) { + return Number(tripId) === 1 && (userId === 5 || userId === 6) ? { id: 1, user_id: 5, currency: null } : undefined; + }, async sharesTripWith(userIdA: number, userIdB: number) { return !!raw .prepare( @@ -283,17 +289,6 @@ const scheduledTasksRepo = { }, } as unknown as PluginScheduledTasksRepository; -const dbs = new DatabaseService(mockDb); -// Plan 3c Task 0b: `canAccessTrip` is `TripsRepository.findAccessible` now, -// which needs a real EntityManager over the full migrated schema — this -// file's `mockDb` is a hand-trimmed `:memory:` table set (no `currency` -// column on `trips`, among others), so a real repository read isn't -// possible against it. Spied directly with the exact access rule the old -// `db/database` mock factory above encoded (trip 1 belongs to user 5, user 6 -// is a member), which is now dead code for this purpose. -vi.spyOn(dbs, 'canAccessTrip').mockImplementation(async (tripId, userId) => - Number(tripId) === 1 && (userId === 5 || userId === 6) ? { id: 1, user_id: 5, currency: null } : undefined, -); // Plan 3j Task 1: PluginGuards' own role lookup (PG3/PG4) now goes through // UsersRepository.getRole, not `dbs.prepare(...)`. Same reasoning as the // canAccessTrip spy above — a real UsersRepository would need a real @@ -315,11 +310,14 @@ const usersRepo = { ); }, } as unknown as UsersRepository; -const guards = new PluginGuards(dbs, permissions, addons, usersRepo); +// Plan 4 Task 2 — PluginGuards'/MetaRpc's/HostSurfaceRpc's own DatabaseService +// params are gone: canAccessTrip now reads through the TripsRepository each +// already injects (`tripsRepo` above, findAccessible-only). +const guards = new PluginGuards(tripsRepo, permissions, addons, usersRepo); const registry = createTestPluginRegistry([ new DbRpc(userSettings), - new MetaRpc(dbs, guards, metaRepo, tripsRepo, placesRepo, daysRepo, reservationsRepo, dayAccommodationsRepo), - new HostSurfaceRpc(dbs, new RealtimeService(), notifications, llmConfig, oauth, guards, pluginAuditRepo, usersRepo, tripsRepo, scheduledTasksRepo), + new MetaRpc(guards, metaRepo, tripsRepo, placesRepo, daysRepo, reservationsRepo, dayAccommodationsRepo), + new HostSurfaceRpc(new RealtimeService(), notifications, llmConfig, oauth, guards, pluginAuditRepo, usersRepo, tripsRepo, scheduledTasksRepo), ]); const factory = new PluginRpcHostFactory(pluginAuditRepo, registry as unknown as PluginRpcRegistryService); const stubRouter: PluginCallRouter = { callPlugin: async () => undefined, emitPluginEvent: async () => {} }; diff --git a/server/tests/unit/plugins/map-layers.controller.test.ts b/server/tests/unit/plugins/map-layers.controller.test.ts index 13cbc6c1d..5c0ae045c 100644 --- a/server/tests/unit/plugins/map-layers.controller.test.ts +++ b/server/tests/unit/plugins/map-layers.controller.test.ts @@ -16,7 +16,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { MapLayersController } from '../../../src/nest/plugins/contributions/map-layers.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -25,7 +25,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { providersOf: vi.fn(() => providers), mapLayers: vi.fn(async (id: string) => invoke(id)), } as unknown as PluginHooks; - return { c: new MapLayersController(runtime, { canAccessTrip } as unknown as DatabaseService), runtime }; + return { c: new MapLayersController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime }; } const line = (n = 3, over: Record = {}) => ({ type: 'polyline', diff --git a/server/tests/unit/plugins/map-markers.controller.test.ts b/server/tests/unit/plugins/map-markers.controller.test.ts index f574f6db9..3cf0f8a48 100644 --- a/server/tests/unit/plugins/map-markers.controller.test.ts +++ b/server/tests/unit/plugins/map-markers.controller.test.ts @@ -15,7 +15,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { MapMarkersController } from '../../../src/nest/plugins/contributions/map-markers.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -24,7 +24,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { providersOf: vi.fn(() => providers), mapMarkers: vi.fn(async (id: string) => invoke(id)), } as unknown as PluginHooks; - return { c: new MapMarkersController(runtime, { canAccessTrip } as unknown as DatabaseService), runtime }; + return { c: new MapMarkersController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime }; } const mk = (over: Record = {}) => ({ id: 'm1', lat: 48.85, lng: 2.35, ...over }); diff --git a/server/tests/unit/plugins/pdf-sections.controller.test.ts b/server/tests/unit/plugins/pdf-sections.controller.test.ts index 33982205d..65b7ed4f3 100644 --- a/server/tests/unit/plugins/pdf-sections.controller.test.ts +++ b/server/tests/unit/plugins/pdf-sections.controller.test.ts @@ -16,7 +16,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { PdfSectionsController } from '../../../src/nest/plugins/contributions/pdf-sections.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -25,7 +25,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { providersOf: vi.fn(() => providers), pdfSections: vi.fn(async (id: string) => invoke(id)), } as unknown as PluginHooks; - return { c: new PdfSectionsController(runtime, { canAccessTrip } as unknown as DatabaseService), runtime }; + return { c: new PdfSectionsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime }; } const sec = (over: Record = {}) => ({ title: 'Weather', ...over }); diff --git a/server/tests/unit/plugins/place-details.controller.test.ts b/server/tests/unit/plugins/place-details.controller.test.ts index 4d48417e1..9b312df76 100644 --- a/server/tests/unit/plugins/place-details.controller.test.ts +++ b/server/tests/unit/plugins/place-details.controller.test.ts @@ -9,7 +9,7 @@ vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ get: (placeId: number) => placeTrip(placeId) }) }, canAccessTrip, })); -import { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { PlaceDetailsController } from '../../../src/nest/plugins/contributions/place-details.controller'; @@ -26,7 +26,7 @@ function controller(over: Partial = {}) { } as unknown as PluginHooks; // CT7 (Plan 3j Task 5) — the place's owning trip id is now Places.repository.ts#findTripId. const places = { findTripId: vi.fn(async (placeId: number) => placeTrip(placeId)?.trip_id) } as unknown as PlacesRepository; - return { c: new PlaceDetailsController(runtime, { canAccessTrip } as unknown as DatabaseService, places), runtime }; + return { c: new PlaceDetailsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, places), runtime }; } describe('PlaceDetailsController', () => { diff --git a/server/tests/unit/plugins/plugin-guards.test.ts b/server/tests/unit/plugins/plugin-guards.test.ts index 7e99b1775..ec214f3d5 100644 --- a/server/tests/unit/plugins/plugin-guards.test.ts +++ b/server/tests/unit/plugins/plugin-guards.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { PluginGuards } from '../../../src/nest/plugins/host/plugin-guards.service'; import { BadParams, ForbiddenResource } from '../../../src/nest/plugins/host/rpc-errors'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; @@ -47,10 +47,10 @@ const ctx = (actingUserId: number | undefined): PluginRpcContext => ({ */ function build(overrides: { role?: string | null; allow?: boolean; addonOn?: boolean } = {}) { const db = { - canAccessTrip: vi.fn(async (tripId: number, userId: number) => + findAccessible: vi.fn(async (tripId: number, userId: number) => tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined, ), - } as unknown as DatabaseService; + } as unknown as TripsRepository; const users = { getRole: vi.fn(async () => ('role' in overrides ? (overrides.role ?? null) : 'user')), } as unknown as UsersRepository; @@ -149,8 +149,8 @@ describe('PluginGuards — requireTripEdit and canEditAs', () => { it('PGUARD-014 a non-owner member is flagged as shared', async () => { const db = { - canAccessTrip: vi.fn(async () => ({ id: 1, user_id: 7 })), - } as unknown as DatabaseService; + findAccessible: vi.fn(async () => ({ id: 1, user_id: 7 })), + } as unknown as TripsRepository; const users = { getRole: vi.fn(async () => 'user') } as unknown as UsersRepository; const permissions = { checkPermission: vi.fn(() => true) } as unknown as PermissionsService; const guards = new PluginGuards(db, permissions, {} as AddonsService, users); @@ -201,8 +201,8 @@ describe('PluginGuards — canEditAs/canCreateAs against a real UsersRepository orm = await createTestOrm(testDb); const permissions = new PermissionsService(orm.repo(AppSettings), new UnitOfWork(orm.em)); const db = { - canAccessTrip: vi.fn(async (tripId: number) => (tripId === 1 ? { id: 1, user_id: ownerId } : undefined)), - } as unknown as DatabaseService; + findAccessible: vi.fn(async (tripId: number) => (tripId === 1 ? { id: 1, user_id: ownerId } : undefined)), + } as unknown as TripsRepository; // The permissions cache is module-scoped (permissions-cache.ts), not // per-instance — invalidate before every case so a stored override from // another describe block in this same worker can't leak in. diff --git a/server/tests/unit/plugins/plugin-routes.controller.test.ts b/server/tests/unit/plugins/plugin-routes.controller.test.ts index fa2539a60..f0adb1bd4 100644 --- a/server/tests/unit/plugins/plugin-routes.controller.test.ts +++ b/server/tests/unit/plugins/plugin-routes.controller.test.ts @@ -16,7 +16,7 @@ vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ get: () => (capabilitiesRow.value === undefined ? undefined : { capabilities: capabilitiesRow.value }) }) }, canAccessTrip, })); -import { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { PluginRoutesController } from '../../../src/nest/plugins/contributions/plugin-routes.controller'; @@ -36,7 +36,7 @@ function controller(invoke: () => unknown, providers = ['ev-plug']) { const plugins = { findCapabilities: vi.fn(async () => (capabilitiesRow.value === undefined ? null : capabilitiesRow.value)), } as unknown as PluginsRepository; - return { c: new PluginRoutesController(runtime, { canAccessTrip } as unknown as DatabaseService, plugins), runtime }; + return { c: new PluginRoutesController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, plugins), runtime }; } const wp = (n = 3) => Array.from({ length: n }, (_, i) => ({ lat: 48 + i * 0.1, lng: 2 + i * 0.1 })); const goodRoute = (n = 3, over: Record = {}) => ({ diff --git a/server/tests/unit/plugins/request-context.test.ts b/server/tests/unit/plugins/request-context.test.ts index 7a4a8da39..1ad0a675c 100644 --- a/server/tests/unit/plugins/request-context.test.ts +++ b/server/tests/unit/plugins/request-context.test.ts @@ -42,6 +42,7 @@ import { RpcRateLimiter, DEFAULT_RPC_LIMIT } from '../../../src/nest/plugins/hos import { createTestOrm, type TestOrm } from '../../helpers/test-orm'; import { AppSettings } from '../../../src/db/entities/AppSettings.entity'; import { Users } from '../../../src/db/entities/Users.entity'; +import { Trips } from '../../../src/db/entities/Trips.entity'; import type { PluginRpcHost } from '../../../src/nest/plugins/host/rpc-host'; import type { RpcRequest, RpcResponse, RpcError } from '../../../src/nest/plugins/protocol/envelope'; import type { EntityManager } from '@mikro-orm/core'; @@ -76,7 +77,9 @@ beforeAll(async () => { // above), not a stub, because this suite's whole point is proving that // read fails outside withRequestContext and succeeds inside it (D6/C3). // A mocked UsersRepository would defeat that regression coverage. - guards = new PluginGuards(dbs, permissions, await createTestAddonsService(testDb, dbs), t.repo(Users)); + // Plan 4 Task 2 — PluginGuards' own canAccessTrip delegate is now + // TripsRepository.findAccessible; same REAL-repository reasoning applies. + guards = new PluginGuards(t.repo(Trips), permissions, await createTestAddonsService(testDb, dbs), t.repo(Users)); userId = createUser(testDb, { role: 'user' }).user.id; // An admin has tightened trip_create from its 'everybody' default. testDb.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('perm_trip_create', 'admin')").run(); diff --git a/server/tests/unit/plugins/rpc-fallbacks.test.ts b/server/tests/unit/plugins/rpc-fallbacks.test.ts index 11d34877d..69c819524 100644 --- a/server/tests/unit/plugins/rpc-fallbacks.test.ts +++ b/server/tests/unit/plugins/rpc-fallbacks.test.ts @@ -24,7 +24,7 @@ import { DaysRpc } from '../../../src/nest/days/days.rpc'; import { VacayRpc } from '../../../src/nest/vacay/vacay.rpc'; import { NotFoundError, ValidationError } from '../../../src/nest/trips/trips.service'; import type { EntityManager } from '@mikro-orm/core'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; @@ -42,9 +42,9 @@ const ALL = new Set([ function guardsFor() { const db = { - canAccessTrip: vi.fn(async () => ({ id: 1, user_id: 42 })), + findAccessible: vi.fn(async () => ({ id: 1, user_id: 42 })), prepare: vi.fn(() => ({ get: () => ({ role: 'user' }), all: () => [] })), - } as unknown as DatabaseService; + } as unknown as TripsRepository; // Plan 3c Task 7: TripsRpc's RP1-RP6 resolve `EntityManager.getRepository(...)` // now, not `db.prepare(...)` — a generic fake repository (same shape regardless // of which entity class was requested) covers every RP method these fallback diff --git a/server/tests/unit/plugins/rpc-host-inv-strip.test.ts b/server/tests/unit/plugins/rpc-host-inv-strip.test.ts index 27611051a..d6e9830f9 100644 --- a/server/tests/unit/plugins/rpc-host-inv-strip.test.ts +++ b/server/tests/unit/plugins/rpc-host-inv-strip.test.ts @@ -18,7 +18,7 @@ import { TripsRpc } from '../../../src/nest/trips/trips.rpc'; import { DbRpc } from '../../../src/nest/plugins/host/rpc/db.rpc'; import { PluginGuards } from '../../../src/nest/plugins/host/plugin-guards.service'; import type { EntityManager } from '@mikro-orm/core'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import type { PluginUserSettingsService } from '../../../src/nest/plugins/plugin-user-settings.service'; const req = (method: string, params: Record): RpcRequest => ({ k: 'req', id: 'x', method, params }); @@ -36,9 +36,9 @@ const dbRegistry = () => { /** trips.getById lives on the decorators now, so the audit cases bind it through them. */ const tripsRegistry = () => { const db = { - canAccessTrip: async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined), + findAccessible: async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined), prepare: () => ({ get: () => ({ id: 1, title: 'Japan' }), all: () => [] }), - } as unknown as DatabaseService; + } as unknown as TripsRepository; const guards = new PluginGuards(db, {} as never, {} as never, {} as never); // Plan 3c Task 7: trips.getById (RP1) now reads through // `EntityManager.getRepository(Trips).findRaw(...)`, not `db.prepare(...)`. diff --git a/server/tests/unit/plugins/trip-card-contributions.controller.test.ts b/server/tests/unit/plugins/trip-card-contributions.controller.test.ts index 20684cd24..d09696b00 100644 --- a/server/tests/unit/plugins/trip-card-contributions.controller.test.ts +++ b/server/tests/unit/plugins/trip-card-contributions.controller.test.ts @@ -17,7 +17,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { TripCardContributionsController } from '../../../src/nest/plugins/contributions/trip-card-contributions.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -26,7 +26,7 @@ function controller(invoke: (id: string, args: unknown[]) => unknown, providers providersOf: vi.fn(() => providers), tripCards: vi.fn(async (id: string, _h: string, _fn: string, args: unknown[]) => invoke(id, args)), } as unknown as PluginHooks; - return { c: new TripCardContributionsController(runtime, { canAccessTrip } as unknown as DatabaseService), runtime }; + return { c: new TripCardContributionsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime }; } const badge = (over: Record = {}) => ({ tripId: 1, id: 'b1', label: 'Visa', ...over }); diff --git a/server/tests/unit/plugins/trip-warnings.controller.test.ts b/server/tests/unit/plugins/trip-warnings.controller.test.ts index bdc28bb5b..7422509eb 100644 --- a/server/tests/unit/plugins/trip-warnings.controller.test.ts +++ b/server/tests/unit/plugins/trip-warnings.controller.test.ts @@ -9,7 +9,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { TripWarningsController } from '../../../src/nest/plugins/contributions/trip-warnings.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -20,7 +20,7 @@ function controller(over: Partial = {}) { id === 'p2' ? [{ level: 'error', message: 'Day 3 is overpacked', dayId: 3 }] : [{ level: 'warning', message: 'Museum closed Mon', placeId: 7 }]), ...over, } as unknown as PluginHooks; - return { c: new TripWarningsController(runtime, { canAccessTrip } as unknown as DatabaseService), runtime }; + return { c: new TripWarningsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime }; } describe('TripWarningsController', () => { diff --git a/server/tests/unit/plugins/view-contributions.controller.test.ts b/server/tests/unit/plugins/view-contributions.controller.test.ts index 79b5090be..15a7b2c86 100644 --- a/server/tests/unit/plugins/view-contributions.controller.test.ts +++ b/server/tests/unit/plugins/view-contributions.controller.test.ts @@ -16,7 +16,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { ViewContributionsController } from '../../../src/nest/plugins/contributions/view-contributions.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -25,7 +25,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { providersOf: vi.fn(() => providers), tableContributions: vi.fn(async (id: string) => invoke(id)), } as unknown as PluginHooks; - return { c: new ViewContributionsController(runtime, { canAccessTrip } as unknown as DatabaseService), runtime }; + return { c: new ViewContributionsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime }; } const col = (over: Record = {}) => ({ kind: 'column', entityId: 1, id: 'c1', label: 'X', ...over }); const act = (over: Record = {}) => ({ kind: 'action', entityId: 1, id: 'a1', label: 'Go', target: { kind: 'frame', sub: '/ui' }, ...over }); diff --git a/server/tests/unit/reservations/bookings.rpc.test.ts b/server/tests/unit/reservations/bookings.rpc.test.ts index 214e09b31..1a40d4d93 100644 --- a/server/tests/unit/reservations/bookings.rpc.test.ts +++ b/server/tests/unit/reservations/bookings.rpc.test.ts @@ -20,7 +20,7 @@ import { AccommodationsModule } from '../../../src/nest/accommodations/accommoda import type { ReservationsService } from '../../../src/nest/reservations/reservations.service'; import type { AccommodationsService } from '../../../src/nest/accommodations/accommodations.service'; import type { RealtimeService } from '../../../src/nest/realtime/realtime.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; @@ -81,8 +81,8 @@ function build(opts: { canEdit?: boolean; cascade?: boolean; stop?: boolean; see } as unknown as AccommodationsService & Record>; const guards = new PluginGuards( { - canAccessTrip: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), - } as unknown as DatabaseService, + findAccessible: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), + } as unknown as TripsRepository, { checkPermission: vi.fn((action: string) => { opts.seenActions?.push(action); diff --git a/server/tests/unit/todo/todo.rpc.test.ts b/server/tests/unit/todo/todo.rpc.test.ts index ac5f537dd..b0e188e51 100644 --- a/server/tests/unit/todo/todo.rpc.test.ts +++ b/server/tests/unit/todo/todo.rpc.test.ts @@ -12,7 +12,7 @@ import { TodoRpc } from '../../../src/nest/todo/todo.rpc'; import { TodoModule } from '../../../src/nest/todo/todo.module'; import type { TodoService } from '../../../src/nest/todo/todo.service'; import type { RealtimeService } from '../../../src/nest/realtime/realtime.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; @@ -33,8 +33,8 @@ function build(canEdit = true) { const permissions = { checkPermission: vi.fn(() => canEdit) } as unknown as PermissionsService; const guards = new PluginGuards( { - canAccessTrip: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), - } as unknown as DatabaseService, + findAccessible: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), + } as unknown as TripsRepository, permissions, { isAddonEnabled: vi.fn(() => true) } as unknown as AddonsService, { getRole: vi.fn(async () => 'user') } as unknown as UsersRepository, diff --git a/server/tests/unit/trips/trips.rpc.test.ts b/server/tests/unit/trips/trips.rpc.test.ts index 4a992b850..4f2e5cf18 100644 --- a/server/tests/unit/trips/trips.rpc.test.ts +++ b/server/tests/unit/trips/trips.rpc.test.ts @@ -24,7 +24,7 @@ import type { AccommodationsService } from '../../../src/nest/accommodations/acc import type { TripMembersService } from '../../../src/nest/trip-members/trip-members.service'; import type { TripMembershipService } from '../../../src/nest/trip-membership/trip-membership.service'; import type { RealtimeService } from '../../../src/nest/realtime/realtime.service'; -import type { DatabaseService } from '../../../src/nest/database/database.service'; +import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; @@ -72,8 +72,8 @@ export function build(opts: { allow?: (action: string) => boolean; updateThrows? // uses for `TripsRpc.update` itself, but through PluginGuards' own // constructor param, stubbed separately just below. const db = { - canAccessTrip: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), - } as unknown as DatabaseService; + findAccessible: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), + } as unknown as TripsRepository; // Plan 3c Task 7: RP1-RP6's SQL-text-keyed `db.prepare` stub is gone — // every raw statement moved to a repository method, so this is a // repository-level stub, keyed on the entity CLASS `em.getRepository(...)` @@ -176,8 +176,8 @@ describe('TripsRpc reads', () => { }), } as unknown as EntityManager; const db = { - canAccessTrip: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), - } as unknown as DatabaseService; + findAccessible: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 42 ? { id: 1, user_id: 42 } : undefined)), + } as unknown as TripsRepository; const permissions = { checkPermission: vi.fn(() => true) } as unknown as PermissionsService; const guards = new PluginGuards( db,