chore: move day backend to proper DI

This commit is contained in:
jubnl
2026-07-27 20:57:01 +02:00
parent e62628bf93
commit 2d1eec76c3
35 changed files with 1491 additions and 1123 deletions
+9 -7
View File
@@ -19,7 +19,7 @@ The plugin runtime has three layers. Only the third is "un-Nest".
|---|---|---|---|
| Sandbox / transport | `supervisor/plugin-supervisor.ts`, `runtime/plugin-host-entry.ts`, `protocol/envelope.ts` | Forked child per plugin, IPC envelopes, permission tables, actor resolution, rate limiting, RSS policing | Intentionally not — this is the security boundary |
| Enforcement | `host/rpc-host.ts` (~1330 lines) | `PluginRpcHost`: builds a per-plugin method map at spawn — a handler is registered **only if the plugin holds the unlocking permission** (registration = authorization). Depends only on the `HostDeps` interface | Neutral — decoupled by design |
| **Wiring** | `host/plugin-host-deps.factory.ts` (was `create-rpc-host.ts`) | Fills the ~130-member `HostDeps` object. Since Option A: an `@Injectable() PluginHostDepsFactory` with the DI-native domain services (`BudgetService`, `ReservationsService`, `TagsService`, `CategoriesService`, `TodoService`, `PluginOAuthService`) constructor-injected; legacy `services/*` domains stay plain function imports until their own migration. Raw `db`/`broadcast` also stay until injectable equivalents exist | **Yes (Option A)** — drains further per §3 |
| **Wiring** | `host/plugin-host-deps.factory.ts` (was `create-rpc-host.ts`) | Fills the ~130-member `HostDeps` object. Since Option A: an `@Injectable() PluginHostDepsFactory` with the DI-native domain services (`BudgetService`, `ReservationsService`, `TagsService`, `CategoriesService`, `TodoService`, `PluginOAuthService`) constructor-injected, fourteen domains as of 2026-07; legacy `services/*` domains stay plain function imports until their own migration. Raw `db`/`broadcast` also stay until injectable equivalents exist | **Yes (Option A)** — drains further per §3 |
| Host state | `host/plugin-host-state.ts` | Deliberately module-level (NOT a provider): the per-plugin data-DB map (`getPluginDataDb`/`closePluginDataDb`) and daily broker budgets (`budgetFor`/`pluginBudgetUsage`). Must be one shared instance across host recreations, and is read by `PluginsService`, which the factory imports from — folding it into the injectable would create a provider cycle | Intentionally not |
Key fact that made Option A cheap: the wiring is **called from inside a Nest
@@ -109,9 +109,10 @@ fileService swapped in 2026-07 — its ten imported symbols became the injected
`FilesService` plus the load-time constants from `files.constants.ts`;
collabService swapped in 2026-07 — its seven imported symbols became the
injected `CollabService`, completing Wave 3; vacayService swapped in 2026-07 —
its four imported symbols became the injected `VacayService`; the remaining
factory imports are all Wave-4+ domains: trips, places, days, journeys, atlas,
collections).
its four imported symbols became the injected `VacayService`; dayService
swapped in 2026-07 — its eleven imported symbols (the heaviest single import)
became the injected `DaysService`; the remaining factory imports are all
Wave-4+ domains: trips, places, journeys, atlas, collections).
### Test impact (as landed)
@@ -119,7 +120,7 @@ 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 (eleven stubs as of the 2026-07 vacay migration); the ~24
stubs (fourteen stubs as of the 2026-07 day migration); the ~23
legacy-service path mocks remain; a file-local shim keeps the
historical `createRealRpcHost(id, granted)` call sites and supplies a default
no-op router.
@@ -233,8 +234,9 @@ none of it is throwaway.
done 2026-07 with no factory impact — it was never imported here;
`reservationService` done 2026-07, draining the factory's last
plain-function reservation import — `notifyBookingChange` became a private
method over the already-injected `ReservationsService`; next per
`src/nest/README.md`): migrate the
method over the already-injected `ReservationsService`; `dayService` done
2026-07, the heaviest factory import — eleven symbols became the injected
`DaysService`; next per `src/nest/README.md`): migrate the
service to DI as usual; the factory swaps one legacy import for one
injected service. Optionally pilot `tags.rpc.ts` here — tags is small and
already fully DI on HTTP + MCP.
@@ -17,7 +17,6 @@ import { randomUUID } from 'node:crypto';
import { checkPermission } from '../../../services/permissions';
import { listTrips, updateTrip, createTrip, removeMember as removeTripMemberSvc, NotFoundError, ValidationError } from '../../../services/tripService';
import { createPlace, updatePlace, deletePlace } from '../../../services/placeService';
import { createDay, getDay, updateDay, deleteDay, listDays, listAccommodations, validateAccommodationRefs, createAccommodation as createAccommodationSvc, getAccommodation, updateAccommodation as updateAccommodationSvc, deleteAccommodation as deleteAccommodationSvc } from '../../../services/dayService';
import { isAddonEnabled } from '../../../services/adminService';
import { isDemoEmail } from '../../../services/demo';
import { ADDON_IDS } from '../../../addons';
@@ -31,6 +30,7 @@ import { CategoriesService } from '../../categories/categories.service';
import { TodoService } from '../../todo/todo.service';
import { PackingService } from '../../packing/packing.service';
import { DayNotesService } from '../../days/day-notes.service';
import { DaysService } from '../../days/days.service';
import { AssignmentsService } from '../../assignments/assignments.service';
import { LlmConfigResolver } from '../../llm-parse/llm-config.resolver';
import { DatabaseService } from '../../database/database.service';
@@ -154,6 +154,7 @@ export class PluginHostDepsFactory {
private readonly files: FilesService,
private readonly collab: CollabService,
private readonly vacay: VacayService,
private readonly days: DaysService,
) {}
/**
@@ -479,21 +480,21 @@ export class PluginHostDepsFactory {
canEditDays: (tripId, userId) => this.canEditTripAs('day_edit', tripId, userId),
createDay: (tripId, input) => {
const i = input as { date?: string; notes?: string };
const day = createDay(tripId, i.date, i.notes);
const day = this.days.create(tripId, i.date, i.notes);
broadcast(tripId, 'day:created', { day });
return day;
},
updateDay: (tripId, dayId, input) => {
const current = getDay(dayId, tripId);
const current = this.days.getDay(dayId, tripId);
if (!current) throw new ForbiddenResource(`no day ${dayId} on trip ${tripId}`);
const day = updateDay(dayId, current, input as { notes?: string; title?: string | null });
const day = this.days.update(dayId, current, input as { notes?: string; title?: string | null });
broadcast(tripId, 'day:updated', { day });
return day;
},
deleteDay: (tripId, dayId) => {
const current = getDay(dayId, tripId);
const current = this.days.getDay(dayId, tripId);
if (!current) throw new ForbiddenResource(`no day ${dayId} on trip ${tripId}`);
deleteDay(dayId);
this.days.remove(dayId);
broadcast(tripId, 'day:deleted', { dayId });
return { deleted: true };
},
@@ -567,10 +568,10 @@ export class PluginHostDepsFactory {
},
// --- Trip-scoped hydrated reads (membership already checked by tripRead). Same
// services as the REST GETs, so plugins see the exact planner shapes. ---
listTripDays: (tripId) => (listDays(tripId) as { days: unknown[] }).days,
listTripDays: (tripId) => (this.days.list(tripId) as { days: unknown[] }).days,
listTripReservations: (tripId) => this.reservations.list(String(tripId)),
listTripAccommodations: (tripId) => listAccommodations(tripId) as unknown[],
// --- Accommodations (lodging blocks, day_edit). Delegates to dayService so the
listTripAccommodations: (tripId) => this.days.listAccommodations(tripId) as unknown[],
// --- Accommodations (lodging blocks, day_edit). Delegates to DaysService so the
// partner hotel reservation, the metadata sync and the delete cascade behave
// exactly like the accommodations REST controller, cascade broadcasts included. ---
createAccommodation: (tripId, input) => {
@@ -579,9 +580,9 @@ export class PluginHostDepsFactory {
const startDayId = Math.trunc(Number(i.start_day_id));
const endDayId = Math.trunc(Number(i.end_day_id));
if (!placeId || !startDayId || !endDayId) throw new BadParams('place_id, start_day_id, and end_day_id are required');
const errors = validateAccommodationRefs(tripId, placeId, startDayId, endDayId);
const errors = this.days.validateAccommodationRefs(tripId, placeId, startDayId, endDayId);
if (errors.length > 0) throw new ForbiddenResource(errors[0].message);
const accommodation = createAccommodationSvc(tripId, {
const accommodation = this.days.createAccommodation(tripId, {
place_id: placeId, start_day_id: startDayId, end_day_id: endDayId,
check_in: i.check_in ?? undefined, check_in_end: i.check_in_end ?? undefined,
check_out: i.check_out ?? undefined, confirmation: i.confirmation ?? undefined, notes: i.notes ?? undefined,
@@ -591,18 +592,18 @@ export class PluginHostDepsFactory {
return accommodation;
},
updateAccommodation: (tripId, accommodationId, input) => {
const existing = getAccommodation(accommodationId, tripId);
const existing = this.days.getAccommodation(accommodationId, tripId);
if (!existing) throw new ForbiddenResource(`no accommodation ${accommodationId} on trip ${tripId}`);
const i = input as { place_id?: number; start_day_id?: number; end_day_id?: number; check_in?: string; check_in_end?: string; check_out?: string; confirmation?: string; notes?: string };
const errors = validateAccommodationRefs(tripId, i.place_id, i.start_day_id, i.end_day_id);
const errors = this.days.validateAccommodationRefs(tripId, i.place_id, i.start_day_id, i.end_day_id);
if (errors.length > 0) throw new ForbiddenResource(errors[0].message);
const accommodation = updateAccommodationSvc(accommodationId, existing, i);
const accommodation = this.days.updateAccommodation(accommodationId, existing, i);
broadcast(tripId, 'accommodation:updated', { accommodation });
return accommodation;
},
deleteAccommodation: (tripId, accommodationId) => {
if (!getAccommodation(accommodationId, tripId)) throw new ForbiddenResource(`no accommodation ${accommodationId} on trip ${tripId}`);
const { linkedReservationId, deletedBudgetItemId } = deleteAccommodationSvc(accommodationId);
if (!this.days.getAccommodation(accommodationId, tripId)) throw new ForbiddenResource(`no accommodation ${accommodationId} on trip ${tripId}`);
const { linkedReservationId, deletedBudgetItemId } = this.days.deleteAccommodation(accommodationId);
if (linkedReservationId) broadcast(tripId, 'reservation:deleted', { reservationId: linkedReservationId });
if (deletedBudgetItemId) broadcast(tripId, 'budget:deleted', { itemId: deletedBudgetItemId });
broadcast(tripId, 'accommodation:deleted', { accommodationId });