refactor(plugins): route injectable-class SQL through the injected DatabaseService

PluginRuntimeService's operatorEgressHosts/pruneErrorLog and PluginsService's
egressHostCount were module-level helpers on the raw db proxy; they fold into
their classes as methods over the injected service. (PluginHostDepsFactory's
equivalent conversion rode the settings commit — same files.) No injectable
class under src/nest imports the db global anymore; the remaining importers
are the documented module-level seams (bridges, platform shell,
plugin-host-state, signature-status, the bare plugin user-setting readers).
This commit is contained in:
jubnl
2026-07-26 22:24:24 +02:00
parent e0530680cb
commit 4a7d9cd0be
2 changed files with 28 additions and 32 deletions
@@ -1,6 +1,5 @@
import { Injectable, type OnModuleInit, type OnModuleDestroy } from '@nestjs/common';
import semver from 'semver';
import { db } from '../../db/database';
import { DatabaseService } from '../database/database.service';
import { pluginsEnabled } from './kill-switch';
import { setPluginEventSink } from '../../plugin-event-sink';
@@ -40,15 +39,6 @@ const HTTP_OUTBOUND = 'http:outbound:';
// any embedded space — the string is interpolated into the egress guard and the CSP.
const EGRESS_HOST_RE = /^(\*\.[a-z0-9-]+(\.[a-z0-9-]+)+|[a-z0-9-]+(\.[a-z0-9-]+)*)$/i;
/** Hosts an admin added post-install for a plugin that declared `operatorEgress`. */
function operatorEgressHosts(id: string): string[] {
try {
return (db.prepare('SELECT host FROM plugin_egress_hosts WHERE plugin_id = ? ORDER BY host').all(id) as Array<{ host: string }>)
.map((r) => r.host);
} catch {
return []; // table absent (a slimmed test app) — never block activation
}
}
/**
* Remove `<plugins>/<id>` whether it is a real directory, a POSIX symlink or a
@@ -155,7 +145,7 @@ export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy {
// Retention: a crash-looping plugin emits a stderr line per restart, so an
// uncapped table grows without bound in the shared trek.db. Keep only the
// most recent LOG_RETENTION rows per plugin (the admin view shows 200).
pruneErrorLog(id);
this.pruneErrorLog(id);
} catch { /* DB unavailable — a log line must never crash the host */ }
},
});
@@ -555,13 +545,18 @@ export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy {
// still bounds what is possible — and it is always the admin, never an end user,
// who widens it. The egress list is spawn-time only, which is why changing it
// re-spawns the plugin (see setOperatorEgressHosts).
const egress = [...new Set([...manifestHosts, ...operatorEgressHosts(id)])];
const egress = [...new Set([...manifestHosts, ...this.operatorEgressHosts(id)])];
await this.supervisor.activate(id, new Set(declared), config, egress);
}
/** Hosts an admin added for this plugin (empty unless it declared `operatorEgress`). */
operatorEgressHosts(id: string): string[] {
return operatorEgressHosts(id);
try {
return (this.db.prepare('SELECT host FROM plugin_egress_hosts WHERE plugin_id = ? ORDER BY host').all(id) as Array<{ host: string }>)
.map((r) => r.host);
} catch {
return []; // table absent (a slimmed test app) — never block activation
}
}
/** Does this plugin's manifest declare that it needs operator-supplied hosts? */
@@ -1123,6 +1118,16 @@ export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy {
return [];
}
}
/** Trim a plugin's error log to the most recent LOG_RETENTION rows. Cheap: onLog
* only fires on warn/error, so this never runs on the hot path. */
private pruneErrorLog(pluginId: string): void {
this.db.prepare(
`DELETE FROM plugin_error_log WHERE plugin_id = ? AND id NOT IN (
SELECT id FROM plugin_error_log WHERE plugin_id = ? ORDER BY id DESC LIMIT ${LOG_RETENTION}
)`,
).run(pluginId, pluginId);
}
}
function parseArray(json: string): string[] {
@@ -1151,12 +1156,3 @@ function decryptConfig(config: Record<string, unknown>): Record<string, unknown>
}
const LOG_RETENTION = 500; // rows kept per plugin (the admin view shows the newest 200)
/** Trim a plugin's error log to the most recent LOG_RETENTION rows. Cheap: onLog
* only fires on warn/error, so this never runs on the hot path. */
function pruneErrorLog(pluginId: string): void {
db.prepare(
`DELETE FROM plugin_error_log WHERE plugin_id = ? AND id NOT IN (
SELECT id FROM plugin_error_log WHERE plugin_id = ? ORDER BY id DESC LIMIT ${LOG_RETENTION}
)`,
).run(pluginId, pluginId);
}
+10 -10
View File
@@ -45,15 +45,6 @@ interface PluginRawRow {
update_block_version: string | null;
}
/** Hosts an admin has added for a plugin (0 unless it declared operatorEgress). */
function egressHostCount(id: string): number {
try {
return (db.prepare('SELECT COUNT(*) AS n FROM plugin_egress_hosts WHERE plugin_id = ?').get(id) as { n: number }).n;
} catch {
return 0; // table absent (a slimmed test app)
}
}
export interface PluginListItem {
id: string;
name: string;
@@ -106,6 +97,15 @@ export class PluginsService {
return this.dbs.connection;
}
/** Hosts an admin has added for a plugin (0 unless it declared operatorEgress). */
private egressHostCount(id: string): number {
try {
return (this.db.prepare('SELECT COUNT(*) AS n FROM plugin_egress_hosts WHERE plugin_id = ?').get(id) as { n: number }).n;
} catch {
return 0; // table absent (a slimmed test app)
}
}
list(): { enabled: boolean; devLink: boolean; plugins: PluginListItem[] } {
const rows = this.db
.prepare(
@@ -145,7 +145,7 @@ export class PluginsService {
return {
...rest,
operatorEgress: _oe === 1,
egressHostCount: egressHostCount(r.id),
egressHostCount: this.egressHostCount(r.id),
dependencies: deps,
dependencyStatus,
trekRange: trek_range,