refactor(server): plan 4 — inline canAccessTrip/isOwner off DatabaseService (plugins + MCP surfaces)

This commit is contained in:
jubnl
2026-09-24 05:47:29 +02:00
parent 51327e75c0
commit ea7b93de5d
51 changed files with 232 additions and 179 deletions
+7 -3
View File
@@ -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<void> {
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}`);
}
+8 -3
View File
@@ -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;
}
@@ -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<number>, 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: [] };
@@ -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<number>, 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: [] };
@@ -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(
@@ -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(
@@ -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(
@@ -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(
@@ -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,
@@ -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 };
@@ -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<number>): 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);
@@ -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(
@@ -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<McpTextResult> {
// 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');
@@ -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(
@@ -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],
})
@@ -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<void> {
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<boolean> {
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
@@ -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);
+3 -3
View File
@@ -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<string, string> = {
@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))) {
@@ -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) {
@@ -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],
})
+8 -3
View File
@@ -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) {
+5 -5
View File
@@ -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],
+14 -14
View File
@@ -231,7 +231,6 @@ export async function createMcpTestRegistry(): Promise<McpRegistry> {
);
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<McpRegistry> {
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<McpRegistry> {
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<McpRegistry> {
// 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<McpRegistry> {
);
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<McpRegistry> {
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<McpRegistry> {
// 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<McpRegistry> {
// 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<McpRegistry> {
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<McpRegistry> {
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<McpRegistry> {
// (`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<McpRegistry> {
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 },
+17 -12
View File
@@ -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),
+4 -2
View File
@@ -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.
+3 -3
View File
@@ -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<string, ReturnType<typeof vi.fn>>;
const realtime = { broadcast: vi.fn() } as unknown as RealtimeService & { broadcast: ReturnType<typeof vi.fn> };
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,
+3 -3
View File
@@ -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<string, ReturnType<typeof vi.fn>>;
const realtime = { broadcast: vi.fn() } as unknown as RealtimeService & { broadcast: ReturnType<typeof vi.fn> };
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,
@@ -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<typeof 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,
+5 -5
View File
@@ -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<string, unknown> | undefined; foreign?: str
softDeleteFile: vi.fn(),
} as unknown as FilesService & Record<string, ReturnType<typeof vi.fn>>;
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,
@@ -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,
+3 -3
View File
@@ -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,
@@ -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<string, unknown>;
} = {}) {
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,
@@ -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,
@@ -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<string, unknown> = {}) => ({ id: 's1', dayId: 10, label: 'Charging', ...over });
@@ -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<string, unknown> = {}) => ({ dayId: 10, tone: 'success', ...over });
+13 -15
View File
@@ -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 () => {} };
@@ -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<string, unknown> = {}) => ({
type: 'polyline',
@@ -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<string, unknown> = {}) => ({ id: 'm1', lat: 48.85, lng: 2.35, ...over });
@@ -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<string, unknown> = {}) => ({ title: 'Weather', ...over });
@@ -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<PluginHooks> = {}) {
} 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', () => {
@@ -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.
@@ -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<string, unknown> = {}) => ({
@@ -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();
@@ -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
@@ -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<string, unknown>): 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(...)`.
@@ -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<string, unknown> = {}) => ({ tripId: 1, id: 'b1', label: 'Visa', ...over });
@@ -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<PluginHooks> = {}) {
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', () => {
@@ -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<string, unknown> = {}) => ({ kind: 'column', entityId: 1, id: 'c1', label: 'X', ...over });
const act = (over: Record<string, unknown> = {}) => ({ kind: 'action', entityId: 1, id: 'a1', label: 'Go', target: { kind: 'frame', sub: '/ui' }, ...over });
@@ -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<string, ReturnType<typeof 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((action: string) => {
opts.seenActions?.push(action);
+3 -3
View File
@@ -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,
+5 -5
View File
@@ -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,