chore: wire TripsService into the plugin RPC host and retire the legacy tripService

Swap the factory's six tripService imports for the injected TripsService; delete services/tripService.ts and the collab/vacay bridges with it; prune the days/budget bridges to their surviving exports; update the migration docs and regenerate migration-graph.md.
This commit is contained in:
jubnl
2026-07-29 01:53:54 +02:00
parent 530c7cb005
commit 287dceb532
18 changed files with 179 additions and 1370 deletions
+25 -4
View File
@@ -28,8 +28,8 @@ mount to Nest and leaves the sibling trip routes (days, places, ...) on Express.
- **Phase 2 (trip sub-domains):** vacay (addon), packing, todo.
- **DI-native services (legacy `src/services/*` deleted):** tags, categories,
todo, packing, day-notes, trip-invite, assignments, share, settings, files,
collab, vacay, reservations, day, permissions, audit — see the migration
recipe below.
collab, vacay, reservations, day, permissions, audit, budget, trip — see the
migration recipe below.
## Cross-cutting Foundation pieces
@@ -203,9 +203,30 @@ MCP class) and BookingImportService inject `BudgetService`; a 4-export
trips/transports registrars; `exchange-rates.bridge.ts` was deleted with its
last consumers, and the controller adopted `budget.dto.ts` — all nine
allow-list entries removed).
tripService followed (the 1121-line Wave-4 hub — the biggest fold — moved into
the wrapper `TripsService`: TRIP_SELECT + list/create/get, the
`generateDays` two-phase renumber engine, the updateTrip date-shift
transaction, the member/guest lifecycle (#973/#1362), the ICS export with its
module-scoped tz-validity cache, `copyTripById` and `getTripSummary`; its six
bridge imports became injected services (CollabService + VacayService joined
the constructor); the 10-tool trips registrar + 3 `resources.ts` trip
resources + the trip-summary prompt moved to `trips.mcp.ts` — the first
`@Prompt` use, with the fire-once static-token deprecation notice now riding
the `registry.attach` ctx — and the 3 share-link tools it carried moved to
`share.mcp.ts` on the `canShareTrips` predicate (delete_trip and the
canReadTrips reads are predicates too — the broadened legacy gates have no
declarative equivalent); FeedsService and the plugin host inject
`TripsService` (its 20th constructor dep); a 3-export `trips.bridge.ts`
serves the legacy prompts registrar and budget.mcp.ts's owner/member seam
(injecting there would need a forwardRef'd TripsModule↔BudgetModule cycle);
`todo.bridge.ts`, `share.bridge.ts`, `collab.bridge.ts` and `vacay.bridge.ts`
were deleted with their last consumers and the unused days/budget bridge
exports pruned; the controller adopted `trips.dto.ts` — all seven allow-list
entries removed).
Repeat these steps per
service (next up: **tripService** — the budget fold was its unblock, per the
dependency-honest order in `migration-graph.md`). This is a
service (next up: the **mapsService → transitService → placeService** chain —
or the notifications fan-in — per the dependency-honest order in
`migration-graph.md`). This is a
**pure relocation** — byte-identical
SQL, statuses, bodies, and error strings. The plugin RPC host is **no longer a
bridge consumer**: since Option A of `src/nest/plugins/DI-MIGRATION.md` it
+10 -17
View File
@@ -7,15 +7,16 @@ import { ExchangeRatesService } from './exchange-rates.service';
/**
* Non-Nest entry point for the budget domain — for code running OUTSIDE the
* Nest container (the legacy services/tripService.ts and
* services/userCleanupService.ts, and the legacy update_trip / create_transport
* registrars in src/mcp/tools/trips.ts and src/mcp/tools/transports.ts; the
* budget MCP tools and resources moved to the DI-discovered budget.mcp.ts, and
* the plugin RPC host injects BudgetService via PluginHostDepsFactory).
* Exports only the legacy services/budgetService names still consumed outside
* the container, 1:1, so repointing a consumer is an import-path-only diff.
* Inside the container, inject BudgetService instead. Delete this file when
* tripService/userCleanupService and the legacy MCP registrars migrate.
* Nest container (services/userCleanupService.ts and the legacy
* create_transport registrar in src/mcp/tools/transports.ts; the budget MCP
* tools and resources moved to the DI-discovered budget.mcp.ts, the plugin
* RPC host injects BudgetService via PluginHostDepsFactory, and the trip
* domain folded into the DI-native TripsService, which injects BudgetService —
* its bridge exports were pruned with it). Exports only the legacy
* services/budgetService names still consumed outside the container, 1:1, so
* repointing a consumer is an import-path-only diff. Inside the container,
* inject BudgetService instead. Delete this file when
* userCleanupService and the transports registrar migrate.
*
* Module-level construction is safe: `db` is the reinitialize-proof Proxy onto
* the shared better-sqlite3 singleton, and ExchangeRatesService keeps its FX
@@ -24,18 +25,10 @@ import { ExchangeRatesService } from './exchange-rates.service';
*/
const budget = new BudgetService(new DatabaseService(db), new PermissionsService(new DatabaseService(db)), new ExchangeRatesService(), new RealtimeService());
export function listBudgetItems(tripId: string | number) {
return budget.listBudgetItems(tripId);
}
export function removeUserFromBudgetItems(userId: number): void {
budget.removeUserFromBudgetItems(userId);
}
export function rebaseTripCurrency(tripId: string | number, newCurrency: string | null | undefined): Promise<void> {
return budget.rebaseTripCurrency(tripId, newCurrency);
}
export function linkBudgetItemToReservation(
tripId: string | number,
reservationId: number,
-33
View File
@@ -1,33 +0,0 @@
import { db } from '../../db/database';
import { DatabaseService } from '../database/database.service';
import { RealtimeService } from '../realtime/realtime.service';
import { CollabService } from './collab.service';
import { PermissionsService } from '../permissions/permissions.service';
/**
* Non-Nest entry point for the collab domain — for code running OUTSIDE the
* Nest container (the legacy trip-summary builder in src/services/tripService.ts
* and the legacy get_trip_summary registrar in src/mcp/tools/trips.ts; the
* collab MCP tools and resources moved to the DI-discovered collab.mcp.ts, and
* the plugin RPC host injects CollabService via PluginHostDepsFactory). Exports
* only the legacy services/collabService names still consumed outside the
* container, 1:1, so repointing a consumer is an import-path-only diff. Inside
* the container, inject CollabService instead. Delete this file when
* tripService and the legacy MCP trips registrar migrate.
*
* Module-level construction is safe: `db` is the reinitialize-proof Proxy onto
* the shared better-sqlite3 singleton.
*/
const collab = new CollabService(new DatabaseService(db), new PermissionsService(new DatabaseService(db)), new RealtimeService());
export function listNotes(tripId: string | number) {
return collab.listNotes(tripId);
}
export function listPolls(tripId: string | number) {
return collab.listPolls(tripId);
}
export function countMessages(tripId: string | number): number {
return collab.countMessages(tripId);
}
+3 -2
View File
@@ -58,8 +58,9 @@ export interface LinkPreviewResult {
* run in db.transaction(), getFormattedNoteById is trip-scoped and null-safe,
* votePoll rejects non-integer indexes, and linkPreview absorbs malformed URLs
* instead of throwing.
* Non-Nest consumers (legacy tripService, the legacy MCP trips registrar) go
* through collab.bridge.ts instead of importing this class directly.
* All consumers are in-container since the trip fold (TripsService and
* TripsMcp inject this class); collab.bridge.ts was deleted with its last
* outside-container consumers.
*/
@Injectable()
export class CollabService {
+10 -30
View File
@@ -1,27 +1,26 @@
import { db } from '../../db/database';
import { DatabaseService } from '../database/database.service';
import { RealtimeService } from '../realtime/realtime.service';
import { DaysService, addDays } from './days.service';
import { DaysService } from './days.service';
import { PermissionsService } from '../permissions/permissions.service';
/**
* Non-Nest entry point for the day domain — for code running OUTSIDE the
* Nest container (the legacy services/tripService.ts and the legacy
* transit/transports MCP registrars in src/mcp/tools/; the day MCP tools and
* resources moved to the DI-discovered days.mcp.ts, and the plugin RPC host
* injects DaysService via PluginHostDepsFactory). Exports only the legacy
* services/dayService names still consumed outside the container, 1:1, so
* repointing a consumer is an import-path-only diff. Inside the container,
* inject DaysService instead. Delete this file when tripService and the
* transit/transports registrars migrate.
* Nest container (the legacy transit/transports MCP registrars in
* src/mcp/tools/; the day MCP tools and resources moved to the DI-discovered
* days.mcp.ts, the plugin RPC host injects DaysService via
* PluginHostDepsFactory, and the trip domain folded into the DI-native
* TripsService, which injects DaysService — its bridge exports were pruned
* with it). Exports only the legacy services/dayService names still consumed
* outside the container, 1:1, so repointing a consumer is an import-path-only
* diff. Inside the container, inject DaysService instead. Delete this file
* when the transit/transports registrars migrate.
*
* Module-level construction is safe: `db` is the reinitialize-proof Proxy onto
* the shared better-sqlite3 singleton.
*/
const days = new DaysService(new DatabaseService(db), new PermissionsService(new DatabaseService(db)), new RealtimeService());
export { addDays };
export function getDay(id: string | number, tripId: string | number) {
return days.getDay(id, tripId);
}
@@ -29,22 +28,3 @@ export function getDay(id: string | number, tripId: string | number) {
export function listDays(tripId: string | number) {
return days.list(tripId);
}
export function listAccommodations(tripId: string | number) {
return days.listAccommodations(tripId);
}
export function restampReservationDates(
tripId: string | number,
oldDateById: Map<number, string | null>,
newDateById: Map<number, string | null>,
): void {
days.restampReservationDates(tripId, oldDateById, newDateById);
}
export function resyncAccommodationDays(
tripId: string | number,
prevDateByDayId: Map<number, string | null>,
): void {
days.resyncAccommodationDays(tripId, prevDateByDayId);
}
+15 -7
View File
@@ -36,10 +36,11 @@ DI-only service and the legacy function module was deleted, but the wiring
layer didn't inject — so a module-level shim re-exported the legacy names over
a hand-built instance. Option A deleted `tags.bridge.ts` and
`categories.bridge.ts` (the plugin host was their only consumer).
**`todo.bridge.ts` survives** for exactly one remaining consumer that genuinely
runs outside the container: the legacy `get_trip_summary` registrar in
`src/mcp/tools/trips.ts`. Delete it when that registrar migrates to the
DI-discovered MCP registry. New bridges should only be added for genuinely
`todo.bridge.ts`, `share.bridge.ts`, `collab.bridge.ts` and `vacay.bridge.ts`
followed with the 2026-07 trip fold — their last consumers (the legacy
`src/mcp/tools/trips.ts` registrar and the legacy tripService itself) migrated
into the DI-discovered `trips.mcp.ts`/`share.mcp.ts` and the DI-native
`TripsService`. New bridges should only be added for genuinely
non-Nest consumers (legacy MCP registrars, scheduler, websocket) — the plugin
host no longer needs them, ever.
@@ -118,8 +119,12 @@ host's audit trail is the separate `plugin-audit.ts`); exchangeRateService
swapped in 2026-07 with the budget-domain fold — its `getRates` import became
the injected `ExchangeRatesService`; budgetService swapped in 2026-07 with the
budget migration — its `listBudgetItems` import became the already-injected
`BudgetService`, so no constructor change at all; the remaining factory
imports are all Wave-4+ domains: trips, places, journeys, atlas, collections).
`BudgetService`, so no constructor change at all; tripService swapped in
2026-07 with the trip fold — its six imported symbols (`listTrips`,
`updateTrip`, `createTrip`, `removeMember`, `NotFoundError`, `ValidationError`)
became the injected `TripsService`, with the error classes now imported from
`nest/trips/trips.service`; the remaining factory
imports are all Wave-4+/5 domains: places, journeys, atlas, collections).
### Test impact (as landed)
@@ -127,7 +132,10 @@ imports are all Wave-4+ domains: trips, places, journeys, atlas, collections).
`PluginRpcHost` directly with hand-built `HostDeps`).
- `tests/unit/plugins/plugin-host-deps.factory.test.ts` (was
`create-rpc-host.test.ts`) — the six DI-domain path mocks became constructor
stubs (sixteen stubs as of the 2026-07 exchange-rates fold); the ~22
stubs (sixteen stubs as of the 2026-07 exchange-rates fold, plus `tripsStub`
with the 2026-07 trip fold — the tripService path mock's sentinel behaviors
moved onto the stub, its throws now built from the real
`nest/trips/trips.service` error classes); the ~22
legacy-service path mocks shrank by one with the 2026-07 budget migration
(the budgetService satisfy-the-import mock died with the legacy file — the
`budgetStub` gained `listBudgetItems` for the two swapped closures); a
@@ -14,7 +14,7 @@ import fsMod from 'node:fs';
import pathMod from 'node:path';
import { randomUUID } from 'node:crypto';
import { PermissionsService } from '../../permissions/permissions.service';
import { listTrips, updateTrip, createTrip, removeMember as removeTripMemberSvc, NotFoundError, ValidationError } from '../../../services/tripService';
import { TripsService, NotFoundError, ValidationError } from '../../trips/trips.service';
import { createPlace, updatePlace, deletePlace } from '../../../services/placeService';
import { AddonsService } from '../../addons/addons.service';
import { isDemoEmail } from '../../../services/demo';
@@ -125,7 +125,7 @@ export interface PluginCallRouter {
*
* DI-native domain services (budget, exchange-rates, reservations, tags,
* categories, todo, packing, day-notes, assignments, oauth, files, collab,
* vacay, the LLM config resolver) and
* vacay, trips, the LLM config resolver) and
* the DatabaseService (all inline SQL + the trip-access helper) are
* constructor-injected; legacy `services/*` domains stay plain function
* imports until their own migration lands (DI-MIGRATION.md), at which point
@@ -153,6 +153,7 @@ export class PluginHostDepsFactory {
private readonly exchangeRates: ExchangeRatesService,
private readonly addons: AddonsService,
private readonly realtime: RealtimeService,
private readonly trips: TripsService,
) {}
/**
@@ -347,7 +348,7 @@ export class PluginHostDepsFactory {
// transfer is a separate, deliberate action, not a member-management side effect.
const trip = this.db.prepare('SELECT user_id FROM trips WHERE id = ?').get(tripId) as { user_id: number } | undefined;
if (trip && trip.user_id === targetUserId) throw new ForbiddenResource('cannot remove the trip owner');
removeTripMemberSvc(tripId, targetUserId);
this.trips.removeMember(tripId, targetUserId);
return { removed: true };
},
// --- Host-mediated notifications. Recipient resolution + channel fan-out +
@@ -431,7 +432,7 @@ export class PluginHostDepsFactory {
// Cross-trip: every accessible trip's budget items (membership predicate is
// baked into listTrips). Reuses the hydrated list so members/payers come too.
listCostsForUser: (userId) => {
const trips = listTrips(userId, null) as Array<{ id: number }>;
const trips = this.trips.list(userId, null) as Array<{ id: number }>;
return trips.flatMap((t) => this.budget.listBudgetItems(t.id));
},
// Reuses BudgetService.create (frozen FX + members/payers), then broadcasts
@@ -528,7 +529,7 @@ export class PluginHostDepsFactory {
},
createTripForUser: (userId, input) => {
try {
const result = createTrip(userId, input as unknown as Parameters<typeof createTrip>[1]);
const result = this.trips.create(userId, input as unknown as Parameters<TripsService['create']>[1]);
return result.trip;
} catch (e) {
if (e instanceof ValidationError) throw new BadParams(e.message);
@@ -552,7 +553,9 @@ export class PluginHostDepsFactory {
}
const u = this.db.prepare('SELECT role FROM users WHERE id = ?').get(userId) as { role?: string } | undefined;
try {
const result = updateTrip(tripId, userId, input as Parameters<typeof updateTrip>[2], u?.role ?? 'user');
// The no-rebase updateTrip core — parity with the legacy host path,
// which never re-anchored the budget currency.
const result = this.trips.updateTrip(tripId, userId, input as Parameters<TripsService['updateTrip']>[2], u?.role ?? 'user');
this.realtime.broadcast(tripId, 'trip:updated', { trip: result.updatedTrip });
return result.updatedTrip;
} catch (e) {
@@ -564,9 +567,9 @@ export class PluginHostDepsFactory {
// --- Cross-trip reads. The membership predicate is baked into listTrips, so a
// plugin only ever sees the acting user's own trips/reservations (no raw
// cross-tenant SELECT). Reuses the same hydrated services as the REST paths. ---
listTripsForUser: (userId) => listTrips(userId, null),
listTripsForUser: (userId) => this.trips.list(userId, null),
listReservationsForUser: (userId) => {
const trips = listTrips(userId, null) as Array<{ id: number }>;
const trips = this.trips.list(userId, null) as Array<{ id: number }>;
return trips.flatMap((t) => this.reservations.list(String(t.id)));
},
// --- Trip-scoped hydrated reads (membership already checked by tripRead). Same
+2 -1
View File
@@ -36,6 +36,7 @@ import { LlmParseModule } from '../llm-parse/llm-parse.module';
import { FilesModule } from '../files/files.module';
import { CollabModule } from '../collab/collab.module';
import { VacayModule } from '../vacay/vacay.module';
import { TripsModule } from '../trips/trips.module';
import { PermissionsModule } from '../permissions/permissions.module';
import { AuditModule } from '../audit/audit.module';
import { AddonsModule } from '../addons/addons.module';
@@ -50,7 +51,7 @@ import { AddonsModule } from '../addons/addons.module';
@Module({
// The DI-native domain services the plugin host wiring injects
// (PluginHostDepsFactory); DatabaseModule is @Global, so not listed.
imports: [TagsModule, CategoriesModule, BudgetModule, ReservationsModule, TodoModule, PackingModule, DaysModule, AssignmentsModule, LlmParseModule, FilesModule, CollabModule, VacayModule, PermissionsModule, AuditModule, AddonsModule],
imports: [TagsModule, CategoriesModule, BudgetModule, ReservationsModule, TodoModule, PackingModule, DaysModule, AssignmentsModule, LlmParseModule, FilesModule, CollabModule, VacayModule, TripsModule, PermissionsModule, AuditModule, AddonsModule],
controllers: [PluginsController, PluginsFeedController, PluginsProxyController, PluginFrameController, PlaceDetailsController, TripWarningsController, ViewContributionsController, TripCardContributionsController, PluginPhotosController, PluginCalendarController, MapMarkersController, MapLayersController, PluginRoutesController, DayScheduleController, PdfSectionsController, AtlasLayersController, JournalEntryRowsController, PluginUserSettingsController, PluginOAuthController, PluginActivityController],
providers: [PluginsService, PluginRuntimeService, PluginRegistryService, PluginOAuthService, PluginHostDepsFactory],
// Exported so the admin addon-toggle handler can cascade-disable plugins whose
-28
View File
@@ -1,28 +0,0 @@
import { db } from '../../db/database';
import { DatabaseService } from '../database/database.service';
import { RealtimeService } from '../realtime/realtime.service';
import { VacayService } from './vacay.service';
/**
* Non-Nest entry point for the vacay domain — for code running OUTSIDE the
* Nest container (the legacy services/tripService.ts, which shifts a trip
* owner's vacation entries when the trip window moves; the vacay MCP tools and
* resources moved to the DI-discovered vacay.mcp.ts, and the plugin RPC host
* injects VacayService via PluginHostDepsFactory). Exports only the legacy
* services/vacayService names still consumed outside the container, 1:1, so
* repointing a consumer is an import-path-only diff. Inside the container,
* inject VacayService instead. Delete this file when tripService migrates.
*
* Module-level construction is safe: `db` is the reinitialize-proof Proxy onto
* the shared better-sqlite3 singleton.
*/
const vacay = new VacayService(new DatabaseService(db), new RealtimeService());
export function shiftOwnerEntriesForTripWindow(
ownerId: number,
oldStart: string,
oldEnd: string,
newStart: string
): void {
return vacay.shiftOwnerEntriesForTripWindow(ownerId, oldStart, oldEnd, newStart);
}
+3 -2
View File
@@ -155,8 +155,9 @@ function windowEndYear(end: string): number {
* an AbortSignal timeout and the nager.at responses are ok-checked;
* applyHolidayCalendars honors the cache TTL; addYear no longer swallows real
* errors; the holiday cache is instance state instead of a module-level map.
* The only consumer outside the Nest container is the legacy tripService,
* which goes through vacay.bridge.ts instead of importing this class.
* All consumers are in-container since the trip fold (TripsService injects
* this class); vacay.bridge.ts was deleted with its last outside-container
* consumer.
*/
@Injectable()
export class VacayService {
File diff suppressed because it is too large Load Diff