refactor(plugins): wire PluginHostDepsFactory through DI, drop tags/categories bridges

This commit is contained in:
jubnl
2026-07-26 15:28:55 +02:00
parent 167db04d69
commit 811230fa5d
21 changed files with 197 additions and 226 deletions
+2
View File
@@ -6,5 +6,7 @@ import { BudgetService } from './budget.service';
@Module({
controllers: [BudgetController],
providers: [BudgetService],
// For in-container consumers (PluginHostDepsFactory).
exports: [BudgetService],
})
export class BudgetModule {}
@@ -1,21 +0,0 @@
import type { Category } from '@trek/shared';
import { db } from '../../db/database';
import { DatabaseService } from '../database/database.service';
import { CategoriesService } from './categories.service';
/**
* Non-Nest entry point for the categories domain — for code running OUTSIDE
* the Nest container (currently only the plugin RPC host; the MCP tool and
* resource moved to the DI-discovered categories.mcp.ts). Exports only the
* legacy services/categoryService names still consumed outside the container,
* 1:1, so repointing a consumer is an import-path-only diff. Inside the
* container, inject CategoriesService instead.
*
* Module-level construction is safe: `db` is the reinitialize-proof Proxy onto
* the shared better-sqlite3 singleton (same pattern as tags.bridge.ts).
*/
const categories = new CategoriesService(new DatabaseService(db));
export function listCategories(): Category[] {
return categories.list();
}
@@ -7,5 +7,7 @@ import { CategoriesService } from './categories.service';
@Module({
controllers: [CategoriesController],
providers: [CategoriesService, CategoriesMcp],
// For in-container consumers (PluginHostDepsFactory).
exports: [CategoriesService],
})
export class CategoriesModule {}
@@ -5,8 +5,8 @@ import { DatabaseService } from '../database/database.service';
/**
* Categories domain service — owns the category SQL (moved 1:1 from the legacy
* services/categoryService.ts: identical statements, the #6366f1/📍 defaults
* and the COALESCE update semantics). Non-Nest consumers (plugin RPC host) go
* through categories.bridge.ts instead of importing this class directly.
* and the COALESCE update semantics). All consumers are in-container now — the
* plugin RPC host injects this class via PluginHostDepsFactory.
*/
@Injectable()
export class CategoriesService {
@@ -14,7 +14,8 @@ import { decrypt_api_key } from '../../services/apiKeyCrypto';
import { PluginSupervisor, type PluginRouteInfo } from './supervisor/plugin-supervisor';
import fs from 'node:fs';
import path from 'node:path';
import { createRealRpcHost, closePluginDataDb } from './host/create-rpc-host';
import { PluginHostDepsFactory } from './host/plugin-host-deps.factory';
import { closePluginDataDb } from './host/plugin-host-state';
import { ForbiddenResource } from './host/rpc-host';
import { removePluginData } from './host/plugin-data.service';
import { isKnownPermission } from './protocol/envelope';
@@ -130,8 +131,13 @@ export class PluginDependencyError extends Error {
export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy {
// The rpc-host factory is bound to `this` as the inter-plugin router, so a
// plugin's ctx.plugins.call / ctx.events.emit resolve through callPlugin/
// emitPluginEvent below (which own the dependency-edge authorization).
private readonly supervisor = new PluginSupervisor((id, granted) => createRealRpcHost(id, granted, this), {
// emitPluginEvent below (which own the dependency-edge authorization). The
// arrow reads this.hostDeps lazily at spawn time, so the field-initializer
// ordering (it runs before the constructor params are assigned) is safe.
private readonly supervisor = new PluginSupervisor((id, granted) => {
if (!this.hostDeps) throw new Error('PluginHostDepsFactory not provided — tests that activate plugins must pass one');
return this.hostDeps.create(id, granted, this);
}, {
// Both hooks run from child lifecycle EventEmitter callbacks (exit / stderr 'data'),
// so a throw here becomes an uncaughtException that has no host-side handler. During a
// restore the core DB is briefly CLOSED (closeDb → the this.db proxy throws on access), so a
@@ -163,12 +169,13 @@ export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy {
// Coalesces overlapping erasure drains (the sweep and enqueue both trigger one).
private drainInFlight: Promise<void> | null = null;
// The registry stays optional at the type level so tests can construct the
// service without one; Nest always injects the real one (the provider is in
// the module).
// The registry and host-deps factory stay optional at the type level so tests
// can construct the service without them; Nest always injects the real ones
// (both providers are in the module).
constructor(
private readonly dbs: DatabaseService,
private readonly registry?: PluginRegistryService,
private readonly hostDeps?: PluginHostDepsFactory,
) {}
private get db() {
+10 -1
View File
@@ -23,6 +23,12 @@ import { PluginOAuthService } from './plugin-oauth.service';
import { PluginsService } from './plugins.service';
import { PluginRuntimeService } from './plugin-runtime.service';
import { PluginRegistryService } from './registry/registry.service';
import { PluginHostDepsFactory } from './host/plugin-host-deps.factory';
import { TagsModule } from '../tags/tags.module';
import { CategoriesModule } from '../categories/categories.module';
import { BudgetModule } from '../budget/budget.module';
import { ReservationsModule } from '../reservations/reservations.module';
import { TodoModule } from '../todo/todo.module';
/**
* Plugin system (#plugins). M0 read side + M2 isolated runtime + M3 frontend:
@@ -32,8 +38,11 @@ import { PluginRegistryService } from './registry/registry.service';
* widget assets at /plugin-frame/:id/*.
*/
@Module({
// The DI-native domain services the plugin host wiring injects
// (PluginHostDepsFactory); DatabaseModule is @Global, so not listed.
imports: [TagsModule, CategoriesModule, BudgetModule, ReservationsModule, TodoModule],
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],
providers: [PluginsService, PluginRuntimeService, PluginRegistryService, PluginOAuthService, PluginHostDepsFactory],
// Exported so the admin addon-toggle handler can cascade-disable plugins whose
// required addon was just turned off (#plugins dependencies).
exports: [PluginRuntimeService],
+1 -1
View File
@@ -6,7 +6,7 @@ import { devLinkEnabled } from './dev-link';
import { maybe_encrypt_api_key, decrypt_api_key } from '../../services/apiKeyCrypto';
import { readAudit } from './host/plugin-audit';
import { keyFingerprint } from './signature-status';
import { pluginBudgetUsage } from './host/create-rpc-host';
import { pluginBudgetUsage } from './host/plugin-host-state';
import { isAddonEnabled } from '../../services/adminService';
import { parseDependencies, disabledRequiredAddons, resolveDependencyState, type PluginDepRow, type PluginDependencies, type VersionMismatch } from './dependencies';
import { hostSatisfies, hostVersion } from './install/host-compat';
@@ -13,5 +13,7 @@ import { UpcomingReservationsController } from './upcoming-reservations.controll
@Module({
controllers: [ReservationsController, AccommodationsController, UpcomingReservationsController],
providers: [ReservationsService, AccommodationsService],
// For in-container consumers (PluginHostDepsFactory).
exports: [ReservationsService],
})
export class ReservationsModule {}
-37
View File
@@ -1,37 +0,0 @@
import type { Tag } from '@trek/shared';
import { db } from '../../db/database';
import { DatabaseService } from '../database/database.service';
import { TagsService } from './tags.service';
/**
* Non-Nest entry point for the tags domain — for code running OUTSIDE the Nest
* container (currently only the plugin RPC host; the MCP tools moved to the
* DI-discovered tags.mcp.ts). Exports the legacy services/tagService function
* names 1:1 so repointing a consumer is an import-path-only diff. Inside the
* container, inject TagsService instead.
*
* Module-level construction is safe: `db` is the reinitialize-proof Proxy onto
* the shared better-sqlite3 singleton (same pattern as
* plugins/host/create-rpc-host.ts).
*/
const tags = new TagsService(new DatabaseService(db));
export function listTags(userId: number): Tag[] {
return tags.list(userId);
}
export function createTag(userId: number, name: string, color?: string): Tag {
return tags.create(userId, name, color);
}
export function getTagByIdAndUser(tagId: number | string, userId: number): Tag | undefined {
return tags.getByIdAndUser(tagId, userId);
}
export function updateTag(tagId: number | string, name?: string, color?: string): Tag | undefined {
return tags.update(tagId, name, color);
}
export function deleteTag(tagId: number | string): void {
tags.remove(tagId);
}
+2
View File
@@ -7,5 +7,7 @@ import { TagsService } from './tags.service';
@Module({
controllers: [TagsController],
providers: [TagsService, TagsMcp],
// For in-container consumers (PluginHostDepsFactory).
exports: [TagsService],
})
export class TagsModule {}
+2 -2
View File
@@ -5,8 +5,8 @@ import { DatabaseService } from '../database/database.service';
/**
* Tags domain service — owns the tag SQL (moved 1:1 from the legacy
* services/tagService.ts: identical statements, the #10b981 default and the
* COALESCE update semantics). Non-Nest consumers (MCP tools, plugin RPC host)
* go through tags.bridge.ts instead of importing this class directly.
* COALESCE update semantics). All consumers are in-container now — the plugin
* RPC host injects this class via PluginHostDepsFactory.
*/
@Injectable()
export class TagsService {
+8 -7
View File
@@ -4,15 +4,16 @@ import { TodoService } from './todo.service';
/**
* Non-Nest entry point for the todo domain — for code running OUTSIDE the
* Nest container (the plugin RPC host and the legacy get_trip_summary
* registrar in src/mcp/tools/trips.ts; the todo MCP tools and resource moved
* to the DI-discovered todo.mcp.ts). Exports only the legacy
* services/todoService names still consumed outside the container, 1:1, so
* repointing a consumer is an import-path-only diff. Inside the container,
* inject TodoService instead.
* Nest container (the legacy get_trip_summary registrar in
* src/mcp/tools/trips.ts; the todo MCP tools and resource moved to the
* DI-discovered todo.mcp.ts, and the plugin RPC host injects TodoService via
* PluginHostDepsFactory). Exports only the legacy services/todoService names
* still consumed outside the container, 1:1, so repointing a consumer is an
* import-path-only diff. Inside the container, inject TodoService instead.
* Delete this file when the legacy MCP trips registrar migrates.
*
* Module-level construction is safe: `db` is the reinitialize-proof Proxy onto
* the shared better-sqlite3 singleton (same pattern as categories.bridge.ts).
* the shared better-sqlite3 singleton.
*/
const todos = new TodoService(new DatabaseService(db));