Files
Termix/src/backend/database/repositories/factory.ts
T
LukeGus 248698f62d feat: add plugin worker protocol, bootstrap and plugin_storage table
Worker-side ctx proxy plus the message envelope it talks over, and the
per-plugin key/value table behind ctx.storage with migrations for all
three dialects. Rows cascade with the plugin so uninstalling leaves
nothing behind.
2026-09-21 07:19:01 -05:00

674 lines
25 KiB
TypeScript

import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js";
import { getDb, getSqlite } from "../db/index.js";
import { needsExplicitPersist, resolveDatabaseDialect } from "../db/dialect.js";
import { primeSettingsCache, readCachedSetting } from "./settings-cache.js";
import type { DatabaseContext } from "./database-context.js";
import { WebauthnCredentialRepository } from "./webauthn-credential-repository.js";
import { AiRepository } from "./ai-repository.js";
import { AlertRepository } from "./alert-repository.js";
import { AutomationRepository } from "./automation-repository.js";
import { ApiKeyRepository } from "./api-key-repository.js";
import { AuditLogRepository } from "./audit-log-repository.js";
import { C2sTunnelPresetRepository } from "./c2s-tunnel-preset-repository.js";
import { CommandHistoryRepository } from "./command-history-repository.js";
import { CredentialRepository } from "./credential-repository.js";
import { DashboardServiceLinkRepository } from "./dashboard-service-link-repository.js";
import { DismissedAlertRepository } from "./dismissed-alert-repository.js";
import { FileManagerBookmarkRepository } from "./file-manager-bookmark-repository.js";
import { FleetRepository } from "./fleet-repository.js";
import { FleetInventoryRepository } from "./fleet-inventory-repository.js";
import { HomepageItemRepository } from "./homepage-item-repository.js";
import { HomepageLayoutRepository } from "./homepage-layout-repository.js";
import { HostFolderRepository } from "./host-folder-repository.js";
import { HostHealthRepository } from "./host-health-repository.js";
import { HostMetricsHistoryRepository } from "./host-metrics-history-repository.js";
import { HostMetricsPreferenceRepository } from "./host-metrics-preference-repository.js";
import { ProxmoxNodeHistoryRepository } from "./proxmox-node-history-repository.js";
import { HostRepository } from "./host-repository.js";
import { HostResolutionRepository } from "./host-resolution-repository.js";
import { HostSidebarPreferenceRepository } from "./host-sidebar-preference-repository.js";
import { CredentialSidebarPreferenceRepository } from "./credential-sidebar-preference-repository.js";
import { UiPreferenceRepository } from "./ui-preference-repository.js";
import { NetworkTopologyRepository } from "./network-topology-repository.js";
import { OpenTabRepository } from "./open-tab-repository.js";
import { OpksshTokenRepository } from "./opkssh-token-repository.js";
import { PluginRepository } from "./plugin-repository.js";
import { PluginStorageRepository } from "./plugin-storage-repository.js";
import { PluginPermissionGrantRepository } from "./plugin-permission-grant-repository.js";
import { PluginRegistryRepository } from "./plugin-registry-repository.js";
import { PluginInstallCountRepository } from "./plugin-install-count-repository.js";
import { RbacAccessRepository } from "./rbac-access-repository.js";
import { RecentActivityRepository } from "./recent-activity-repository.js";
import { RoleRepository } from "./role-repository.js";
import { SessionRecordingRepository } from "./session-recording-repository.js";
import { SessionRepository } from "./session-repository.js";
import { SessionShareRepository } from "./session-share-repository.js";
import { CollabRoomRepository } from "./collab-room-repository.js";
import { SecretSourceRepository } from "./secret-source-repository.js";
import { CredentialAccessRepository } from "./credential-access-repository.js";
import { SharedCredentialSecretsRepository } from "./shared-credential-secrets-repository.js";
import { FolderAccessRepository } from "./folder-access-repository.js";
import { SettingsRepository } from "./settings-repository.js";
import { SharedHostAuthOverrideRepository } from "./shared-host-auth-override-repository.js";
import { SharedHostSecretsRepository } from "./shared-host-secrets-repository.js";
import { SnippetRepository } from "./snippet-repository.js";
import { SshCredentialUsageRepository } from "./ssh-credential-usage-repository.js";
import { SyncTombstoneRepository } from "./sync-tombstone-repository.js";
import { SsoProviderRepository } from "./sso-provider-repository.js";
import { TermixIdentityCaRepository } from "./termix-identity-ca-repository.js";
import { TermixIdentityRepository } from "./termix-identity-repository.js";
import { TmuxSessionTagRepository } from "./tmux-session-tag-repository.js";
import { TransferRecentRepository } from "./transfer-recent-repository.js";
import { TrustedDeviceRepository } from "./trusted-device-repository.js";
import { UserDataExportRepository } from "./user-data-export-repository.js";
import { UserPreferenceRepository } from "./user-preference-repository.js";
import { UserRepository } from "./user-repository.js";
import { VaultProfileRepository } from "./vault-profile-repository.js";
import { VaultTokenRepository } from "./vault-token-repository.js";
import { WorkspaceRepository } from "./workspace-repository.js";
/**
* The context every repository runs against.
*
* The dialect has to be resolved, not assumed: it is what `returning.ts` reads
* to decide whether it can ask for RETURNING, and whether an upsert spells
* itself `onConflictDoUpdate` or `onDuplicateKeyUpdate`. Reporting "sqlite"
* while connected to MySQL makes the second of those a TypeError on the first
* write.
*
* Both cross-dialect harnesses build a DatabaseContext themselves, so neither
* exercises this function — see tests/database/repositories/factory-context.
*/
export function createCurrentRepositoryContext(): DatabaseContext {
return {
dialect: resolveDatabaseDialect(),
drizzle: getDb(),
};
}
/**
* Post-write hook handed to every repository.
*
* Only meaningful for SQLite, where the database lives in memory and has to be
* serialised back to its encrypted file. On Postgres and MySQL the write is
* already durable, so no hook is installed at all rather than one that does
* nothing — repositories call it as `this.onWrite?.()`.
*/
export function createCurrentRepositoryWriteHook(
reason: string,
): (() => Promise<void>) | undefined {
if (!needsExplicitPersist(resolveDatabaseDialect())) return undefined;
return () => DatabaseSaveTrigger.forceSave(reason);
}
/**
* Post-write hook for high-frequency, non-critical writes (telemetry
* inserts/cleanup, informational timestamp touches).
*
* Marks the in-memory database dirty and lets DatabaseSaveTrigger's existing
* debounce coalesce the actual serialize+encrypt, instead of forcing one on
* every single sample. A lost 2-second window of telemetry on crash is
* acceptable; blocking the event loop that serves SSH traffic on every metric
* sample is not.
*/
export function createCurrentRepositoryLazyWriteHook(
reason: string,
): (() => Promise<void>) | undefined {
if (!needsExplicitPersist(resolveDatabaseDialect())) return undefined;
return () => DatabaseSaveTrigger.triggerSave(reason);
}
/**
* Raw driver handle for the few synchronous call sites that cannot await —
* getCurrentSettingValue below, and settings reads during startup. Repositories
* must not use this: they take a DatabaseContext, which is drizzle-only.
* Porting to another engine means giving these callers an async path first.
*/
export function getCurrentRepositorySqlite() {
return getSqlite();
}
/**
* Synchronous settings read.
*
* SQLite can be queried synchronously, so it is read directly and stays
* authoritative. Other engines have no synchronous query, so the value comes
* from the cache primed at startup and kept current by SettingsRepository.
*/
export function getCurrentSettingValue(key: string): string | null {
if (!needsExplicitPersist(resolveDatabaseDialect())) {
return readCachedSetting(key);
}
const row = getCurrentRepositorySqlite()
.prepare("SELECT value FROM settings WHERE key = ?")
.get(key) as { value?: string } | undefined;
return row?.value ?? null;
}
export function createCurrentWebauthnCredentialRepository(): WebauthnCredentialRepository {
return new WebauthnCredentialRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("webauthn_credential_repository_write"),
);
}
export function createCurrentAiRepository(): AiRepository {
return new AiRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("ai_repository_write"),
);
}
export function createCurrentAlertRepository(): AlertRepository {
return new AlertRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("alert_repository_write"),
);
}
export function createCurrentAutomationRepository(): AutomationRepository {
return new AutomationRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("automation_repository_write"),
);
}
export function createCurrentApiKeyRepository(): ApiKeyRepository {
return new ApiKeyRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("api_key_repository_write"),
);
}
export function createCurrentAuditLogRepository(): AuditLogRepository {
return new AuditLogRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("audit_log_repository_write"),
);
}
export function createCurrentC2sTunnelPresetRepository(): C2sTunnelPresetRepository {
return new C2sTunnelPresetRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("c2s_tunnel_preset_repository_write"),
);
}
export function createCurrentCommandHistoryRepository(): CommandHistoryRepository {
return new CommandHistoryRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("command_history_repository_write"),
);
}
export function createCurrentCredentialRepository(): CredentialRepository {
return new CredentialRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("credential_repository_write"),
);
}
export function createCurrentDashboardServiceLinkRepository(): DashboardServiceLinkRepository {
return new DashboardServiceLinkRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("dashboard_service_link_repository_write"),
);
}
export function createCurrentSyncTombstoneRepository(): SyncTombstoneRepository {
return new SyncTombstoneRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("sync_tombstone_repository_write"),
);
}
export function createCurrentDismissedAlertRepository(): DismissedAlertRepository {
return new DismissedAlertRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("dismissed_alert_repository_write"),
);
}
export function createCurrentFileManagerBookmarkRepository(): FileManagerBookmarkRepository {
return new FileManagerBookmarkRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("file_manager_bookmarks_repository_write"),
);
}
export function createCurrentFleetRepository(): FleetRepository {
return new FleetRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("fleet_repository_write"),
);
}
export function createCurrentFleetInventoryRepository(): FleetInventoryRepository {
return new FleetInventoryRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("fleet_inventory_repository_write"),
);
}
export function createCurrentHomepageItemRepository(): HomepageItemRepository {
return new HomepageItemRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("homepage_item_repository_write"),
);
}
export function createCurrentHomepageLayoutRepository(): HomepageLayoutRepository {
return new HomepageLayoutRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("homepage_layout_repository_write"),
);
}
export function createCurrentHostFolderRepository(): HostFolderRepository {
return new HostFolderRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("host_folder_repository_write"),
);
}
export function createCurrentHostHealthRepository(): HostHealthRepository {
return new HostHealthRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("host_health_repository_write"),
);
}
export function createCurrentHostMetricsHistoryRepository(): HostMetricsHistoryRepository {
return new HostMetricsHistoryRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryLazyWriteHook(
"host_metrics_history_repository_write",
),
);
}
export function createCurrentHostMetricsPreferenceRepository(): HostMetricsPreferenceRepository {
return new HostMetricsPreferenceRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook(
"host_metrics_preference_repository_write",
),
);
}
export function createCurrentProxmoxNodeHistoryRepository(): ProxmoxNodeHistoryRepository {
return new ProxmoxNodeHistoryRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryLazyWriteHook(
"proxmox_node_history_repository_write",
),
);
}
export function createCurrentHostRepository(): HostRepository {
return new HostRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("host_repository_write"),
);
}
export function createCurrentHostResolutionRepository(): HostResolutionRepository {
return new HostResolutionRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("host_resolution_repository_write"),
createCurrentRepositoryLazyWriteHook(
"host_resolution_repository_lazy_write",
),
);
}
export function createCurrentHostSidebarPreferenceRepository(): HostSidebarPreferenceRepository {
return new HostSidebarPreferenceRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook(
"host_sidebar_preference_repository_write",
),
);
}
export function createCurrentCredentialSidebarPreferenceRepository(): CredentialSidebarPreferenceRepository {
return new CredentialSidebarPreferenceRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook(
"credential_sidebar_preference_repository_write",
),
);
}
export function createCurrentUiPreferenceRepository(): UiPreferenceRepository {
return new UiPreferenceRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("ui_preference_repository_write"),
);
}
export function createCurrentNetworkTopologyRepository(): NetworkTopologyRepository {
return new NetworkTopologyRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("network_topology_repository_write"),
);
}
export function createCurrentOpenTabRepository(): OpenTabRepository {
return new OpenTabRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("open_tab_repository_write"),
);
}
export function createCurrentOpksshTokenRepository(): OpksshTokenRepository {
return new OpksshTokenRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("opkssh_token_repository_write"),
);
}
export function createCurrentPluginRepository(): PluginRepository {
return new PluginRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("plugin_repository_write"),
);
}
export function createCurrentPluginPermissionGrantRepository(): PluginPermissionGrantRepository {
return new PluginPermissionGrantRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook(
"plugin_permission_grant_repository_write",
),
);
}
export function createCurrentPluginStorageRepository(): PluginStorageRepository {
return new PluginStorageRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("plugin_storage_repository_write"),
);
}
export function createCurrentPluginRegistryRepository(): PluginRegistryRepository {
return new PluginRegistryRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("plugin_registry_repository_write"),
);
}
export function createCurrentPluginInstallCountRepository(): PluginInstallCountRepository {
return new PluginInstallCountRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryLazyWriteHook(
"plugin_install_count_repository_write",
),
);
}
export function createCurrentRbacAccessRepository(): RbacAccessRepository {
return new RbacAccessRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("rbac_access_repository_write"),
);
}
export function createCurrentRecentActivityRepository(): RecentActivityRepository {
return new RecentActivityRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("recent_activity_repository_write"),
);
}
export function createCurrentRoleRepository(): RoleRepository {
return new RoleRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("role_repository_write"),
);
}
export function createCurrentSessionRecordingRepository(): SessionRecordingRepository {
return new SessionRecordingRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("session_recording_repository_write"),
);
}
export function createCurrentSessionRepository(): SessionRepository {
return new SessionRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("session_repository_write"),
);
}
export function createCurrentCollabRoomRepository(): CollabRoomRepository {
return new CollabRoomRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("collab_room_repository_write"),
);
}
export function createCurrentSessionShareRepository(): SessionShareRepository {
return new SessionShareRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("session_share_repository_write"),
);
}
export function createCurrentSettingsRepository(): SettingsRepository {
return new SettingsRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("settings_repository_write"),
);
}
export function createCurrentSharedHostSecretsRepository(): SharedHostSecretsRepository {
return new SharedHostSecretsRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("shared_host_secrets_repository_write"),
);
}
export function createCurrentSharedHostAuthOverrideRepository(): SharedHostAuthOverrideRepository {
return new SharedHostAuthOverrideRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook(
"shared_host_auth_override_repository_write",
),
);
}
export function createCurrentSnippetRepository(): SnippetRepository {
return new SnippetRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("snippet_repository_write"),
);
}
export function createCurrentSshCredentialUsageRepository(): SshCredentialUsageRepository {
return new SshCredentialUsageRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("ssh_credential_usage_repository_write"),
);
}
export function createCurrentSsoProviderRepository(): SsoProviderRepository {
return new SsoProviderRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("sso_provider_repository_write"),
);
}
export function createCurrentTermixIdentityCaRepository(): TermixIdentityCaRepository {
return new TermixIdentityCaRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("termix_identity_ca_repository_write"),
);
}
export function createCurrentTermixIdentityRepository(): TermixIdentityRepository {
return new TermixIdentityRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("termix_identity_repository_write"),
);
}
export function createCurrentTmuxSessionTagRepository(): TmuxSessionTagRepository {
return new TmuxSessionTagRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("tmux_session_tag_repository_write"),
);
}
export function createCurrentTransferRecentRepository(): TransferRecentRepository {
return new TransferRecentRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("transfer_recent_repository_write"),
);
}
export function createCurrentTrustedDeviceRepository(): TrustedDeviceRepository {
return new TrustedDeviceRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("trusted_device_repository_write"),
);
}
export function createCurrentUserDataExportRepository(): UserDataExportRepository {
return new UserDataExportRepository(createCurrentRepositoryContext());
}
export function createCurrentUserPreferenceRepository(): UserPreferenceRepository {
return new UserPreferenceRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("user_preference_repository_write"),
);
}
export function createCurrentUserRepository(): UserRepository {
return new UserRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("user_repository_write"),
);
}
export function createCurrentFolderAccessRepository(): FolderAccessRepository {
return new FolderAccessRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("folder_access_repository_write"),
);
}
export function createCurrentCredentialAccessRepository(): CredentialAccessRepository {
return new CredentialAccessRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("credential_access_repository_write"),
);
}
export function createCurrentSharedCredentialSecretsRepository(): SharedCredentialSecretsRepository {
return new SharedCredentialSecretsRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook(
"shared_credential_secrets_repository_write",
),
);
}
export function createCurrentSecretSourceRepository(): SecretSourceRepository {
return new SecretSourceRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("secret_source_repository_write"),
);
}
export function createCurrentVaultProfileRepository(): VaultProfileRepository {
return new VaultProfileRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("vault_profile_repository_write"),
);
}
export function createCurrentVaultTokenRepository(): VaultTokenRepository {
return new VaultTokenRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("vault_token_repository_write"),
);
}
export function createCurrentWorkspaceRepository(): WorkspaceRepository {
return new WorkspaceRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("workspace_repository_write"),
);
}
/**
* Loads the settings cache. Must run during startup on engines without a
* synchronous read, before anything calls getCurrentSettingValue.
*/
export async function primeCurrentSettingsCache(): Promise<void> {
const rows = await createCurrentSettingsRepository().listAll();
primeSettingsCache(rows);
}
/**
* How often a replica re-reads the settings table.
*
* Override with SETTINGS_CACHE_REFRESH_SECONDS; 0 disables the refresh.
*/
const REFRESH_SECONDS_ENV = "SETTINGS_CACHE_REFRESH_SECONDS";
const DEFAULT_REFRESH_SECONDS = 30;
let refreshTimer: NodeJS.Timeout | null = null;
/**
* Keeps the settings cache from drifting on a multi-replica deployment.
*
* The cache is per-process and updated in the process that writes. That is
* enough for SQLite, where there is only ever one process. On Postgres and
* MySQL — which exist here precisely so more than one instance can share the
* data — a setting changed on one replica would otherwise never reach the
* others, because the synchronous read has no way to go back to the database.
*
* Periodic re-priming does not make the value immediately consistent. It bounds
* how long it can be wrong, which is the difference between a setting that
* takes effect on the next tick and one that takes effect at the next restart.
*/
export function startSettingsCacheRefresh(
env = process.env,
refresh: () => Promise<void> = primeCurrentSettingsCache,
): void {
if (refreshTimer) return;
const seconds = refreshIntervalSeconds(env);
if (seconds === null) return;
refreshTimer = setInterval(() => {
void refresh().catch(() => {
// A failed refresh leaves the previous values in place, which is the
// right outcome: a transient database blip should not blank the cache.
// Every caller reads a missing setting as "use the default", so an empty
// cache would silently revert configuration across the deployment.
});
}, seconds * 1000);
refreshTimer.unref();
}
/** The configured interval, or null when refreshing is switched off. */
export function refreshIntervalSeconds(env = process.env): number | null {
const seconds = Number(env[REFRESH_SECONDS_ENV] ?? DEFAULT_REFRESH_SECONDS);
return Number.isFinite(seconds) && seconds > 0 ? seconds : null;
}
/** Test seam. */
export function stopSettingsCacheRefresh(): void {
if (!refreshTimer) return;
clearInterval(refreshTimer);
refreshTimer = null;
}