From ef4969dd356448d176d3f98a7cce5c6487a80f84 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Wed, 15 Jul 2026 23:56:02 -0500 Subject: [PATCH] refactor(db): collapse repository rollout scaffolding into single factory Repositories are now the only data path. Replaces the 41 current-*-repository wrapper files, the DATABASE_LAYER_REPOSITORY_ROLLOUT flag/alias map and the unused database/runtime adapter with repositories/factory.ts, a plain DatabaseContext type and an in-memory TestSqliteDatabase test harness. --- readme/DATABASE-LAYER-GRAY-ROLLOUT.md | 447 ------- readme/DATABASE-LAYER-PHASE-0-AUDIT.md | 548 -------- readme/DATABASE-LAYER-REFACTOR-PLAN.md | 1187 ----------------- src/backend/dashboard.ts | 10 +- src/backend/database/database.ts | 20 +- .../repositories/alert-repository.test.ts | 10 +- .../database/repositories/alert-repository.ts | 2 +- .../repositories/api-key-repository.test.ts | 10 +- .../repositories/api-key-repository.ts | 2 +- .../repositories/audit-log-repository.test.ts | 10 +- .../repositories/audit-log-repository.ts | 2 +- .../c2s-tunnel-preset-repository.test.ts | 10 +- .../c2s-tunnel-preset-repository.ts | 2 +- .../command-history-repository.test.ts | 10 +- .../command-history-repository.ts | 2 +- .../repositories/credential-repository.ts | 2 +- .../repositories/current-alert-repository.ts | 15 - .../current-api-key-repository.ts | 15 - .../current-audit-log-repository.ts | 15 - .../current-c2s-tunnel-preset-repository.ts | 15 - .../current-command-history-repository.ts | 15 - .../current-credential-repository.ts | 15 - ...rrent-dashboard-service-link-repository.ts | 15 - .../current-dismissed-alert-repository.ts | 15 - ...urrent-file-manager-bookmark-repository.ts | 15 - .../current-homepage-item-repository.ts | 15 - .../current-homepage-layout-repository.ts | 15 - .../current-host-folder-repository.ts | 15 - .../current-host-health-repository.ts | 15 - ...current-host-metrics-history-repository.ts | 15 - ...rent-host-metrics-preference-repository.ts | 17 - .../repositories/current-host-repository.ts | 15 - .../current-host-resolution-repository.ts | 15 - .../current-network-topology-repository.ts | 15 - .../current-open-tab-repository.ts | 15 - .../current-opkssh-token-repository.ts | 15 - .../current-rbac-access-repository.ts | 15 - .../current-recent-activity-repository.ts | 15 - .../current-repository-runtime.ts | 21 - .../repositories/current-role-repository.ts | 15 - .../current-session-recording-repository.ts | 15 - .../current-session-repository.ts | 15 - .../current-settings-repository.ts | 26 - .../current-shared-credential-repository.ts | 15 - .../current-snippet-repository.ts | 15 - ...current-ssh-credential-usage-repository.ts | 15 - .../current-sso-provider-repository.ts | 15 - .../current-termix-identity-ca-repository.ts | 15 - .../current-termix-identity-repository.ts | 15 - .../current-tmux-session-tag-repository.ts | 15 - .../current-transfer-recent-repository.ts | 15 - .../current-trusted-device-repository.ts | 15 - .../current-user-data-export-repository.ts | 9 - .../current-user-preference-repository.ts | 15 - .../repositories/current-user-repository.ts | 15 - .../current-vault-profile-repository.ts | 15 - .../current-vault-token-repository.ts | 15 - .../dashboard-service-link-repository.test.ts | 10 +- .../dashboard-service-link-repository.ts | 2 +- .../database/repositories/database-context.ts | 9 + .../dismissed-alert-repository.test.ts | 10 +- .../dismissed-alert-repository.ts | 2 +- src/backend/database/repositories/factory.ts | 348 +++++ .../file-manager-bookmark-repository.test.ts | 10 +- .../file-manager-bookmark-repository.ts | 2 +- .../homepage-item-repository.test.ts | 10 +- .../repositories/homepage-item-repository.ts | 2 +- .../homepage-layout-repository.test.ts | 10 +- .../homepage-layout-repository.ts | 2 +- .../host-credential-repositories.test.ts | 12 +- .../host-folder-repository.test.ts | 12 +- .../repositories/host-folder-repository.ts | 2 +- .../host-health-repository.test.ts | 10 +- .../repositories/host-health-repository.ts | 2 +- .../host-metrics-history-repository.test.ts | 10 +- .../host-metrics-history-repository.ts | 2 +- ...host-metrics-preference-repository.test.ts | 10 +- .../host-metrics-preference-repository.ts | 2 +- .../database/repositories/host-repository.ts | 2 +- .../host-resolution-repository.test.ts | 10 +- .../host-resolution-repository.ts | 2 +- .../network-topology-repository.test.ts | 10 +- .../network-topology-repository.ts | 2 +- .../repositories/open-tab-repository.test.ts | 10 +- .../repositories/open-tab-repository.ts | 2 +- .../opkssh-token-repository.test.ts | 10 +- .../repositories/opkssh-token-repository.ts | 2 +- .../rbac-access-repository.test.ts | 10 +- .../repositories/rbac-access-repository.ts | 2 +- .../recent-activity-repository.test.ts | 12 +- .../recent-activity-repository.ts | 2 +- .../repositories/repository-rollout.test.ts | 219 --- .../repositories/repository-rollout.ts | 381 ------ .../repositories/role-repository.test.ts | 10 +- .../database/repositories/role-repository.ts | 2 +- .../session-recording-repository.test.ts | 10 +- .../session-recording-repository.ts | 6 +- .../repositories/session-repository.ts | 2 +- .../repositories/settings-repository.test.ts | 10 +- .../repositories/settings-repository.ts | 2 +- .../shared-credential-repository.test.ts | 12 +- .../shared-credential-repository.ts | 2 +- .../repositories/snippet-repository.test.ts | 12 +- .../repositories/snippet-repository.ts | 2 +- .../sqlite-foreign-keys.ts} | 2 +- .../ssh-credential-usage-repository.test.ts | 10 +- .../ssh-credential-usage-repository.ts | 2 +- .../sso-provider-repository.test.ts | 12 +- .../repositories/sso-provider-repository.ts | 2 +- .../termix-identity-ca-repository.test.ts | 12 +- .../termix-identity-ca-repository.ts | 2 +- .../termix-identity-repository.test.ts | 10 +- .../termix-identity-repository.ts | 2 +- .../database/repositories/test-support.ts | 31 + .../tmux-session-tag-repository.test.ts | 10 +- .../tmux-session-tag-repository.ts | 2 +- .../transfer-recent-repository.test.ts | 10 +- .../transfer-recent-repository.ts | 2 +- .../trusted-device-repository.test.ts | 10 +- .../repositories/trusted-device-repository.ts | 2 +- .../user-data-export-repository.test.ts | 10 +- .../user-data-export-repository.ts | 2 +- .../user-preference-repository.test.ts | 10 +- .../user-preference-repository.ts | 2 +- .../database/repositories/user-repository.ts | 2 +- .../user-session-repositories.test.ts | 10 +- .../vault-profile-repository.test.ts | 10 +- .../repositories/vault-profile-repository.ts | 2 +- .../vault-token-repository.test.ts | 10 +- .../repositories/vault-token-repository.ts | 2 +- .../database/routes/acme-ssl-routes.ts | 6 +- .../database/routes/alert-rules-routes.ts | 2 +- src/backend/database/routes/alerts.ts | 2 +- .../database/routes/audit-log-routes.ts | 6 +- .../database/routes/c2s-tunnel-presets.ts | 2 +- .../routes/credential-deploy-routes.ts | 2 +- src/backend/database/routes/credentials.ts | 12 +- .../routes/dashboard-service-links-routes.ts | 2 +- .../database/routes/delete-user-data.ts | 74 +- .../database/routes/homepage-items-routes.ts | 2 +- .../database/routes/homepage-layout-routes.ts | 2 +- .../database/routes/host-autostart-routes.ts | 2 +- .../database/routes/host-bulk-routes.ts | 8 +- .../routes/host-command-history-routes.ts | 2 +- .../host-file-manager-bookmark-routes.ts | 2 +- .../database/routes/host-folder-routes.ts | 18 +- .../database/routes/host-internal-routes.ts | 2 +- .../database/routes/host-network-routes.ts | 2 +- src/backend/database/routes/host.ts | 26 +- .../database/routes/ldap-auth-routes.ts | 10 +- .../database/routes/network-topology.ts | 2 +- src/backend/database/routes/open-tabs.ts | 6 +- src/backend/database/routes/rbac.ts | 14 +- .../database/routes/session-log-routes.ts | 6 +- src/backend/database/routes/snippets.ts | 12 +- .../database/routes/sso-provider-routes.ts | 2 +- .../database/routes/tailscale-routes.ts | 2 +- src/backend/database/routes/terminal.ts | 8 +- src/backend/database/routes/termix-id.test.ts | 5 +- src/backend/database/routes/termix-id.ts | 10 +- .../database/routes/user-admin-routes.ts | 6 +- .../database/routes/user-api-key-routes.ts | 6 +- .../routes/user-oidc-account-routes.ts | 2 +- .../database/routes/user-oidc-utils.ts | 6 +- .../routes/user-password-reset-routes.ts | 20 +- .../database/routes/user-preferences.ts | 2 +- .../database/routes/user-session-routes.ts | 6 +- .../database/routes/user-settings-routes.ts | 6 +- .../database/routes/user-totp-routes.test.ts | 2 +- .../database/routes/user-totp-routes.ts | 10 +- src/backend/database/routes/users.ts | 6 +- src/backend/database/routes/vault.ts | 6 +- src/backend/database/runtime/adapter.ts | 31 - src/backend/database/runtime/config.test.ts | 57 - src/backend/database/runtime/config.ts | 58 - .../database/runtime/sqlite-adapter.test.ts | 55 - .../database/runtime/sqlite-adapter.ts | 92 -- .../sqlite-foreign-key-boundary.test.ts | 28 - src/backend/guacamole/guacamole-server.ts | 2 +- src/backend/guacamole/routes.ts | 6 +- src/backend/ssh/alert-engine.ts | 2 +- src/backend/ssh/auth-manager.ts | 2 +- src/backend/ssh/credential-username.test.ts | 6 +- src/backend/ssh/credential-username.ts | 2 +- src/backend/ssh/docker-console.ts | 2 +- src/backend/ssh/file-manager.ts | 2 +- src/backend/ssh/host-key-verifier.ts | 2 +- .../ssh/host-metrics-history-routes.ts | 2 +- src/backend/ssh/host-metrics-jump-hosts.ts | 2 +- .../ssh/host-metrics-preferences-routes.ts | 2 +- .../ssh/host-metrics-settings-routes.ts | 2 +- src/backend/ssh/host-metrics.ts | 8 +- src/backend/ssh/host-resolver.ts | 6 +- src/backend/ssh/jump-host-chain.ts | 2 +- src/backend/ssh/managers/health.ts | 2 +- src/backend/ssh/opkssh-auth.ts | 2 +- src/backend/ssh/terminal-jump-hosts.ts | 2 +- .../ssh/terminal-session-manager.test.ts | 13 +- src/backend/ssh/terminal-session-manager.ts | 7 +- src/backend/ssh/terminal.ts | 2 +- src/backend/ssh/tmux-monitor.ts | 6 +- src/backend/ssh/tunnel.ts | 2 +- src/backend/ssh/vault-oidc-auth.ts | 6 +- src/backend/ssh/vault-signer-auth.ts | 2 +- src/backend/starter.ts | 6 +- src/backend/utils/audit-logger.test.ts | 2 +- src/backend/utils/audit-logger.ts | 2 +- src/backend/utils/auth-manager.ts | 12 +- ...ential-system-encryption-migration.test.ts | 14 +- .../credential-system-encryption-migration.ts | 6 +- .../database-layer-preupgrade-backup.test.ts | 7 +- .../utils/database-layer-preupgrade-backup.ts | 3 - src/backend/utils/permission-manager.ts | 10 +- .../utils/shared-credential-manager.ts | 10 +- src/backend/utils/user-crypto.ts | 2 +- src/backend/utils/user-data-export.ts | 12 +- .../utils/user-encryption-migration-store.ts | 2 +- 217 files changed, 843 insertions(+), 4297 deletions(-) delete mode 100644 readme/DATABASE-LAYER-GRAY-ROLLOUT.md delete mode 100644 readme/DATABASE-LAYER-PHASE-0-AUDIT.md delete mode 100644 readme/DATABASE-LAYER-REFACTOR-PLAN.md delete mode 100644 src/backend/database/repositories/current-alert-repository.ts delete mode 100644 src/backend/database/repositories/current-api-key-repository.ts delete mode 100644 src/backend/database/repositories/current-audit-log-repository.ts delete mode 100644 src/backend/database/repositories/current-c2s-tunnel-preset-repository.ts delete mode 100644 src/backend/database/repositories/current-command-history-repository.ts delete mode 100644 src/backend/database/repositories/current-credential-repository.ts delete mode 100644 src/backend/database/repositories/current-dashboard-service-link-repository.ts delete mode 100644 src/backend/database/repositories/current-dismissed-alert-repository.ts delete mode 100644 src/backend/database/repositories/current-file-manager-bookmark-repository.ts delete mode 100644 src/backend/database/repositories/current-homepage-item-repository.ts delete mode 100644 src/backend/database/repositories/current-homepage-layout-repository.ts delete mode 100644 src/backend/database/repositories/current-host-folder-repository.ts delete mode 100644 src/backend/database/repositories/current-host-health-repository.ts delete mode 100644 src/backend/database/repositories/current-host-metrics-history-repository.ts delete mode 100644 src/backend/database/repositories/current-host-metrics-preference-repository.ts delete mode 100644 src/backend/database/repositories/current-host-repository.ts delete mode 100644 src/backend/database/repositories/current-host-resolution-repository.ts delete mode 100644 src/backend/database/repositories/current-network-topology-repository.ts delete mode 100644 src/backend/database/repositories/current-open-tab-repository.ts delete mode 100644 src/backend/database/repositories/current-opkssh-token-repository.ts delete mode 100644 src/backend/database/repositories/current-rbac-access-repository.ts delete mode 100644 src/backend/database/repositories/current-recent-activity-repository.ts delete mode 100644 src/backend/database/repositories/current-repository-runtime.ts delete mode 100644 src/backend/database/repositories/current-role-repository.ts delete mode 100644 src/backend/database/repositories/current-session-recording-repository.ts delete mode 100644 src/backend/database/repositories/current-session-repository.ts delete mode 100644 src/backend/database/repositories/current-settings-repository.ts delete mode 100644 src/backend/database/repositories/current-shared-credential-repository.ts delete mode 100644 src/backend/database/repositories/current-snippet-repository.ts delete mode 100644 src/backend/database/repositories/current-ssh-credential-usage-repository.ts delete mode 100644 src/backend/database/repositories/current-sso-provider-repository.ts delete mode 100644 src/backend/database/repositories/current-termix-identity-ca-repository.ts delete mode 100644 src/backend/database/repositories/current-termix-identity-repository.ts delete mode 100644 src/backend/database/repositories/current-tmux-session-tag-repository.ts delete mode 100644 src/backend/database/repositories/current-transfer-recent-repository.ts delete mode 100644 src/backend/database/repositories/current-trusted-device-repository.ts delete mode 100644 src/backend/database/repositories/current-user-data-export-repository.ts delete mode 100644 src/backend/database/repositories/current-user-preference-repository.ts delete mode 100644 src/backend/database/repositories/current-user-repository.ts delete mode 100644 src/backend/database/repositories/current-vault-profile-repository.ts delete mode 100644 src/backend/database/repositories/current-vault-token-repository.ts create mode 100644 src/backend/database/repositories/database-context.ts create mode 100644 src/backend/database/repositories/factory.ts delete mode 100644 src/backend/database/repositories/repository-rollout.test.ts delete mode 100644 src/backend/database/repositories/repository-rollout.ts rename src/backend/database/{runtime/sqlite-foreign-key-boundary.ts => repositories/sqlite-foreign-keys.ts} (85%) create mode 100644 src/backend/database/repositories/test-support.ts delete mode 100644 src/backend/database/runtime/adapter.ts delete mode 100644 src/backend/database/runtime/config.test.ts delete mode 100644 src/backend/database/runtime/config.ts delete mode 100644 src/backend/database/runtime/sqlite-adapter.test.ts delete mode 100644 src/backend/database/runtime/sqlite-adapter.ts delete mode 100644 src/backend/database/runtime/sqlite-foreign-key-boundary.test.ts diff --git a/readme/DATABASE-LAYER-GRAY-ROLLOUT.md b/readme/DATABASE-LAYER-GRAY-ROLLOUT.md deleted file mode 100644 index 1c6e18b4..00000000 --- a/readme/DATABASE-LAYER-GRAY-ROLLOUT.md +++ /dev/null @@ -1,447 +0,0 @@ -# Database Layer Gray Rollout Guide - -Status: Draft branch gray-rollout guide -Branch: `feature/database-layer-refactor` -Scope: repository-boundary migration for current SQLite snapshot runtime - -This branch is not the full multi-database refactor. Gray rollout is only for -the already-migrated repository boundary paths while the production runtime still -uses the existing encrypted SQLite snapshot model. - -## 1. Gray Scope - -Allowed in gray rollout: - -- Existing encrypted SQLite snapshot runtime. -- Existing database file format. -- Existing schema and migration flow. -- Settings, user, host, API key, session, trusted device, audit log, user - preference, open tab, dismissed alert, homepage layout/item, and network - topology, dashboard service link, session recording, command history, recent - activity, transfer recent, and file-manager bookmark current repository - plus C2S tunnel preset, tmux session tag, OPKSSH token, Vault token/profile, - SSH credential usage, role, SSO provider, host folder, alert, host health, - host metrics preference/history, credential, host resolution, snippet, - RBAC access, shared credential, Termix ID identity/CA, and user data export - current repository factories share `current-repository-runtime` for SQLite - context and write-save hooks. -- Settings reads and writes migrated behind `SettingsRepository`. -- Database import/export settings reads and admin upserts migrated behind - `SettingsRepository`. -- Session create/read/update/revoke/list paths migrated behind - `SessionRepository`. -- User create/read/update/delete/auth paths migrated behind `UserRepository`. -- User setup/count/db-health, password-login TOTP guard, and last-admin delete - guard reads migrated behind `UserRepository`. -- Database import/export user unlock/admin checks migrated behind - `UserRepository`. -- LDAP first-user provisioning and admin-group user creation migrated behind - `UserRepository`. -- API key create/list/delete and authentication last-used updates migrated behind - `ApiKeyRepository`. -- Trusted device check/add/remove and TOTP trusted-device cleanup migrated - behind `TrustedDeviceRepository`. -- Credential list, folder list, detail reads, update lookup/readback, host route - credential resolution reads, folder rename writes, apply usage writes, shared - credential source row reads, credential delete lookup/delete, and - credential-host list reads migrated behind `CredentialRepository` and - `HostResolutionRepository`. -- Database export host and credential read/decryption paths migrated behind - `HostRepository` and `CredentialRepository`. -- Database import host and credential duplicate checks plus encrypted creates - migrated behind `HostRepository` and `CredentialRepository`. -- Credential create/update encrypted writes migrated behind - `CredentialRepository`, including system-key copies for shared credentials. -- Credential delete host cleanup and apply-to-host writes migrated behind - `HostRepository`. -- Credential system-key copy backfill migration migrated behind - `CredentialRepository` and `SharedCredentialRepository`. -- Legacy user field-encryption migration SQL centralized behind - `RawSqliteUserEncryptionMigrationStore`. -- Auth login lazy user-field encryption migration now calls the `DataCrypto` - current-runtime migration boundary instead of opening SQLite in - `auth-manager.ts`. -- Current user-field encryption migration now resolves SQLite through - `createCurrentUserEncryptionMigrationStore` and the shared current runtime - helper, keeping `DataCrypto` on the migration-store interface. -- User, admin, TOTP, OIDC account, and credential migration explicit saves now - use `DatabaseSaveTrigger` instead of importing the SQLite snapshot save - function from routes. -- Current SQLite snapshot save trigger now initializes after database startup - regardless of file-encryption mode, and backend shutdown uses that save - boundary. -- Direct SQLite snapshot save-function imports are now isolated inside - `database/db/index.ts`; current user-field migration saves through - `DatabaseSaveTrigger`. -- Database import SQLite foreign-key toggling is isolated behind the - `withSqliteForeignKeysDisabled` runtime boundary and restores constraints in - a `finally` path; the current variant resolves SQLite through the shared - current runtime helper. -- Database import now uses the current SQLite foreign-key boundary without - importing `getDb()` in the route handler. -- Legacy unencrypted SQLite copy/verification path centralized behind - `LegacySqliteDatabaseCopyStore`. -- Termix ID credential lookup, generated credential persistence, and generated - credential cleanup migrated behind `CredentialRepository`. -- Host route update readback, single-host fetch, password-field fetch, and host - export reads migrated behind `HostResolutionRepository`. -- Host route create/update encrypted writes migrated behind `HostRepository`. -- Host route update-state and delete-audit host reads migrated behind - `HostResolutionRepository`. -- Host route delete final host-row writes and audit actor username lookups - migrated behind `HostRepository` and `UserRepository`. -- Host route own/shared list assembly reads migrated behind - `HostResolutionRepository` while preserving route-level own-host decryption. -- Host bulk-update state reads and non-sensitive bulk flag/config writes - migrated behind `HostRepository`. -- Host bulk import overwrite lookup and credential fallback reads migrated behind - current host resolution and credential repository boundaries. -- Host bulk JSON and SSH-config import encrypted create/update writes migrated - behind `HostRepository`. -- Host autostart enable, disable, status, and endpoint-host resolution paths - migrated behind `HostRepository`. -- Guacamole host token host and protocol credential reads migrated behind - `HostResolutionRepository`. -- Host user-cleanup delete paths migrated behind `HostRepository`. -- Snippet folder list/create/metadata/rename/delete, owned lookup, visible-list - owned reads, reorder, create/update/delete, export reads, and bulk import - plus user/password-reset cleanup migrated behind `SnippetRepository`. -- Host folder metadata user cleanup migrated behind `HostFolderRepository`. -- SSO provider listing, management, OIDC config loading, and LDAP provider - validation migrated behind `SsoProviderRepository`. -- Audit log writes, filtered reads, action lists, and user cleanup migrated - behind `AuditLogRepository`. -- User preferences read/write and user cleanup migrated behind - `UserPreferenceRepository`. -- Open tab restore/upsert/sync/update/delete and user cleanup migrated behind - `OpenTabRepository`. -- Dismissed alert read/dismiss/undismiss/export and user cleanup migrated behind - `DismissedAlertRepository`. -- Database import/export dismissed alert reads and writes migrated behind - `DismissedAlertRepository`. -- Homepage layout read/write paths migrated behind - `HomepageLayoutRepository`. -- Homepage item list/create/update/delete migrated behind - `HomepageItemRepository`. -- Network topology read/write and user cleanup migrated behind - `NetworkTopologyRepository`. -- Dashboard service link list/create/update/delete migrated behind - `DashboardServiceLinkRepository`. -- Session recording create/list/read/content/delete/prune and host/folder/user - cleanup migrated behind `SessionRecordingRepository`. -- Command history save/list/delete and host/user cleanup migrated behind - `CommandHistoryRepository`. -- Dashboard recent activity list/log/trim/reset and host/user cleanup migrated - behind `RecentActivityRepository`. -- SSH credential usage writes and host/user cleanup migrated behind - `SshCredentialUsageRepository`. -- Database export SSH credential usage reads migrated behind - `SshCredentialUsageRepository`. -- Transfer recent list/upsert/prune/export and host/folder/user cleanup migrated - behind `TransferRecentRepository`. -- File manager recent/pinned/shortcut list/create/delete/export and - host/folder/user/password-reset cleanup migrated behind - `FileManagerBookmarkRepository`. -- Database import/export file-manager recent/pinned/shortcut target reads and - writes migrated behind `FileManagerBookmarkRepository`. -- C2S tunnel preset list/create/update/delete migrated behind - `C2sTunnelPresetRepository`. -- Tmux session tag list/rename/delete/replace migrated behind - `TmuxSessionTagRepository`. -- OPKSSH token upsert/read/touch/delete and user cleanup migrated behind - `OpksshTokenRepository`. -- Vault token upsert/read/touch/delete migrated behind `VaultTokenRepository`. -- Vault profile list/create/update/delete and profile lookup migrated behind - `VaultProfileRepository`. -- Host metrics layout preference read/upsert and statsConfig widget sync migrated behind - `HostMetricsPreferenceRepository`. -- Host health check config and history read/write migrated behind - `HostHealthRepository`. -- Host metrics history record/prune/query migrated behind - `HostMetricsHistoryRepository`. -- Alert notification channels, rules, linked channels, firings, and alert - engine persistence reads/writes migrated behind `AlertRepository`. -- User data export host and credential read models migrated behind - `UserDataExportRepository`. -- SSH folder list, metadata upsert, rename, and folder host deletion writes - migrated behind `HostFolderRepository`. -- Host, jump-host, Docker SSH, Proxmox discovery, Docker console jump-host, - file-manager activity, host metrics, terminal SSH auth, and tunnel endpoint - credential plus credential deployment and command history host-flag resolution - plus snippet execution, terminal OPKSSH/activity, Vault OIDC profile, and - Wake-on-LAN host, internal host list, host-key verification metadata, - credential, permission-manager owner checks, and shared override read/write - models migrated behind `HostResolutionRepository`. -- Host metrics, host metrics viewer, tmux monitor, Docker, tunnel, and Proxmox - unlock gates now use `DataCrypto` directly instead of `SimpleDBOps`. -- Legacy `SimpleDBOps` compatibility helper removed after migrated route and - utility paths stopped importing it. -- RBAC role management and user-role assignment/listing paths migrated behind - `RoleRepository`. -- Permission manager role permission aggregation and admin-role checks migrated - behind `RoleRepository`. -- User deletion role-assignment cleanup migrated behind `RoleRepository`. -- User deletion API key, trusted device, dashboard link, homepage layout/item, - host health, host metrics preference, C2S preset, Vault token/profile, audit, - alert, Termix ID identity/CA, tmux session tag, encrypted-data, UI state, and - related per-user cleanup now uses current repository boundaries before the - final user row delete. -- User admin route role sync and admin-created default role assignment migrated - behind `RoleRepository`. -- LDAP login default role assignment and admin-role sync migrated behind - `RoleRepository`. -- Local, GitHub OIDC, and standard OIDC user default role assignment plus OIDC - admin-group role sync migrated behind `RoleRepository`. -- RBAC host/snippet access-list read models migrated behind - `RbacAccessRepository`. -- RBAC shared host/shared snippet read models migrated behind - `RbacAccessRepository`. -- RBAC route host/snippet owner checks and direct host-access credential - existence reads migrated behind current host, snippet, and credential - repository boundaries. -- RBAC host/snippet access grant, revoke, and direct host-access credential - override writes migrated behind `RbacAccessRepository`. -- Shared credential material create/update/delete, pending re-encryption, and - user cleanup persistence migrated behind `SharedCredentialRepository`. -- Permission manager host-access cleanup, shared-access lookup, and last-access - touch migrated behind `RbacAccessRepository`. -- Termix ID identity handle CRUD/resolution, public key publish/list/update/delete, - linked credential lookup, and certificate target key lookup migrated behind - `TermixIdentityRepository`. -- Termix ID CA public lookup, encrypted private-key create/rotate/delete, and - certificate signing reads migrated behind `TermixIdentityCaRepository`. -- Current field encryption behavior. - -Not included in gray rollout: - -- PostgreSQL or MySQL/MariaDB runtime. -- New external database configuration UI. -- New schema migration strategy. -- Host and credential route migration beyond existing repository skeletons. -- Remaining audit, preferences, file manager, and metrics repository migration. -- Multi-instance backend deployment. - -## 2. Required Preflight - -Before enabling this branch for any gray target: - -1. Confirm the target is running from a full backup of the data directory. -2. Confirm the app can write to the data directory so the automatic - pre-upgrade backup can be created before first database startup. -3. Record the exact source commit and container/image tag currently serving - traffic. -4. Confirm the gray build commit is known. -5. Run the validation commands from this branch. -6. Use a target with a small, known user set first. -7. Keep an operator available for rollback during the first login/API-key smoke - tests. - -Repository rollout is controlled by `DATABASE_LAYER_REPOSITORY_ROLLOUT`. - -Recommended gray value: - -```bash -DATABASE_LAYER_REPOSITORY_ROLLOUT=settings,users,sessions,api_keys,trusted_devices,credentials,termix_identity,termix_identity_ca,hosts,snippets,sso_providers,audit_logs,user_preferences,open_tabs,dismissed_alerts,homepage_layouts,homepage_items,network_topology,dashboard_service_links,session_recordings,command_history,recent_activity,ssh_credential_usage,transfer_recent,file_manager_bookmarks,c2s_tunnel_presets,tmux_session_tags,opkssh_tokens,vault_tokens,vault_profiles,host_metrics_preferences,host_health,host_metrics_history,alerts,user_data_exports,host_folders,host_resolution,roles,rbac_access,shared_credentials -``` - -Accepted values: - -| Value | Behavior | -| ------------------------------------------- | --------------------------------------------- | -| unset | current migrated slice enabled; logs implicit | -| `all`, `true`, `1`, `on`, `enabled` | all current migrated repository domains | -| `off`, `false`, `0`, `none`, `disabled` | no migrated repository domains; fail closed | -| comma list, for example `settings,users` | only listed repository domains enabled | -| aliases such as `user`, `api-key`, `apikey` | normalized to the supported domain names | - -Supported domains: - -- `settings` -- `users` -- `sessions` -- `api_keys` -- `trusted_devices` -- `credentials` -- `termix_identity` -- `termix_identity_ca` -- `hosts` -- `snippets` -- `sso_providers` -- `audit_logs` -- `user_preferences` -- `open_tabs` -- `dismissed_alerts` -- `homepage_layouts` -- `homepage_items` -- `network_topology` -- `dashboard_service_links` -- `session_recordings` -- `command_history` -- `recent_activity` -- `ssh_credential_usage` -- `transfer_recent` -- `file_manager_bookmarks` -- `c2s_tunnel_presets` -- `tmux_session_tags` -- `opkssh_tokens` -- `vault_tokens` -- `vault_profiles` -- `host_metrics_preferences` -- `host_health` -- `host_metrics_history` -- `alerts` -- `user_data_exports` -- `host_folders` -- `host_resolution` -- `roles` -- `rbac_access` -- `shared_credentials` - -The backend logs the parsed rollout mode at startup with operation -`repository_rollout_config`. Use an explicit value in any staging or production -gray target so logs show the intended rollout state. - -Admin users can also verify the active rollout state through -`GET /database/migration/status`. The response includes `repositoryRollout` -with the parsed mode, enabled domains, supported domains, env key, and whether -the value was explicitly configured. Startup logs and this endpoint also expose -rollout warnings for implicit, disabled, or partial configurations. - -Minimum backup artifacts: - -- encrypted SQLite snapshot file -- environment configuration -- application version or image tag -- logs from the last healthy startup on the previous version - -This branch also creates one automatic pre-upgrade backup before opening the -database for the first time. If `db.sqlite.encrypted`, `db.sqlite.encrypted.meta`, -or `db.sqlite` exists and `.database-layer-preupgrade-backup.json` is absent, the -backend copies the database file(s) plus `.env` into: - -```text -/backups/pre-database-layer-refactor-/ -``` - -The backup directory and marker both include a `manifest.json` payload with the -app version, rollout config, source files, copied files, and backup reason. A -successful marker prevents repeat backups on every restart. If the backup cannot -be created, startup fails closed. Operators may bypass only when an external -backup has already been verified: - -```bash -DATABASE_LAYER_SKIP_PREUPGRADE_BACKUP=1 -``` - -The app keeps the latest three automatic pre-upgrade backups by default. Override -with `DATABASE_LAYER_PREUPGRADE_BACKUP_KEEP=`. - -## 3. Required Validation Commands - -Run before deployment: - -```bash -npm run type-check -npx eslint src/backend/database/repositories/repository-rollout.ts src/backend/database/repositories/repository-rollout.test.ts src/backend/database/repositories/current-settings-repository.ts src/backend/database/repositories/current-user-repository.ts src/backend/database/repositories/current-session-repository.ts src/backend/database/repositories/current-api-key-repository.ts src/backend/database/repositories/current-trusted-device-repository.ts src/backend/database/repositories/current-credential-repository.ts src/backend/database/repositories/credential-repository.ts src/backend/database/repositories/current-host-repository.ts src/backend/database/repositories/host-repository.ts src/backend/database/repositories/host-credential-repositories.test.ts src/backend/database/repositories/current-sso-provider-repository.ts src/backend/database/repositories/sso-provider-repository.ts src/backend/database/repositories/sso-provider-repository.test.ts src/backend/database/repositories/current-audit-log-repository.ts src/backend/database/repositories/audit-log-repository.ts src/backend/database/repositories/audit-log-repository.test.ts src/backend/database/repositories/current-user-preference-repository.ts src/backend/database/repositories/user-preference-repository.ts src/backend/database/repositories/user-preference-repository.test.ts src/backend/database/repositories/current-open-tab-repository.ts src/backend/database/repositories/open-tab-repository.ts src/backend/database/repositories/open-tab-repository.test.ts src/backend/database/repositories/current-dismissed-alert-repository.ts src/backend/database/repositories/dismissed-alert-repository.ts src/backend/database/repositories/dismissed-alert-repository.test.ts src/backend/database/repositories/current-homepage-layout-repository.ts src/backend/database/repositories/homepage-layout-repository.ts src/backend/database/repositories/homepage-layout-repository.test.ts src/backend/database/repositories/current-homepage-item-repository.ts src/backend/database/repositories/homepage-item-repository.ts src/backend/database/repositories/homepage-item-repository.test.ts src/backend/database/repositories/current-network-topology-repository.ts src/backend/database/repositories/network-topology-repository.ts src/backend/database/repositories/network-topology-repository.test.ts src/backend/database/repositories/current-dashboard-service-link-repository.ts src/backend/database/repositories/dashboard-service-link-repository.ts src/backend/database/repositories/dashboard-service-link-repository.test.ts src/backend/database/repositories/current-session-recording-repository.ts src/backend/database/repositories/session-recording-repository.ts src/backend/database/repositories/session-recording-repository.test.ts src/backend/database/repositories/current-command-history-repository.ts src/backend/database/repositories/command-history-repository.ts src/backend/database/repositories/command-history-repository.test.ts src/backend/database/repositories/current-recent-activity-repository.ts src/backend/database/repositories/recent-activity-repository.ts src/backend/database/repositories/recent-activity-repository.test.ts src/backend/database/repositories/current-ssh-credential-usage-repository.ts src/backend/database/repositories/ssh-credential-usage-repository.ts src/backend/database/repositories/ssh-credential-usage-repository.test.ts src/backend/database/repositories/current-transfer-recent-repository.ts src/backend/database/repositories/transfer-recent-repository.ts src/backend/database/repositories/transfer-recent-repository.test.ts src/backend/database/repositories/current-file-manager-bookmark-repository.ts src/backend/database/repositories/file-manager-bookmark-repository.ts src/backend/database/repositories/file-manager-bookmark-repository.test.ts src/backend/database/repositories/current-c2s-tunnel-preset-repository.ts src/backend/database/repositories/c2s-tunnel-preset-repository.ts src/backend/database/repositories/c2s-tunnel-preset-repository.test.ts src/backend/database/repositories/current-tmux-session-tag-repository.ts src/backend/database/repositories/tmux-session-tag-repository.ts src/backend/database/repositories/tmux-session-tag-repository.test.ts src/backend/database/repositories/current-opkssh-token-repository.ts src/backend/database/repositories/opkssh-token-repository.ts src/backend/database/repositories/opkssh-token-repository.test.ts src/backend/database/repositories/current-vault-token-repository.ts src/backend/database/repositories/vault-token-repository.ts src/backend/database/repositories/vault-token-repository.test.ts src/backend/database/repositories/current-vault-profile-repository.ts src/backend/database/repositories/vault-profile-repository.ts src/backend/database/repositories/vault-profile-repository.test.ts src/backend/database/repositories/current-host-metrics-preference-repository.ts src/backend/database/repositories/host-metrics-preference-repository.ts src/backend/database/repositories/host-metrics-preference-repository.test.ts src/backend/database/repositories/current-host-health-repository.ts src/backend/database/repositories/host-health-repository.ts src/backend/database/repositories/host-health-repository.test.ts src/backend/database/repositories/current-host-metrics-history-repository.ts src/backend/database/repositories/host-metrics-history-repository.ts src/backend/database/repositories/host-metrics-history-repository.test.ts src/backend/database/repositories/current-alert-repository.ts src/backend/database/repositories/alert-repository.ts src/backend/database/repositories/alert-repository.test.ts src/backend/database/repositories/current-user-data-export-repository.ts src/backend/database/repositories/user-data-export-repository.ts src/backend/database/repositories/user-data-export-repository.test.ts src/backend/database/repositories/current-host-folder-repository.ts src/backend/database/repositories/host-folder-repository.ts src/backend/database/repositories/host-folder-repository.test.ts src/backend/database/repositories/current-host-resolution-repository.ts src/backend/database/repositories/host-resolution-repository.ts src/backend/database/repositories/host-resolution-repository.test.ts src/backend/database/repositories/current-snippet-repository.ts src/backend/database/repositories/snippet-repository.ts src/backend/database/repositories/snippet-repository.test.ts src/backend/database/repositories/current-role-repository.ts src/backend/database/repositories/role-repository.ts src/backend/database/repositories/role-repository.test.ts src/backend/database/repositories/current-rbac-access-repository.ts src/backend/database/repositories/rbac-access-repository.ts src/backend/database/repositories/rbac-access-repository.test.ts src/backend/database/routes/rbac.ts src/backend/database/routes/snippets.ts src/backend/database/routes/host.ts src/backend/database/routes/credentials.ts src/backend/database/routes/delete-user-data.ts src/backend/database/routes/open-tabs.ts src/backend/database/routes/user-preferences.ts src/backend/database/routes/user-admin-routes.ts src/backend/database/routes/ldap-auth-routes.ts src/backend/database/routes/users.ts src/backend/database/routes/user-oidc-utils.ts src/backend/database/routes/sso-provider-routes.ts src/backend/database/routes/audit-log-routes.ts src/backend/database/routes/host-folder-routes.ts src/backend/database/routes/host-file-manager-bookmark-routes.ts src/backend/database/routes/c2s-tunnel-presets.ts src/backend/database/routes/alerts.ts src/backend/database/routes/alert-rules-routes.ts src/backend/database/routes/homepage-layout-routes.ts src/backend/database/routes/homepage-items-routes.ts src/backend/database/routes/network-topology.ts src/backend/database/routes/dashboard-service-links-routes.ts src/backend/database/routes/session-log-routes.ts src/backend/database/routes/host-command-history-routes.ts src/backend/database/routes/terminal.ts src/backend/database/routes/user-password-reset-routes.ts src/backend/ssh/terminal-session-manager.ts src/backend/ssh/tmux-monitor.ts src/backend/ssh/opkssh-auth.ts src/backend/ssh/vault-signer-auth.ts src/backend/ssh/vault-oidc-auth.ts src/backend/ssh/host-resolver.ts src/backend/ssh/host-metrics-preferences-routes.ts src/backend/ssh/managers/health.ts src/backend/ssh/host-metrics.ts src/backend/ssh/host-metrics-history-routes.ts src/backend/ssh/alert-engine.ts src/backend/utils/user-data-export.ts src/backend/utils/audit-logger.ts src/backend/utils/audit-logger.test.ts src/backend/utils/permission-manager.ts src/backend/utils/shared-credential-manager.ts src/backend/starter.ts -npm run test -- src/backend/database/runtime/config.test.ts src/backend/database/runtime/sqlite-adapter.test.ts src/backend/database/repositories/repository-rollout.test.ts src/backend/database/repositories/settings-repository.test.ts src/backend/database/repositories/user-session-repositories.test.ts src/backend/database/repositories/api-key-repository.test.ts src/backend/database/repositories/trusted-device-repository.test.ts src/backend/database/repositories/sso-provider-repository.test.ts src/backend/database/repositories/audit-log-repository.test.ts src/backend/database/repositories/user-preference-repository.test.ts src/backend/database/repositories/open-tab-repository.test.ts src/backend/database/repositories/dismissed-alert-repository.test.ts src/backend/database/repositories/homepage-layout-repository.test.ts src/backend/database/repositories/homepage-item-repository.test.ts src/backend/database/repositories/network-topology-repository.test.ts src/backend/database/repositories/dashboard-service-link-repository.test.ts src/backend/database/repositories/session-recording-repository.test.ts src/backend/database/repositories/command-history-repository.test.ts src/backend/database/repositories/recent-activity-repository.test.ts src/backend/database/repositories/ssh-credential-usage-repository.test.ts src/backend/database/repositories/transfer-recent-repository.test.ts src/backend/database/repositories/file-manager-bookmark-repository.test.ts src/backend/database/repositories/c2s-tunnel-preset-repository.test.ts src/backend/database/repositories/tmux-session-tag-repository.test.ts src/backend/database/repositories/opkssh-token-repository.test.ts src/backend/database/repositories/vault-token-repository.test.ts src/backend/database/repositories/vault-profile-repository.test.ts src/backend/database/repositories/host-metrics-preference-repository.test.ts src/backend/database/repositories/host-health-repository.test.ts src/backend/database/repositories/host-metrics-history-repository.test.ts src/backend/database/repositories/alert-repository.test.ts src/backend/database/repositories/user-data-export-repository.test.ts src/backend/database/repositories/host-folder-repository.test.ts src/backend/database/repositories/host-resolution-repository.test.ts src/backend/database/repositories/snippet-repository.test.ts src/backend/database/repositories/role-repository.test.ts src/backend/database/repositories/rbac-access-repository.test.ts src/backend/database/repositories/host-credential-repositories.test.ts src/backend/database/repositories/field-encryption-boundary.test.ts src/backend/utils/field-crypto.test.ts src/backend/utils/audit-logger.test.ts src/backend/guacamole/token-service.test.ts src/backend/database/routes/user-oidc-utils.test.ts src/backend/database/routes/termix-id.test.ts src/backend/database/routes/user-totp-routes.test.ts src/backend/utils/permission-manager.test.ts src/backend/ssh/credential-username.test.ts src/backend/ssh/tmux-monitor-helpers.test.ts -git diff --check -``` - -## 4. Smoke Test Matrix - -Run these against the gray target: - -| Area | Required check | -| ------------- | ------------------------------------------------------------------------------------------------------ | -| Startup | App starts from existing encrypted snapshot without schema errors | -| Login | Password login succeeds for an existing local user | -| Sessions | Refresh, logout, and session list/revoke still work | -| Users | `/users/me`, user list, admin create, make/remove admin still work | -| Registration | New local user registration works when registration is enabled | -| Password | Change password, logout, and login with the new password | -| OIDC | Existing OIDC login works; auto-provision check only if enabled | -| API keys | Admin create/list/delete API key; API key authentication updates usage | -| Settings | Read/write user settings and global auth settings | -| Preferences | Read/write user preferences and user cleanup still work | -| Open tabs | Tab restore, single upsert, bulk sync, update/delete, active session list, and user cleanup work | -| Alerts | Active alert filtering, dismiss/undismiss, dismissed list, export, and user cleanup work | -| Homepage | Homepage layout read/write still works | -| Topology | Network topology read/write and user cleanup still work | -| Dashboard | Dashboard service link list/create/update/delete still works | -| Command log | Terminal and host command history save/list/delete and cleanup still work | -| Activity | Recent activity cleanup for host, folder, password reset, and user deletion still works | -| Usage | SSH credential usage tracking and host/folder/user cleanup still work | -| SSO providers | Login provider list, admin provider list/create/update/delete, and OIDC/LDAP login config loading work | -| Audit logs | Audit write, audit list filters, action filter list, and user cleanup still work | -| Roles | Admin list/create/update/delete role and assign/remove a user role | -| RBAC access | Host/snippet share/revoke/list and shared host/snippet endpoints work | -| Security | Non-admin user is rejected from admin-only endpoints | - -Do not continue gray rollout if any check fails. - -Also verify these startup log cases before production gray: - -| Environment value | Expected startup behavior | -| -------------------------------------------- | -------------------------------------------------- | -| `DATABASE_LAYER_REPOSITORY_ROLLOUT=all` | startup logs `mode: all` and all supported domains | -| `DATABASE_LAYER_REPOSITORY_ROLLOUT=off` | migrated repository use fails closed | -| `DATABASE_LAYER_REPOSITORY_ROLLOUT=settings` | only settings repository boundary is allowed | - -Then check `/database/migration/status` as an admin and confirm -`repositoryRollout.mode`, `repositoryRollout.enabledDomains`, and -`repositoryRollout.explicit` match the intended gray target. -`repositoryRollout.warnings` should be empty for the recommended full gray -slice. - -## 5. Rollout Stages - -Recommended stages: - -1. Local fixture run with copied production-like data. -2. Internal staging with one admin and one normal user. -3. Internal gray target with real integrations enabled. -4. Small production gray group. - -Do not run multiple backend instances from this branch. The current runtime still -uses the encrypted SQLite snapshot model and is not safe for multi-writer -deployment. - -## 6. Rollback - -Rollback should be operationally simple because this gray scope does not change -the data format or require an external database. - -Rollback steps: - -1. Stop the gray build. -2. Restore the pre-gray encrypted database snapshot if any write-path smoke test - failed. -3. Deploy the previous known-good commit or image tag. -4. Start the previous version. -5. Verify startup, login, settings read, and one admin endpoint. - -If the gray build only served read paths and no smoke test failed, restoring the -snapshot may not be necessary. If there is any doubt, restore the snapshot. - -## 7. Stop Conditions - -Stop gray rollout immediately if any of these happen: - -- Login failure spike. -- API key authentication failure for known-good keys. -- Admin authorization mismatch. -- OIDC callback failure for existing users. -- Missing or stale settings after write. -- Data snapshot save errors. -- Any database error mentioning missing tables, unknown columns, or failed - serialization. -- Any user encryption or data unlock failure. -- Any unexpected `Repository domain ... is disabled` error for a domain that was - intended to be enabled. - -## 8. Current Gray Readiness - -Current branch can be considered gray-candidate only after all validation -commands and smoke checks above pass on staging. - -The branch should remain draft until gray evidence is attached to the PR. diff --git a/readme/DATABASE-LAYER-PHASE-0-AUDIT.md b/readme/DATABASE-LAYER-PHASE-0-AUDIT.md deleted file mode 100644 index 2177c2a1..00000000 --- a/readme/DATABASE-LAYER-PHASE-0-AUDIT.md +++ /dev/null @@ -1,548 +0,0 @@ -# Database Layer Refactor Phase 0 Audit - -Status: Draft -Branch: `feature/database-layer-refactor` -Purpose: Establish the current database access inventory, domain map, sensitive field map, and first implementation boundaries before changing runtime persistence. - -## 1. Scope - -Phase 0 does not change runtime behavior. - -It produces the evidence needed for the next implementation phases: - -- where database access currently happens -- which modules write directly to the database -- which tables belong to which product domains -- which fields are sensitive -- which direct writes are risky under the current in-memory snapshot model -- which domains should move first into repositories - -## 2. Current Evidence - -Commands used for the initial audit: - -```bash -rg -n "getDb\\(|getSqlite\\(|DatabaseSaveTrigger|SimpleDBOps|db\\.\\$client|\\.prepare\\(" src/backend -rg -n "getDb\\(\\)\\.(insert|update|delete)|await db\\.(insert|update|delete)|db\\.(insert|update|delete)|\\.\\$client\\.prepare\\(\\\"(INSERT|UPDATE|DELETE)|\\.prepare\\(\\\"(INSERT|UPDATE|DELETE)|DatabaseSaveTrigger\\.triggerSave|DatabaseSaveTrigger\\.forceSave" src/backend -rg -n "export const .* = sqliteTable\\(" src/backend/database/db/schema.ts -``` - -High-level findings: - -- Database infrastructure is concentrated in `src/backend/database/db/index.ts`. -- Business database access is spread across route modules, SSH modules, utilities, and auth helpers. -- `SimpleDBOps` is not the only write path. -- There are many direct Drizzle writes and raw SQLite writes. -- Some direct writes manually trigger `DatabaseSaveTrigger`; many write paths do not. -- Schema is SQLite-specific through `sqliteTable` and manual `CREATE TABLE IF NOT EXISTS` / `addColumnIfNotExists`. - -## 3. Database Access Hotspots - -The most database-heavy files by audit hits: - -| File | Approx. hits | Notes | -| ----------------------------------------------------- | -----------: | --------------------------------------------------------------- | -| `src/backend/database/db/index.ts` | 75 | database init, schema creation, ad-hoc migration, snapshot save | -| `src/backend/database/routes/alert-rules-routes.ts` | 64 | alert rules, channels, firings, raw SQL deletes | -| `src/backend/database/routes/users.ts` | 54 | users, settings, OIDC/GitHub settings, admin flows | -| `src/backend/database/database.ts` | 27 | import/export, legacy SQLite handling | -| `src/backend/ssh/host-metrics.ts` | 26 | metrics connection and settings reads | -| `src/backend/database/routes/user-settings-routes.ts` | 16 | user settings | -| `src/backend/dashboard.ts` | 15 | dashboard aggregation reads | -| `src/backend/ssh/docker.ts` | 14 | host/credential reads for Docker SSH | -| `src/backend/ssh/managers/health.ts` | 13 | host health checks | -| `src/backend/ssh/alert-engine.ts` | 13 | alert evaluation and firing state | -| `src/backend/utils/user-crypto.ts` | 12 | settings writes for crypto metadata | -| `src/backend/ssh/host-metrics-settings-routes.ts` | 12 | metrics settings | -| `src/backend/ssh/tmux-monitor.ts` | 11 | tmux session tags | -| `src/backend/guacamole/routes.ts` | 11 | Guacamole config/routes | -| `src/backend/database/routes/host.ts` | 10 | host operations and related rows | - -This confirms the refactor must be domain-by-domain. A mechanical replacement of `getDb()` would be noisy and unsafe. - -## 4. Direct Write Risk Inventory - -Under the current architecture, a direct write is risky when it bypasses `SimpleDBOps` and does not trigger `DatabaseSaveTrigger`. - -### 4.1 Direct Writes That Need Repository Ownership - -These areas perform direct writes and should move behind repositories/services: - -| Area | Representative files | Examples | -| --------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------- | -| users/settings/auth | `routes/users.ts`, `utils/auth-manager.ts`, `utils/user-crypto.ts` | sessions, trusted devices, OIDC settings, registration settings | -| RBAC/sharing | `routes/rbac.ts`, `utils/shared-credential-manager.ts` | roles, host access, snippet access, shared credentials | -| hosts/credentials | `routes/host.ts`, `routes/credentials.ts`, `host-resolver.ts` | host access cleanup, credential usage | -| file manager metadata | `host-file-manager-bookmark-routes.ts` | recent, pinned, shortcuts | -| alerts | `alert-rules-routes.ts`, `ssh/alert-engine.ts` | channels, rules, firings | -| metrics | `host-metrics-preferences-routes.ts`, `managers/health.ts` | preferences, health checks, history | -| terminal logs | `terminal-session-manager.ts` | session recording metadata | -| tmux | `tmux-monitor.ts` | tmux session tags | -| import/export | `database/database.ts` | SQLite import and forced save | -| open tabs | `routes/open-tabs.ts` | tab persistence and cleanup | -| API keys | `user-api-key-routes.ts`, `utils/auth-manager.ts` | API key create/delete/last-used | -| SSO and identity | `sso-provider-routes.ts`, `termix-id.ts` | providers, identity keys/CA | - -### 4.2 Immediate Compatibility Rule - -Until a domain is migrated to repositories: - -- Every direct write must either be moved into a repository or explicitly trigger persistence in the old runtime. -- New code should not add direct `getDb()` writes outside infrastructure or repositories. -- The draft branch should add an enforcement check before Phase 8, not immediately, because the current codebase still violates the target rule widely. - -## 5. Table Domain Map - -### 5.1 Identity and Authentication - -| Tables | Notes | -| ---------------------- | -------------------------------------------- | -| `users` | local/OIDC users, TOTP fields, password hash | -| `sessions` | JWT sessions | -| `trusted_devices` | remembered devices | -| `api_keys` | API token hashes/prefixes | -| `sso_providers` | configured SSO providers | -| `termix_identities` | Termix identity records | -| `termix_identity_keys` | public/private identity key metadata | -| `termix_identity_ca` | CA material, private key is sensitive | - -Suggested repository: - -- `userRepository` -- `sessionRepository` -- `trustedDeviceRepository` -- `apiKeyRepository` -- `ssoProviderRepository` -- `termixIdentityRepository` - -### 5.2 Hosts and Credentials - -| Tables | Notes | -| ---------------------- | ------------------------------------------------ | -| `ssh_data` | primary host table, many config JSON/text fields | -| `ssh_credentials` | reusable credentials | -| `ssh_credential_usage` | usage history | -| `ssh_folders` | folder metadata | -| `host_access` | sharing/RBAC access rows | -| `shared_credentials` | encrypted shared credential material | -| `network_topology` | topology graph/config | - -Suggested repository: - -- `hostRepository` -- `credentialRepository` -- `credentialUsageRepository` -- `hostAccessRepository` -- `sharedCredentialRepository` -- `hostFolderRepository` -- `networkTopologyRepository` - -### 5.3 RBAC - -| Tables | Notes | -| ---------------- | ------------------------ | -| `roles` | role definitions | -| `user_roles` | user to role assignments | -| `host_access` | host-level grants | -| `snippet_access` | snippet-level grants | - -Suggested repository: - -- `roleRepository` -- `accessRepository` - -### 5.4 File Manager - -| Tables | Notes | -| ------------------------ | ---------------------------------- | -| `file_manager_recent` | recently opened paths | -| `file_manager_pinned` | pinned paths | -| `file_manager_shortcuts` | saved shortcuts | -| `transfer_recent` | host-to-host transfer destinations | - -Suggested repository: - -- `fileManagerRepository` -- `transferRecentRepository` - -### 5.5 Snippets - -| Tables | Notes | -| ----------------- | ----------------------- | -| `snippets` | command snippets | -| `snippet_folders` | snippet folder metadata | -| `snippet_access` | snippet sharing | - -Suggested repository: - -- `snippetRepository` - -### 5.6 Runtime Metadata and Audit - -| Tables | Notes | -| -------------------- | ------------------------------------------------------ | -| `audit_logs` | append-only audit trail | -| `session_recordings` | terminal recording metadata, log content is file-based | -| `recent_activity` | host activity feed | -| `command_history` | terminal command history | -| `user_open_tabs` | UI restore state; currently cleared on startup | -| `user_preferences` | per-user UI/preferences | -| `settings` | global settings | - -Suggested repository: - -- `auditRepository` -- `sessionRecordingRepository` -- `activityRepository` -- `commandHistoryRepository` -- `openTabsRepository` -- `userPreferencesRepository` -- `settingsRepository` - -### 5.7 Metrics and Alerts - -| Tables | Notes | -| -------------------------- | -------------------------- | -| `host_metrics_preferences` | metrics layout/preferences | -| `host_health_checks` | health check definitions | -| `host_health_history` | health check results | -| `host_metrics_history` | metrics history | -| `alert_rules` | alert definitions | -| `notification_channels` | webhook/ntfy/etc config | -| `alert_rule_channels` | rule/channel joins | -| `alert_firings` | firing/ack state | -| `dismissed_alerts` | dismissed system alerts | - -Suggested repository: - -- `metricsRepository` -- `healthCheckRepository` -- `alertRepository` -- `notificationRepository` - -### 5.8 Integrations and Feature Config - -| Tables | Notes | -| ------------------------- | --------------------------------------- | -| `c2s_tunnel_presets` | tunnel preset config | -| `opkssh_tokens` | OPKSSH cert/private key cache | -| `vault_profiles` | Vault profile config, mostly non-secret | -| `vault_tokens` | Vault cert/private key cache | -| `dashboard_service_links` | dashboard links | -| `homepage_items` | homepage widgets/items | -| `homepage_layouts` | homepage layouts | -| `tmux_session_tags` | tmux tag metadata | - -Suggested repository: - -- `tunnelPresetRepository` -- `opksshTokenRepository` -- `vaultRepository` -- `dashboardRepository` -- `homepageRepository` -- `tmuxRepository` - -## 6. Sensitive Field Map - -The current explicit `FieldCrypto` map encrypts: - -| Table | Fields | -| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `users` | `passwordHash`, `clientSecret`, `totpSecret`, `totpBackupCodes`, `oidcIdentifier` | -| `ssh_data` | `password`, `key`, `keyPassword`, `sudoPassword`, `autostartPassword`, `autostartKey`, `autostartKeyPassword`, `socks5Password`, `rdpPassword`, `vncPassword`, `telnetPassword` | -| `ssh_credentials` | `password`, `privateKey`, `keyPassword`, `key`, `publicKey` | -| `opkssh_tokens` | `sshCert`, `privateKey` | -| `termix_identity_ca` | `privateKey` | -| `vault_tokens` | `sshCert`, `privateKey` | - -Additional sensitive fields by table semantics: - -| Table | Fields / reason | -| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ssh_credentials` | `systemPassword`, `systemKey`, `systemKeyPassword` are system-key encrypted credential copies | -| `shared_credentials` | `encryptedUsername`, `encryptedAuthType`, `encryptedPassword`, `encryptedKey`, `encryptedKeyPassword`, `encryptedKeyType` are already encrypted payload fields | -| `api_keys` | `tokenHash` is not plaintext but is authentication material; `tokenPrefix` may remain plaintext for display | -| `settings` | some keys may hold provider secrets or reset codes; repository must classify by key | -| `notification_channels` | config may include webhook URLs/tokens; treat config as sensitive unless split | -| `alert_rules` | rule definitions are usually not secret but can include host/resource metadata | -| `homepage_items` | widget config can include URLs/API config; classify per widget type | -| `c2s_tunnel_presets` | config can include connection details; review before plaintext external DB storage | -| `termix_identity_keys` | inspect key material fields before migration; public keys are not secret but private material must never be plaintext | -| `vault_profiles` | current comments say profile fields are non-secret; keep that invariant explicit | - -Open privacy decision: - -- `ssh_data.ip`, domain fields, usernames, folders, and tags are queryable today. -- Encrypting them improves confidentiality but breaks search/filter/sort unless blind indexes are added. -- Recommended initial migration: keep them plaintext and document privacy implications; add optional privacy mode later. - -## 7. Repository Migration Order - -Use a vertical slice instead of broad replacement. - -### 7.1 First Slice - -1. `settingsRepository` -2. `userRepository` -3. `sessionRepository` -4. `hostRepository` -5. `credentialRepository` - -Reasons: - -- Covers the app's boot/login/core host management path. -- Exercises field encryption. -- Exercises per-user data unlock requirements. -- Exercises transaction and migration behavior. -- Builds reusable patterns for the rest of the backend. - -### 7.2 Second Slice - -1. `hostAccessRepository` -2. `roleRepository` -3. `sharedCredentialRepository` -4. `auditRepository` -5. `userPreferencesRepository` - -Reasons: - -- Completes permission and sharing boundaries. -- Removes high-risk direct writes in admin/RBAC flows. -- Moves audit to an append-only repository. - -### 7.3 Third Slice - -1. `snippetRepository` -2. `fileManagerRepository` -3. `metricsRepository` -4. `alertRepository` -5. `homepageRepository` - -Reasons: - -- These are broad feature domains with many routes. -- They should reuse patterns from the core slices. - -### 7.4 Fourth Slice - -1. `vaultRepository` -2. `opksshTokenRepository` -3. `termixIdentityRepository` -4. `tunnelPresetRepository` -5. `tmuxRepository` - -Reasons: - -- Sensitive token/key cache domains need careful encryption tests. -- Some data is transient and should have retention cleanup. - -## 8. Adapter Design Notes - -The adapter boundary should expose: - -```ts -interface DatabaseAdapter { - dialect: "sqlite" | "postgres" | "mysql"; - connect(): Promise; - close(): Promise; - migrate(): Promise; - transaction(fn: (tx: DatabaseTransaction) => Promise): Promise; -} -``` - -The repository layer should not depend on `better-sqlite3`. - -For Drizzle, likely options: - -- keep dialect-specific Drizzle clients internally -- expose repositories instead of exposing the raw Drizzle client -- keep schema definitions close to migrations, not route handlers - -Important: do not make route modules import dialect-specific schema objects after migration. - -## 9. Compatibility Shims - -During the migration, avoid a flag day. - -Add a compatibility database module that lets old code continue to run while new repositories are introduced: - -```text -legacy getDb() path -new adapter/repository path -``` - -Rules: - -- New modules use repositories only. -- Migrated modules must not fall back to `getDb()`. -- Legacy path is removed only after all domains move. - -## 10. Phase 1 Entry Criteria - -Before implementing the adapter skeleton: - -- This audit document exists. -- The sensitive field map is reviewed. -- The first vertical slice is accepted. -- The draft PR remains draft. -- No runtime behavior has changed. - -## 11. Phase 1 Deliverables - -Recommended first implementation PR on this branch: - -- `src/backend/database/runtime/config.ts` -- `src/backend/database/runtime/adapter.ts` -- `src/backend/database/runtime/sqlite-adapter.ts` -- `src/backend/database/repositories/settings-repository.ts` -- `src/backend/database/repositories/user-repository.ts` -- `src/backend/database/repositories/session-repository.ts` -- `src/backend/database/repositories/host-repository.ts` -- `src/backend/database/repositories/credential-repository.ts` -- `src/backend/database/repositories/field-encryption-boundary.ts` -- `src/backend/database/repositories/current-settings-repository.ts` -- tests for config parsing and SQLite adapter boot - -Started: - -- runtime config parser -- SQLite adapter skeleton -- migration metadata table bootstrap -- `SettingsRepository` skeleton and tests -- `UserRepository` and `SessionRepository` skeletons and tests -- `HostRepository` and `CredentialRepository` skeletons and tests -- `FieldEncryptionBoundary` skeleton and tests -- first settings route slice wired through `SettingsRepository` -- user settings route direct `settings` table access moved behind - `SettingsRepository` -- host metrics settings route direct `settings` table access moved behind - `SettingsRepository` -- ACME SSL settings route direct `settings` table access moved behind - `SettingsRepository` -- terminal route direct `settings` table access moved behind - `SettingsRepository` -- tailscale route direct `settings` table access moved behind - `SettingsRepository` -- Guacamole route and WebSocket server direct `settings` table access moved - behind the current settings repository boundary -- auth token expiry and terminal session timeout settings reads moved behind the - current settings repository boundary -- open tabs, TOTP, and LDAP auth route settings reads moved behind the current - settings repository boundary -- host metrics polling settings reads moved behind the current settings - repository boundary -- backend startup settings reads moved behind the current settings repository - boundary -- user deletion cleanup now removes per-user settings through - `SettingsRepository.deleteLike` -- password reset route reset code and temporary token settings access moved - behind `SettingsRepository` -- OIDC utility legacy config fallback reads `oidc_config` through - `SettingsRepository` -- user route registration/password flags and OIDC config administration moved - behind the current settings repository boundary -- OIDC authorize/callback temporary state and auto-provision reads moved behind - the current settings repository boundary -- user login settings reads moved behind the current settings repository - boundary, completing direct `settings` access cleanup in `routes/users.ts` -- user encryption metadata in `utils/user-crypto.ts` moved behind the current - settings repository boundary -- database startup and schema migration defaults in `database/db/index.ts` - moved to local raw settings helpers -- database import/export settings handling in `database/database.ts` moved to - local helper boundaries -- core `auth-manager.ts` session create/read/update/revoke/list paths started - using the current session repository boundary -- remaining `auth-manager.ts` session cleanup/middleware/logout paths and - `user-session-routes.ts` single-session lookup moved behind the current - session repository boundary -- current user repository factory/write-save hook added, and - `user-admin-routes.ts` list/admin promotion/admin removal/admin-create user - paths moved behind the current user repository boundary -- low-risk `routes/users.ts` current-user lookup and admin gate checks moved - behind the current user repository boundary -- user registration, self-delete, password change hash updates, and admin - delete-user lookup paths in `routes/users.ts` moved behind the current user - repository boundary, with first-user admin creation kept transactional inside - `UserRepository` -- traditional login username lookup and `auth-manager.ts` admin user checks - moved behind the current user repository boundary -- GitHub and standard OIDC callback user lookup/create/rollback/profile/admin - sync writes moved behind the current user repository boundary, removing direct - Drizzle `users` table access from `routes/users.ts`, `user-admin-routes.ts`, - and `auth-manager.ts` -- API key create/list/delete and API key authentication last-used updates moved - behind the current API key repository boundary -- trusted device check/add/remove and TOTP trusted-device cleanup moved behind - the current trusted device repository boundary -- user session routes moved user/admin lookups and admin session username - enrichment behind the current user repository boundary -- vault admin checks, Termix ID audit username lookup, permission manager admin - checks, and user data export user lookup moved behind the current user - repository boundary -- SSH credential OIDC username expansion and tmux monitor audit actor username - lookup moved behind the current user repository boundary -- user settings route admin checks and audit actor username lookups moved behind - the current user repository boundary -- ACME SSL route admin checks and audit actor username lookups moved behind the - current user repository boundary -- audit log route admin checks moved behind the current user repository boundary -- OIDC account link/unlink route user lookups and OIDC field updates moved - behind the current user repository boundary -- password reset route user lookups, password hash updates, and TOTP reset - fields moved behind the current user repository boundary -- user deletion helper now removes sessions through the current session - repository and the final user record through the current user repository -- snippet create/update/delete audit actor username lookups moved behind the - current user repository boundary -- LDAP login existing-user lookup, encryption rollback delete, admin sync, and - display-name sync moved behind the current user repository boundary -- TOTP setup/enable/disable/backup-code/login verification user updates and - session revocation moved behind the current user/session repository - boundaries -- RBAC host sharing, role assignment, and snippet sharing target-user existence - checks moved behind the current user repository boundary, with existing - RBAC/snippet owner username joins retained -- RBAC role list/create/update/delete, user-role assignment/removal/listing, - and shared host/snippet role-id lookups moved behind the current role - repository boundary, with existing RBAC/share credential joins retained -- permission manager role permission aggregation, role-id lookups for shared - host access, and admin role checks moved behind the current role repository - boundary -- RBAC host/snippet access-list read models moved behind the current RBAC access - repository boundary, and snippet route shared-access role-id lookups moved - behind the current role repository boundary -- RBAC shared host/shared snippet read models and the main snippet - shared-snippet read model moved behind the current RBAC access repository - boundary -- RBAC host/snippet access grant, revoke, and direct host-access credential - override writes moved behind the current RBAC access repository boundary, - with shared credential material creation retained in the existing manager -- permission manager host-access expiration cleanup, shared host-access lookup, - and last-access timestamp updates moved behind the current RBAC access - repository boundary -- repository rollout guard added through `DATABASE_LAYER_REPOSITORY_ROLLOUT` - for the migrated settings/users/sessions/API-key/trusted-device/role/RBAC-access - slice - -Keep it small. Do not wire host or credential routes into the new repositories in -the same first implementation commit. - -## 12. Current Unknowns - -- Exact Drizzle multi-dialect strategy needs a spike. -- The project may need a migration generator or a custom migration runner. -- Some raw SQL in `routes/users.ts`, `alert-rules-routes.ts`, and `db/index.ts` must be rewritten or isolated. -- `settings` contains mixed public and sensitive values; key-level classification is required. -- `homepage_items.config`, `notification_channels.config`, and tunnel configs may contain embedded secrets. -- Legacy encrypted snapshot fixtures need to be created before migration implementation. - -## 13. Decision Log - -- Default upgrade target should be persistent SQLite, not PostgreSQL/MySQL. -- PostgreSQL/MySQL migration should be explicit and initially manual/experimental. -- Runtime SSH/WebSocket/tunnel state remains memory-only. -- Field-level encryption is mandatory for every database backend. -- Field-level encryption must use a stable record id; temporary encryption - contexts are forbidden for newly written repository data. -- Repository migration should start with settings/users/sessions/hosts/credentials. diff --git a/readme/DATABASE-LAYER-REFACTOR-PLAN.md b/readme/DATABASE-LAYER-REFACTOR-PLAN.md deleted file mode 100644 index fdc0d957..00000000 --- a/readme/DATABASE-LAYER-REFACTOR-PLAN.md +++ /dev/null @@ -1,1187 +0,0 @@ -# Database Layer Refactor Plan - -Status: Draft -Branch: `feature/database-layer-refactor` -Target: Replace the current in-memory encrypted SQLite snapshot model with a persistent, secure, multi-database architecture. - -Phase 0 audit: [`DATABASE-LAYER-PHASE-0-AUDIT.md`](./DATABASE-LAYER-PHASE-0-AUDIT.md) -Gray rollout guide: [`DATABASE-LAYER-GRAY-ROLLOUT.md`](./DATABASE-LAYER-GRAY-ROLLOUT.md) - -Phase 1 status: database runtime config and SQLite adapter skeleton have started under -`src/backend/database/runtime/`. This code is not wired into the existing production -database initialization yet. - -Repository status: the first repository skeletons, `SettingsRepository`, -`UserRepository`, `SessionRepository`, `HostRepository`, and -`CredentialRepository`, have started under `src/backend/database/repositories/`. -`RoleRepository` has also started for the RBAC role/user-role slice, and these -repositories are covered by SQLite-backed tests. `RbacAccessRepository` has -started for RBAC host/snippet access-list read models. - -Field encryption status: repository-level field encryption boundary tests have -started. New field encryption requires a stable record id and must not invent a -temporary encryption context. - -Settings migration status: a first low-risk production route slice now uses -`SettingsRepository` through the current SQLite runtime context. Legacy in-memory -runtime writes still force a snapshot save through `DatabaseSaveTrigger`. -The user settings route has also moved its direct `settings` table reads and -writes behind `SettingsRepository`. -Host metrics settings routes now use the same repository boundary for global -monitoring defaults and history retention settings. -ACME SSL settings route also uses the repository boundary for its persisted -configuration key. -Terminal route session settings and command history global flag now read/write -through `SettingsRepository`. -Tailscale device route now reads its API key through `SettingsRepository`. -Guacamole route and WebSocket server now read `guac_url` through the current -settings repository boundary. -Authentication token expiry and terminal session timeout reads now use the -current settings repository boundary. -Open tabs, TOTP, and LDAP auth routes now read their settings through the -current settings repository boundary. -Host metrics polling now reads global interval and retention settings through -the current settings repository boundary. -Backend startup now reads persisted log level and Guacamole enablement through -the current settings repository boundary. -User deletion cleanup now removes per-user settings through -`SettingsRepository.deleteLike`. -Password reset route now stores reset codes and temporary reset tokens through -`SettingsRepository`. -OIDC utility legacy config fallback now reads `oidc_config` through -`SettingsRepository`. -User route registration/password flags and OIDC config administration now use -the current settings repository boundary. -OIDC authorize/callback temporary state and auto-provision reads now use the -current settings repository boundary. -User login settings reads now use the current settings repository boundary, so -`routes/users.ts` no longer reads or writes `settings` directly. -User encryption metadata in `utils/user-crypto.ts` now reads and writes KEK/DEK -settings through the current settings repository boundary. -Database import/export user unlock and admin checks now use the current user -repository boundary. -Auth login lazy user-field encryption migration now calls the `DataCrypto` -current-runtime migration boundary instead of opening SQLite in -`auth-manager.ts`. -Current user-field encryption migration now resolves SQLite through -`createCurrentUserEncryptionMigrationStore` and the shared current runtime -helper, keeping `DataCrypto` on the migration-store interface. -User, admin, TOTP, OIDC account, and credential migration explicit saves now -use `DatabaseSaveTrigger` instead of importing the SQLite snapshot save function -from routes. -Current SQLite snapshot save trigger now initializes after database startup -regardless of file-encryption mode, and backend shutdown uses that save -boundary. -Direct SQLite snapshot save-function imports are now isolated inside -`database/db/index.ts`; current user-field migration saves through -`DatabaseSaveTrigger`. -Database import SQLite foreign-key toggling is isolated behind the -`withSqliteForeignKeysDisabled` runtime boundary and restores constraints in a -`finally` path; the current variant resolves SQLite through the shared current -runtime helper. -Database import now uses the current SQLite foreign-key boundary without -importing `getDb()` in the route handler. -Settings, user, host, API key, session, trusted device, audit log, user -preference, open tab, dismissed alert, homepage layout/item, and network -topology, dashboard service link, session recording, command history, recent -activity, transfer recent, and file-manager bookmark current repository -factories plus C2S tunnel preset, tmux session tag, OPKSSH token, Vault -token/profile, SSH credential usage, role, SSO provider, host folder, alert, -host health, host metrics preference/history, credential, host resolution, -snippet, RBAC access, shared credential, Termix ID identity/CA, and user data -export current repository factories now share `current-repository-runtime` for -SQLite context and write-save hooks. -Database startup and schema migration defaults in `database/db/index.ts` now use -local raw settings helpers instead of scattered settings SQL. -Database import/export settings handling in `database/database.ts` now uses -local helper boundaries for export filtering and admin import upserts. -Those import/export settings helpers now use the current settings repository -boundary. -Session repository now has a current-runtime factory and write-save hook, and -core `auth-manager.ts` session create/read/update/revoke/list paths have started -using it. -Remaining `auth-manager.ts` session cleanup/middleware/logout paths and -`user-session-routes.ts` single-session lookup now use the current session -repository boundary. -User repository now has a current-runtime factory and write-save hook, and -`user-admin-routes.ts` list/admin promotion/admin removal/admin-create user -paths now use it for `users` table reads and writes. -Low-risk `routes/users.ts` current-user lookup and admin gate checks now use the -current user repository boundary. -User registration, self-delete, password change hash updates, and admin -delete-user lookup paths in `routes/users.ts` now use the current user -repository boundary while preserving the first-user admin transaction inside -`UserRepository`. -Traditional login username lookup and `auth-manager.ts` admin user checks now -use the current user repository boundary. -Credential list, folder list, detail reads, update lookup/readback, host route -credential resolution reads, folder rename writes, apply usage writes, shared -credential source row reads, credential delete lookup/delete, and -credential-host list reads now use current credential and host resolution -repository boundaries. -Database export host and credential read/decryption paths now use current host -and credential repository boundaries. -Database import host and credential duplicate checks plus encrypted creates now -use current host and credential repository boundaries. -Database import/export file-manager recent/pinned/shortcut target reads and -writes now use the current file-manager bookmark repository boundary. -Credential create/update encrypted writes now use the current credential -repository boundary, including system-key copies for shared credentials. -Credential delete host cleanup and apply-to-host writes now use the current host -repository boundary. -Credential system-key copy backfill migration now uses current credential and -shared credential repository boundaries. -Legacy user field-encryption migration SQL is now centralized behind -`RawSqliteUserEncryptionMigrationStore`. -Legacy unencrypted SQLite copy/verification during encrypted-file migration is -now centralized behind `LegacySqliteDatabaseCopyStore`. -Termix ID credential lookup, generated credential persistence, and generated -credential cleanup now use the current credential repository boundary. -Termix ID identity handle CRUD/resolution, public key publish/list/update/delete, -linked credential lookup, and certificate target key lookup now use the current -Termix ID repository boundary. -Termix ID CA public lookup, encrypted private-key create/rotate/delete, and -certificate signing reads now use the current Termix ID CA repository boundary. -Host route update readback, single-host fetch, password-field fetch, and host -export reads now use the current host resolution repository boundary. -Host route create/update encrypted writes now use the current host repository -boundary. -Host route update-state and delete-audit host reads now use the current host -resolution repository boundary. -Host route delete final host-row writes and audit actor username lookups now use -the current host and user repository boundaries. -Host route own/shared list assembly reads now use the current host resolution -repository boundary while preserving route-level own-host decryption. -Host bulk-update state reads, non-sensitive bulk flag/config writes, and JSON -plus SSH-config import encrypted create/update writes now use the current host -repository boundary, while host bulk import overwrite lookup plus credential -fallback reads use current host resolution and credential repository boundaries. -Host autostart enable, disable, status, and endpoint-host resolution paths now -use the current host repository boundary. -Guacamole host token host and protocol credential reads now use the current -host resolution repository boundary while preserving request-user decryption -behavior for credential fallback. -Snippet folder list/create/metadata/rename/delete, owned lookup, visible-list -owned reads, reorder, create/update/delete, export reads, and bulk import now -use the current snippet repository boundary. -User cleanup and password-reset data-discard host, credential, snippet, snippet -folder, and host folder deletes now use current repository boundaries. -GitHub and standard OIDC callback user lookup/create/rollback/profile/admin -sync writes now use the current user repository boundary, removing direct -Drizzle `users` table access from `routes/users.ts`, `user-admin-routes.ts`, -and `auth-manager.ts`. -User setup/count/db-health checks, password-login TOTP guard reads, and -last-admin delete guard reads now use the current user repository boundary. -API key create/list/delete and API key authentication last-used updates now use -the current API key repository boundary. -Trusted device check/add/remove and TOTP trusted-device cleanup now use the -current trusted device repository boundary. -User session routes now use the current user repository boundary for user/admin -lookups and admin session username enrichment. -Vault admin checks, Termix ID audit actor username lookup, permission manager -admin checks, and user data export user lookup now use the current user -repository boundary. -SSH credential OIDC username expansion and tmux monitor audit actor username -lookup now use the current user repository boundary. -User settings route admin checks and audit actor username lookups now use the -current user repository boundary, removing direct DB/schema imports from that -route module. -ACME SSL route admin checks and audit actor username lookups now use the current -user repository boundary, removing direct DB/schema imports from that route -module. -Audit log route admin checks now use the current user repository boundary, -removing direct `users` access from that route module. -OIDC account link/unlink route user lookups and OIDC field updates now use the -current user repository boundary, removing direct `users` access from that route -module. -Password reset route user lookups, password hash updates, TOTP reset fields, and -per-user encrypted-data cleanup now use current repository boundaries. -User deletion helper now removes sessions, API keys, trusted devices, dashboard -links, homepage layout/items, host health checks/history, host metrics -preferences, C2S presets, Vault tokens/profiles, roles, alert data, audit logs, -Termix ID identity/CA, tmux session tags, encrypted data, UI state, and related -per-user rows through current repository boundaries before deleting the final -user record. -Snippet create/update/delete audit actor username lookups now use the current -user repository boundary; the remaining snippet `users` usage is the shared -snippet owner join. -LDAP login first-user provisioning, admin-group user creation, existing-user -lookup, encryption rollback delete, admin sync, and display-name sync now use -the current user repository boundary. -TOTP setup/enable/disable/backup-code/login verification user updates and -session revocation now use the current user/session repository boundaries. -RBAC host sharing, role assignment, and snippet sharing target-user existence -checks now use the current user repository boundary while retaining existing -RBAC/snippet owner username joins. -RBAC role list/create/update/delete, user-role assignment/removal/listing, and -shared host/snippet role-id lookups now use the current role repository -boundary while retaining existing RBAC/share credential joins in the route. -Permission manager role permission aggregation, role-id lookups for shared host -access, and admin role checks now use the current role repository boundary. -RBAC host/snippet access-list read models now use the current RBAC access -repository boundary, and snippet route shared-access role-id lookups now use the -current role repository boundary. -RBAC shared host/shared snippet read models and the main snippet shared-snippet -read model now use the current RBAC access repository boundary. -RBAC route host/snippet owner checks and direct host-access credential existence -reads now use current host, snippet, and credential repository boundaries. -RBAC host/snippet access grant, revoke, and direct host-access credential -override writes now use the current RBAC access repository boundary while -retaining shared credential material creation in the existing manager. -Shared credential material create/update/delete, pending re-encryption, and -user cleanup persistence now use the current shared credential repository -boundary while keeping encryption/decryption logic in the existing manager. -Permission manager host-access expiration cleanup, shared host-access lookup, -and last-access timestamp updates now use the current RBAC access repository -boundary. -RBAC role assignment now reads role-shared host credential sources through the -current RBAC access repository boundary while keeping shared credential material -creation in the existing manager. -Snippet single-item shared-access checks now use the current RBAC access -repository boundary. -Shared credential manager host-access lookups for role-member credential -creation, shared credential reads, and pending re-encryption owner discovery now -use the current RBAC access repository boundary. -Host route host-access cleanup writes for credential removal and host deletion -now use the current RBAC access repository boundary. -Host route shared-host list access checks now use the current role and RBAC -access repository boundaries while host row loading stays local to the host -route. -Credential deletion, folder deletion, and user deletion host-access cleanup -writes now use the current RBAC access repository boundary. -Shared credential manager role-member lookups now use the current role -repository boundary. -User deletion role-assignment cleanup now uses the current role repository -boundary. -User admin route role sync and admin-created default role assignment now use -the current role repository boundary. -LDAP login default role assignment and admin-role sync now use the current role -repository boundary. -Local, GitHub OIDC, and standard OIDC user default role assignment plus OIDC -admin-group role sync now use the current role repository boundary. -SSO provider listing, management, OIDC config loading, and LDAP provider -validation now use the current SSO provider repository boundary. -Audit log writes, filtered reads, action lists, and user cleanup now use the -current audit log repository boundary. -User preferences read/write and user cleanup now use the current user -preference repository boundary. -Open tab restore/upsert/sync/update/delete and user cleanup now use the current -open tab repository boundary. -Dismissed alert read/dismiss/undismiss/export and user cleanup now use the -current dismissed alert repository boundary. -Homepage layout read/write now uses the current homepage layout repository -boundary. -Homepage item list/create/update/delete now uses the current homepage item -repository boundary. -Network topology read/write and user cleanup now use the current network -topology repository boundary. -Dashboard service link list/create/update/delete now uses the current dashboard -service link repository boundary. -Session recording create/list/read/content/delete/prune and host/folder/user -cleanup now uses the current session recording repository boundary. -Command history save/list/delete and host/user cleanup now use the current -command history repository boundary. -Dashboard recent activity list/log/trim/reset and host/user cleanup now use the -current recent activity repository boundary, with host ownership/share checks -using current host resolution, role, and RBAC access repository boundaries. -SSH credential usage writes and host/user cleanup now use the current SSH -credential usage repository boundary. -Transfer recent list/upsert/prune/export and host/folder/user cleanup now use -the current transfer recent repository boundary. -File manager recent/pinned/shortcut list/create/delete/export and -host/folder/user/password-reset cleanup now use the current file manager -bookmark repository boundary. -C2S tunnel preset list/create/update/delete now uses the current C2S tunnel -preset repository boundary. -Tmux session tag list/rename/delete/replace now uses the current tmux session -tag repository boundary. -OPKSSH token upsert/read/touch/delete and user cleanup now use the current -OPKSSH token repository boundary. -Database import/export dismissed alert reads and writes now use the current -dismissed alert repository boundary. -Database export SSH credential usage reads now use the current SSH credential -usage repository boundary. -Vault token upsert/read/touch/delete now uses the current Vault token repository -boundary. -Vault profile list/create/update/delete and profile lookup now use the current -Vault profile repository boundary. -Host metrics layout preference read/upsert and statsConfig widget sync now use the current host metrics -preference repository boundary. -Host health check config and history read/write now use the current host health -repository boundary. -Host metrics history record/prune/query now uses the current host metrics -history repository boundary. -Alert notification channels, rules, linked channels, firings, and alert engine -persistence reads/writes now use the current alert repository boundary. -User data export host and credential read models now use the current user data -export repository boundary. -SSH folder list, metadata upsert, rename, and folder host deletion writes now -use the current host folder repository boundary. -Host, jump-host, Docker SSH, Proxmox discovery, Docker console jump-host, -file-manager activity, host metrics, terminal SSH auth, and tunnel endpoint -credential plus credential deployment and command history host-flag resolution -plus snippet execution, terminal OPKSSH/activity, Vault OIDC profile, and -Wake-on-LAN host, internal host list, host-key verification metadata, -credential, and shared override read/write models now use the current host -resolution repository boundary. -Host metrics, host metrics viewer, tmux monitor, Docker, tunnel, and Proxmox -unlock gates now use `DataCrypto` directly instead of `SimpleDBOps`. -Legacy `SimpleDBOps` compatibility helper has been removed after migrated route -and utility paths stopped importing it. -Permission manager host owner checks now use the current host resolution -repository boundary. - -Gray rollout status: the branch is expanding the repository boundary slice while -keeping every migrated domain behind `DATABASE_LAYER_REPOSITORY_ROLLOUT`, which -supports `all`, `off`, and a comma-separated allowlist for controlled gray -targets. - -## 1. Background - -Termix currently uses an encrypted SQLite snapshot model: - -```text -db.sqlite.encrypted - -> decrypt on startup - -> open as in-memory SQLite - -> runtime reads/writes memory database - -> serialize whole database - -> encrypt and write snapshot back to disk -``` - -This works well for small single-instance deployments, but it creates several long-term problems: - -- Recent writes can be lost if the process crashes before the snapshot save runs. -- Direct database writes can forget to call `DatabaseSaveTrigger`, causing data to exist only in memory. -- Saving requires serializing and encrypting the whole database, so cost grows with database size. -- The architecture cannot safely support multiple backend instances. -- It blocks clean PostgreSQL/MySQL support because business code depends directly on SQLite and Drizzle SQLite tables. - -The refactor should keep Termix secure while moving persistence to real database writes. - -## 2. Goals - -- Support persistent database backends: - - SQLite - - PostgreSQL - - MySQL/MariaDB -- Keep sensitive data encrypted at the field level. -- Remove dependence on full in-memory database snapshots. -- Introduce a database adapter boundary. -- Move direct database access behind repositories/services. -- Provide a safe upgrade path from the existing encrypted SQLite snapshot. -- Preserve existing user data, encryption keys, credentials, hosts, settings, audit records, and feature data. -- Keep runtime-only state in memory: - - SSH clients - - WebSocket sessions - - tunnels - - metrics polling sessions - - transfer runtime state -- Make migrations deterministic, idempotent, and rollback-safe. - -## 3. Non-Goals - -- Do not rewrite the whole backend at once. -- Do not force existing users to configure PostgreSQL or MySQL during normal upgrade. -- Do not remove SQLite support. -- Do not store SSH live sessions, WebSocket handles, or tunnel process handles in the database. -- Do not encrypt every field blindly. Searchable and sortable metadata should stay queryable unless it is sensitive. -- Do not change user-facing behavior unless required by the storage model. - -## 4. Current Architecture - -### 4.1 Database Runtime - -- Database initialization lives in `src/backend/database/db/index.ts`. -- `actualDbPath` is `:memory:`. -- Encrypted database files are decrypted into memory on startup. -- Runtime database access uses Drizzle over `better-sqlite3`. -- On save, the whole memory database is serialized and encrypted. - -### 4.2 Save Behavior - -- Historical `SimpleDBOps.insert/update/delete` writes used to trigger - `DatabaseSaveTrigger`; migrated write paths now use repository save hooks. -- Some routes call `getDb().insert/update/delete` or raw SQLite directly. -- Direct writes must manually call `DatabaseSaveTrigger.triggerSave()`. -- This creates a persistence gap when direct writes forget to trigger saves. - -### 4.3 Runtime State - -The following state is intentionally memory-only today and should remain memory-only: - -- Terminal sessions in `terminal-session-manager.ts` -- File manager sessions in `file-manager.ts` -- Metrics sessions and caches in `host-metrics-sessions.ts` and `host-metrics-state.ts` -- Tunnel runtime maps in `tunnel.ts` -- Pending OPKSSH/Vault/OIDC authentication sessions - -### 4.4 Schema State - -- Schema is declared with `sqliteTable` in `src/backend/database/db/schema.ts`. -- Additional schema setup is done manually through `CREATE TABLE IF NOT EXISTS`. -- Schema migration uses many `addColumnIfNotExists` calls. -- This is tightly coupled to SQLite. - -## 5. Target Architecture - -```text -Backend Feature Code - -> Service Layer - -> Repository Layer - -> Database Adapter - -> SQLite - -> PostgreSQL - -> MySQL/MariaDB - -> Field Encryption Boundary - -> Persistent Database -``` - -### 5.1 Database Adapter - -Introduce a database runtime module responsible for: - -- Reading database configuration. -- Creating the correct database client. -- Running migrations. -- Exposing a typed query context. -- Handling database-specific transaction behavior. -- Hiding dialect-specific differences from business logic. - -Proposed configuration: - -```env -DB_TYPE=sqlite -DATABASE_URL=file:/app/data/termix.sqlite - -# or -DB_TYPE=postgres -DATABASE_URL=postgres://termix:password@postgres:5432/termix - -# or -DB_TYPE=mysql -DATABASE_URL=mysql://termix:password@mysql:3306/termix - -SYSTEM_KEY=... -``` - -SQLite should remain the default. - -### 5.2 Repository Layer - -Business code should stop calling `getDb()` directly. - -Examples: - -```ts -hostRepository.getById(userId, hostId); -hostRepository.create(userId, input); -credentialRepository.resolveForHost(userId, hostId); -sessionRepository.create(userId, sessionData); -auditRepository.write(event); -settingsRepository.get(key); -``` - -Repositories should own: - -- Query construction -- Data mapping -- Field encryption/decryption -- Stable record-id enforcement before encrypting field values -- Permission-aware reads where appropriate -- Transaction participation -- Database-specific compatibility - -### 5.3 Service Layer - -Services should own business workflows: - -- Host creation/update/delete -- Credential resolution -- User/session lifecycle -- RBAC operations -- Import/export -- Migration orchestration - -Services can call multiple repositories inside one transaction. - -### 5.4 Runtime State Boundary - -Runtime state should remain in process memory and should not be migrated into the database: - -- SSH connection objects -- PTY streams -- SFTP handles -- WebSocket instances -- Timers -- Child processes -- Pending MFA/auth prompts - -The database should store only durable metadata: - -- session recording metadata -- audit events -- user open tabs, if still needed -- tunnel definitions, not live tunnel handles -- metrics history, not active polling state - -## 6. Security Model - -### 6.1 Keep Field-Level Encryption - -Sensitive fields must be encrypted before entering any persistent database: - -- SSH passwords -- SSH private keys -- SSH key passphrases -- credential secrets -- TOTP secrets and backup codes -- OAuth/OIDC client secrets -- Vault tokens -- OPKSSH tokens/cert cache secrets -- API tokens or token material -- RDP/VNC/Telnet passwords - -### 6.2 Keep Queryable Metadata Plain - -Some data should remain queryable: - -- numeric IDs -- user IDs -- host names -- folders -- tags -- enable flags -- timestamps -- connection type -- port numbers -- status/config flags - -For fields like host IP/domain, decide explicitly: - -- Plaintext improves search, sorting, and connection setup. -- Encrypted improves confidentiality. -- If encrypted, consider additional blind indexes for search. - -### 6.3 Envelope Encryption - -Recommended model: - -```text -SYSTEM_KEY - -> wraps application/database keys - -> wraps per-user data keys - -> per-user data keys encrypt user-owned secrets -``` - -Each encrypted field should include enough metadata for future rotation: - -```json -{ - "v": 2, - "alg": "aes-256-gcm", - "kid": "user-key-id", - "iv": "...", - "tag": "...", - "ct": "..." -} -``` - -### 6.4 Key Rotation - -The new design should support: - -- Detecting encryption version. -- Reading old ciphertext. -- Writing new ciphertext. -- Lazy migration on write. -- Optional explicit re-encryption job. - -### 6.5 Database-Level Encryption - -Field-level encryption is required for all database types. - -Optional database/file encryption: - -- SQLite can optionally use encrypted file storage later. -- PostgreSQL/MySQL should rely on field encryption plus deployment-level disk encryption/TLS. -- Do not require full database encryption for correctness. - -## 7. Database Support Strategy - -### 7.1 SQLite - -SQLite remains the default for simple self-hosted installs. - -Requirements: - -- Direct persistent SQLite file, not in-memory snapshot. -- WAL mode if safe for the deployment model. -- Proper shutdown handling. -- No full database serialize on every save. - -### 7.2 PostgreSQL - -PostgreSQL should be the preferred production multi-user backend. - -Requirements: - -- Connection pool. -- Transaction support. -- Native boolean/timestamp handling. -- JSONB where useful. -- Migration support. -- TLS support through connection string options. - -### 7.3 MySQL/MariaDB - -MySQL/MariaDB support should be implemented after SQLite and PostgreSQL are stable. - -Requirements: - -- Connection pool. -- Compatible migrations. -- JSON/text behavior reviewed. -- Timestamp/default behavior reviewed. -- `RETURNING` differences handled at repository/adapter level. - -### 7.4 Dialect Differences to Handle - -- Boolean storage -- Auto-increment IDs -- Timestamp defaults -- JSON columns -- `RETURNING` -- Upsert syntax -- Case sensitivity and collations -- Foreign key enforcement -- Transaction isolation -- Raw SQL fragments - -## 8. Migration Strategy - -### 8.1 Default Upgrade Path - -Default user upgrade should not require external database setup. - -Recommended default: - -```text -old encrypted SQLite snapshot - -> decrypt with existing logic - -> normalize old schema - -> migrate to new persistent SQLite - -> keep old snapshot as backup -``` - -PostgreSQL/MySQL migration should be explicit: - -```bash -termix migrate-db --from sqlite --to postgres -termix migrate-db --from sqlite --to mysql -``` - -or later through an admin UI. - -### 8.2 Backup Rules - -Before migration: - -- Verify source database can be decrypted. -- Verify target database can be connected. -- Verify target schema can be migrated. -- Create a timestamped backup of the old encrypted database. -- Do not delete the old encrypted snapshot automatically. - -Suggested backup naming: - -```text -db.sqlite.encrypted.pre-db-refactor-YYYYMMDD-HHmmss -``` - -### 8.3 Migration Marker - -New databases should store migration state: - -```text -schema_migrations - id - version - name - applied_at - checksum - -system_migrations - key - value - applied_at -``` - -The old snapshot migration should have a one-shot marker: - -```text -legacy_snapshot_migrated = true -legacy_snapshot_source_hash = ... -legacy_snapshot_migrated_at = ... -``` - -### 8.4 Idempotency - -Migration must be safe to rerun after interruption. - -Rules: - -- Never destroy the source before full success. -- Use unique keys or deterministic IDs where possible. -- Commit in table-level or batch-level transactions. -- Write migration markers only after successful commit. -- On startup, detect partial migration and either resume or fail safely. - -### 8.5 Dry Run - -Add a dry-run mode: - -```bash -termix migrate-db --dry-run -``` - -Dry run should report: - -- Source database detected -- Source schema version -- Target database type -- Target reachability -- Tables to migrate -- Row counts -- Unsupported old schema issues -- Estimated warnings - -### 8.6 Old Version Compatibility - -Users may upgrade from older Termix versions. - -Migration must: - -- Run old schema normalization first. -- Add missing columns in the legacy reader if needed. -- Handle absent optional tables. -- Handle older ciphertext formats. -- Handle old settings keys. - -### 8.7 Failure Behavior - -If migration fails: - -- New app should not start with an empty database. -- Old encrypted snapshot should remain untouched. -- Database-layer gray startup creates a one-time pre-upgrade backup before - opening the existing SQLite snapshot, and fails closed if that backup cannot - be written unless `DATABASE_LAYER_SKIP_PREUPGRADE_BACKUP=1` is explicitly set. -- Error logs should identify the failed stage. -- User should get recovery instructions. -- Retrying should be safe. - -## 9. Implementation Phases - -### Phase 0: Audit and Design Lock - -Estimated time: 2-3 days - -Tasks: - -- Inventory all database tables. -- Inventory all `getDb()`, `getSqlite()`, raw SQL, and `DatabaseSaveTrigger` usage. -- Classify tables by domain: - - auth/users - - hosts/credentials - - RBAC/sharing - - terminal/session logs - - file manager - - snippets - - metrics/alerts - - homepage/preferences - - tunnels/proxmox/guacamole/vault/opkssh -- Mark sensitive fields. -- Decide SQLite/PostgreSQL/MySQL type mapping. -- Decide migration framework. - -Deliverables: - -- Database access inventory. -- Sensitive field map. -- Final adapter/repository interface proposal. - -### Phase 1: Adapter Foundation - -Estimated time: 3-5 days - -Tasks: - -- Introduce database config parser. -- Introduce adapter interface. -- Add SQLite persistent adapter. -- Add migration table. -- Add transaction helper. -- Keep old code path running. -- Add tests for config and adapter initialization. - -Deliverables: - -- `DB_TYPE=sqlite` persistent adapter works in isolation. -- No business routes migrated yet. - -### Phase 2: Repository Skeleton - -Estimated time: 4-6 days - -Tasks: - -- Create repository interfaces. -- Implement initial SQLite-backed repositories. -- Add encryption boundary helpers. -- Add repository tests with SQLite. -- Prevent new direct `getDb()` usage in new code. - -Initial repositories: - -- settings -- users -- sessions -- hosts -- credentials -- audit - -Deliverables: - -- Core repositories compile and pass tests. -- Existing routes can still run through compatibility shims. - -### Phase 3: Core Vertical Slice - -Estimated time: 1-2 weeks - -Tasks: - -- Migrate user/session auth flows. -- Migrate settings routes. -- Migrate hosts CRUD. -- Migrate credentials CRUD and credential resolution. -- Migrate host resolver and permission-critical reads. -- Add transaction coverage for multi-table operations. -- Verify field encryption behavior. - -Acceptance criteria: - -- User login/logout/session validation works. -- Host create/update/delete works. -- Credential create/update/delete/resolve works. -- Existing encrypted user data remains readable. -- No snapshot save trigger needed for migrated core paths. - -### Phase 4: Feature Data Migration - -Estimated time: 1-2 weeks - -Tasks: - -- RBAC and sharing -- folders/tags -- snippets and snippet access -- file manager bookmarks/recent/pinned/shortcuts -- dashboard/homepage/preferences/open tabs -- alerts and notification channels -- metrics preferences/history/health checks -- tunnels and C2S presets -- Proxmox config -- Guacamole config -- OPKSSH/Vault records -- audit logs -- API keys - -Acceptance criteria: - -- Direct feature routes use repositories or domain services. -- Growth tables have retention strategy where appropriate. -- Runtime-only data remains memory-only. - -### Phase 5: Multi-Database Support - -Estimated time: 1-2 weeks - -Tasks: - -- Add PostgreSQL adapter. -- Add MySQL/MariaDB adapter. -- Add dialect-specific schema/migrations. -- Add CI matrix for SQLite/PostgreSQL/MySQL if feasible. -- Replace SQLite-only raw SQL in migrated paths. -- Add Docker compose examples. - -Acceptance criteria: - -- Fresh install works on SQLite. -- Fresh install works on PostgreSQL. -- Fresh install works on MySQL/MariaDB. -- Core CRUD tests pass across supported databases. - -### Phase 6: Legacy Snapshot Migration - -Estimated time: 1 week - -Tasks: - -- Implement legacy encrypted snapshot reader. -- Normalize legacy schema in memory. -- Migrate legacy snapshot to new SQLite. -- Keep backup of old encrypted snapshot. -- Add dry-run. -- Add migration logs. -- Add recovery documentation. - -Acceptance criteria: - -- Existing encrypted SQLite snapshot migrates to new persistent SQLite. -- Migration can be retried safely. -- Failed migration leaves old data untouched. - -### Phase 7: External Database Migration Tool - -Estimated time: 3-5 days - -Tasks: - -- Add CLI or admin-only migration command. -- Support SQLite to PostgreSQL. -- Support SQLite to MySQL/MariaDB. -- Add dry-run and validation. -- Add clear rollback instructions. - -Acceptance criteria: - -- Existing SQLite install can be migrated to PostgreSQL/MySQL explicitly. -- Migration is not automatic unless user opts in. - -### Phase 8: Cleanup and Enforcement - -Estimated time: 3-5 days - -Tasks: - -- Remove in-memory snapshot runtime path. -- Remove `DatabaseSaveTrigger` from normal persistence. -- Keep only legacy import compatibility. -- Add lint/test guard against direct database access outside approved modules. -- Update docs. - -Acceptance criteria: - -- Business modules no longer import `getDb()` directly except approved infrastructure/repository files. -- `DatabaseSaveTrigger` is no longer required for normal data durability. -- Old snapshot code is isolated to legacy migration/import. - -## 10. Testing Plan - -### 10.1 Unit Tests - -- Encryption/decryption helpers -- Repository mapping -- Adapter config parsing -- Migration idempotency -- Legacy ciphertext compatibility -- Dialect-specific value conversion - -### 10.2 Integration Tests - -Run core flows against: - -- SQLite -- PostgreSQL -- MySQL/MariaDB - -Core flows: - -- user create/login/logout/session validation -- host CRUD -- credential CRUD and host resolution -- RBAC host sharing -- snippet CRUD -- settings update -- audit write/read - -### 10.3 Migration Tests - -Fixtures: - -- latest legacy encrypted snapshot -- older legacy snapshot with missing columns -- snapshot with no optional feature tables -- snapshot with encrypted credentials -- snapshot with large metrics/audit tables - -Checks: - -- row counts match -- sensitive fields decrypt after migration -- target app starts -- rerun does not duplicate data -- failed migration preserves source - -### 10.4 Manual QA - -- Fresh SQLite install -- Fresh PostgreSQL install -- Fresh MySQL install -- Upgrade from existing encrypted SQLite install -- Switch to external DB through explicit migration -- Docker deployment -- Electron/local deployment if supported - -## 11. Rollout Plan - -### Version N - -- Add new architecture behind SQLite default. -- Auto-migrate legacy encrypted snapshot to new persistent SQLite. -- PostgreSQL/MySQL marked experimental or manual. -- Keep legacy snapshot backup. - -### Version N+1 - -- Stabilize PostgreSQL/MySQL. -- Add admin UI or CLI migration tooling. -- Improve docs and diagnostics. - -### Version N+2 - -- Remove old snapshot runtime path. -- Keep legacy import compatibility only. - -## 12. User Upgrade Behavior - -Default upgrade should be boring: - -1. User updates Termix. -2. App detects old encrypted snapshot. -3. App creates backup. -4. App migrates to new persistent SQLite. -5. App starts normally. -6. Old snapshot remains available for recovery. - -External DB migration should be opt-in: - -1. User configures target database. -2. User runs dry-run. -3. User confirms migration. -4. App migrates data. -5. App writes new DB config marker. -6. App starts on external DB. - -## 13. Operational Considerations - -### 13.1 Backups - -- SQLite: backup file copy with app stopped, or online backup API. -- PostgreSQL/MySQL: recommend native backup tooling. -- Sensitive fields remain encrypted in backups. - -### 13.2 Observability - -Add logs for: - -- adapter selected -- migration start/end -- migration table/version -- row counts per table -- legacy snapshot backup path -- encryption version warnings -- failed migration stage - -### 13.3 Performance - -Expected improvements: - -- No full database serialization on every save. -- Large audit/metrics tables no longer increase snapshot save cost. -- External DBs can handle larger multi-user installs. - -New costs: - -- Network latency for external DBs. -- Connection pool tuning. -- More complex migrations. - -### 13.4 Deployment - -Docker should support: - -- default SQLite volume -- PostgreSQL compose example -- MySQL/MariaDB compose example -- clear `DATABASE_URL` examples - -## 14. Risk Register - -| Risk | Severity | Mitigation | -| ---------------------------------------------- | -------: | ----------------------------------------------------------- | -| User data cannot decrypt after migration | Critical | Preserve key model, add fixtures, dry-run decryption checks | -| Migration partially writes target DB | High | Transactions, migration markers, idempotent batches | -| Old snapshot overwritten or deleted | Critical | Never delete source automatically, backup before migration | -| Direct DB access remains scattered | High | Repository enforcement and lint guard | -| Dialect differences break features | High | Adapter tests and DB matrix | -| Sensitive fields accidentally stored plaintext | Critical | Central encryption boundary and sensitive field map | -| Large migrations block startup too long | Medium | Progress logs, batching, optional preflight | -| External DB configuration confuses users | Medium | Keep SQLite default, make external DB opt-in | -| Runtime state accidentally persisted | Medium | Explicit runtime/persistent boundary | - -## 15. Initial Work Breakdown - -Recommended first PRs: - -1. Add database access inventory and sensitive field map. (Started in `DATABASE-LAYER-PHASE-0-AUDIT.md`) -2. Add adapter interfaces and SQLite persistent adapter skeleton. -3. Add repository skeleton for settings/users/sessions/hosts/credentials. -4. Add field encryption boundary tests. (Started) -5. Migrate settings as the first low-risk vertical slice. (Started) -6. Migrate users/sessions. -7. Migrate hosts/credentials. -8. Add legacy snapshot migration dry-run. - -## 16. Open Decisions - -- Should host IP/domain be encrypted, plaintext, or plaintext with optional privacy mode? -- Should metrics history stay in the main database or support separate retention storage? -- Should session log content remain file-based with DB metadata only? -- Should PostgreSQL be considered stable before MySQL/MariaDB? -- Should external DB migration be CLI-only first, or include admin UI from the start? -- Should old encrypted snapshot import remain forever as an import feature? - -## 17. Estimated Total Effort - -Conservative estimate for full implementation: - -- Minimum: 3 weeks with reduced scope and SQLite/PostgreSQL focus. -- Realistic: 4-6 weeks for SQLite/PostgreSQL/MySQL, migration, tests, docs. -- Safer production-grade rollout: 6-8 weeks including extensive legacy fixtures and external DB QA. - -Recommended planning estimate: 5 weeks. - -## 18. Recommended Starting Point - -Do not start by replacing every `getDb()` call. - -Start with a vertical slice: - -```text -settings -> users/sessions -> hosts -> credentials -``` - -This validates: - -- adapter shape -- repository shape -- transaction shape -- field encryption -- migration pattern -- test strategy - -After that slice works, migrate the rest domain by domain. diff --git a/src/backend/dashboard.ts b/src/backend/dashboard.ts index 3c73f542..003f69f9 100644 --- a/src/backend/dashboard.ts +++ b/src/backend/dashboard.ts @@ -5,10 +5,12 @@ import { dashboardLogger } from "./utils/logger.js"; import { AuthManager } from "./utils/auth-manager.js"; import type { AuthenticatedRequest } from "../types/index.js"; import { dashboardServiceLinksRouter } from "./database/routes/dashboard-service-links-routes.js"; -import { createCurrentHostResolutionRepository } from "./database/repositories/current-host-resolution-repository.js"; -import { createCurrentRbacAccessRepository } from "./database/repositories/current-rbac-access-repository.js"; -import { createCurrentRecentActivityRepository } from "./database/repositories/current-recent-activity-repository.js"; -import { createCurrentRoleRepository } from "./database/repositories/current-role-repository.js"; +import { + createCurrentHostResolutionRepository, + createCurrentRbacAccessRepository, + createCurrentRecentActivityRepository, + createCurrentRoleRepository, +} from "./database/repositories/factory.js"; import { DataCrypto } from "./utils/data-crypto.js"; const app = express(); diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 88c19bf2..186c2fd8 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -34,15 +34,16 @@ import { DatabaseFileEncryption } from "../utils/database-file-encryption.js"; import { DatabaseMigration } from "../utils/database-migration.js"; import { UserDataExport } from "../utils/user-data-export.js"; import { AutoSSLSetup } from "../utils/auto-ssl-setup.js"; -import { createCurrentCredentialRepository } from "./repositories/current-credential-repository.js"; -import { createCurrentDismissedAlertRepository } from "./repositories/current-dismissed-alert-repository.js"; -import { createCurrentFileManagerBookmarkRepository } from "./repositories/current-file-manager-bookmark-repository.js"; -import { createCurrentHostRepository } from "./repositories/current-host-repository.js"; -import { createCurrentSettingsRepository } from "./repositories/current-settings-repository.js"; -import { createCurrentSshCredentialUsageRepository } from "./repositories/current-ssh-credential-usage-repository.js"; -import { createCurrentUserRepository } from "./repositories/current-user-repository.js"; -import { getRepositoryRolloutStatus } from "./repositories/repository-rollout.js"; -import { withCurrentSqliteForeignKeysDisabled } from "./runtime/sqlite-foreign-key-boundary.js"; +import { + createCurrentCredentialRepository, + createCurrentDismissedAlertRepository, + createCurrentFileManagerBookmarkRepository, + createCurrentHostRepository, + createCurrentSettingsRepository, + createCurrentSshCredentialUsageRepository, + createCurrentUserRepository, +} from "./repositories/factory.js"; +import { withCurrentSqliteForeignKeysDisabled } from "./repositories/sqlite-foreign-keys.js"; import { parseUserAgent } from "../utils/user-agent-parser.js"; import { getProxyAgent } from "../utils/proxy-agent.js"; import type { @@ -1895,7 +1896,6 @@ app.get( res.json({ migrationStatus: status, - repositoryRollout: getRepositoryRolloutStatus(), files: { unencryptedDbSize: unencryptedSize, encryptedDbSize: encryptedSize, diff --git a/src/backend/database/repositories/alert-repository.test.ts b/src/backend/database/repositories/alert-repository.test.ts index e4f6f044..3d0b7c52 100644 --- a/src/backend/database/repositories/alert-repository.test.ts +++ b/src/backend/database/repositories/alert-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { AlertRepository } from "./alert-repository.js"; describe("AlertRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("AlertRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/alert-repository.ts b/src/backend/database/repositories/alert-repository.ts index 8bb99dd2..9b55ca36 100644 --- a/src/backend/database/repositories/alert-repository.ts +++ b/src/backend/database/repositories/alert-repository.ts @@ -6,7 +6,7 @@ import { hosts, notificationChannels, } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; type AlertRuleRecord = typeof alertRules.$inferSelect; type NotificationChannelRecord = typeof notificationChannels.$inferSelect; diff --git a/src/backend/database/repositories/api-key-repository.test.ts b/src/backend/database/repositories/api-key-repository.test.ts index 29fc64c1..e8542efd 100644 --- a/src/backend/database/repositories/api-key-repository.test.ts +++ b/src/backend/database/repositories/api-key-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { ApiKeyRepository } from "./api-key-repository.js"; describe("ApiKeyRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("ApiKeyRepository", () => { async function createRepository(onWrite?: () => void): Promise<{ apiKeys: ApiKeyRepository; }> { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/api-key-repository.ts b/src/backend/database/repositories/api-key-repository.ts index eec17088..a24b880a 100644 --- a/src/backend/database/repositories/api-key-repository.ts +++ b/src/backend/database/repositories/api-key-repository.ts @@ -1,6 +1,6 @@ import { eq, and } from "drizzle-orm"; import { apiKeys, users } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type ApiKeyRecord = typeof apiKeys.$inferSelect; export type NewApiKeyRecord = typeof apiKeys.$inferInsert; diff --git a/src/backend/database/repositories/audit-log-repository.test.ts b/src/backend/database/repositories/audit-log-repository.test.ts index fb97bd09..ec418c6a 100644 --- a/src/backend/database/repositories/audit-log-repository.test.ts +++ b/src/backend/database/repositories/audit-log-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { AuditLogRepository } from "./audit-log-repository.js"; describe("AuditLogRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("AuditLogRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/audit-log-repository.ts b/src/backend/database/repositories/audit-log-repository.ts index 6356674a..6e9c86a9 100644 --- a/src/backend/database/repositories/audit-log-repository.ts +++ b/src/backend/database/repositories/audit-log-repository.ts @@ -1,6 +1,6 @@ import { and, asc, desc, eq, gte, inArray, lte, sql } from "drizzle-orm"; import { auditLogs } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type AuditLogRecord = typeof auditLogs.$inferSelect; export type NewAuditLogRecord = typeof auditLogs.$inferInsert; diff --git a/src/backend/database/repositories/c2s-tunnel-preset-repository.test.ts b/src/backend/database/repositories/c2s-tunnel-preset-repository.test.ts index 7c1119d1..c7643f94 100644 --- a/src/backend/database/repositories/c2s-tunnel-preset-repository.test.ts +++ b/src/backend/database/repositories/c2s-tunnel-preset-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { C2sTunnelPresetRepository } from "./c2s-tunnel-preset-repository.js"; describe("C2sTunnelPresetRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("C2sTunnelPresetRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/c2s-tunnel-preset-repository.ts b/src/backend/database/repositories/c2s-tunnel-preset-repository.ts index 1fe3b387..b0417132 100644 --- a/src/backend/database/repositories/c2s-tunnel-preset-repository.ts +++ b/src/backend/database/repositories/c2s-tunnel-preset-repository.ts @@ -1,6 +1,6 @@ import { and, asc, eq, sql } from "drizzle-orm"; import { c2sTunnelPresets } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type C2sTunnelPresetRecord = typeof c2sTunnelPresets.$inferSelect; diff --git a/src/backend/database/repositories/command-history-repository.test.ts b/src/backend/database/repositories/command-history-repository.test.ts index 3f8da3ea..7b2a732c 100644 --- a/src/backend/database/repositories/command-history-repository.test.ts +++ b/src/backend/database/repositories/command-history-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { CommandHistoryRepository } from "./command-history-repository.js"; describe("CommandHistoryRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("CommandHistoryRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/command-history-repository.ts b/src/backend/database/repositories/command-history-repository.ts index 5aa11d78..5bf72b3a 100644 --- a/src/backend/database/repositories/command-history-repository.ts +++ b/src/backend/database/repositories/command-history-repository.ts @@ -1,6 +1,6 @@ import { and, desc, eq, inArray, sql } from "drizzle-orm"; import { commandHistory } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type CommandHistoryRecord = typeof commandHistory.$inferSelect; diff --git a/src/backend/database/repositories/credential-repository.ts b/src/backend/database/repositories/credential-repository.ts index 0c4dfcb7..eacff71c 100644 --- a/src/backend/database/repositories/credential-repository.ts +++ b/src/backend/database/repositories/credential-repository.ts @@ -1,6 +1,6 @@ import { and, desc, eq, isNull, or, sql } from "drizzle-orm"; import { sshCredentials, sshCredentialUsage } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; import { DataCrypto } from "../../utils/data-crypto.js"; import { SystemCrypto } from "../../utils/system-crypto.js"; diff --git a/src/backend/database/repositories/current-alert-repository.ts b/src/backend/database/repositories/current-alert-repository.ts deleted file mode 100644 index 1b83ea97..00000000 --- a/src/backend/database/repositories/current-alert-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { AlertRepository } from "./alert-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentAlertRepository(): AlertRepository { - assertRepositoryRolloutDomainEnabled("alerts"); - - return new AlertRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("alert_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-api-key-repository.ts b/src/backend/database/repositories/current-api-key-repository.ts deleted file mode 100644 index 8930400d..00000000 --- a/src/backend/database/repositories/current-api-key-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiKeyRepository } from "./api-key-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentApiKeyRepository(): ApiKeyRepository { - assertRepositoryRolloutDomainEnabled("api_keys"); - - return new ApiKeyRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("api_key_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-audit-log-repository.ts b/src/backend/database/repositories/current-audit-log-repository.ts deleted file mode 100644 index 263ebf75..00000000 --- a/src/backend/database/repositories/current-audit-log-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { AuditLogRepository } from "./audit-log-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentAuditLogRepository(): AuditLogRepository { - assertRepositoryRolloutDomainEnabled("audit_logs"); - - return new AuditLogRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("audit_log_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-c2s-tunnel-preset-repository.ts b/src/backend/database/repositories/current-c2s-tunnel-preset-repository.ts deleted file mode 100644 index 7053b56f..00000000 --- a/src/backend/database/repositories/current-c2s-tunnel-preset-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { C2sTunnelPresetRepository } from "./c2s-tunnel-preset-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentC2sTunnelPresetRepository(): C2sTunnelPresetRepository { - assertRepositoryRolloutDomainEnabled("c2s_tunnel_presets"); - - return new C2sTunnelPresetRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("c2s_tunnel_preset_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-command-history-repository.ts b/src/backend/database/repositories/current-command-history-repository.ts deleted file mode 100644 index 46e67578..00000000 --- a/src/backend/database/repositories/current-command-history-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { CommandHistoryRepository } from "./command-history-repository.js"; - -export function createCurrentCommandHistoryRepository(): CommandHistoryRepository { - assertRepositoryRolloutDomainEnabled("command_history"); - - return new CommandHistoryRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("command_history_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-credential-repository.ts b/src/backend/database/repositories/current-credential-repository.ts deleted file mode 100644 index 46f52fe8..00000000 --- a/src/backend/database/repositories/current-credential-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CredentialRepository } from "./credential-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentCredentialRepository(): CredentialRepository { - assertRepositoryRolloutDomainEnabled("credentials"); - - return new CredentialRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("credential_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-dashboard-service-link-repository.ts b/src/backend/database/repositories/current-dashboard-service-link-repository.ts deleted file mode 100644 index ee423195..00000000 --- a/src/backend/database/repositories/current-dashboard-service-link-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { DashboardServiceLinkRepository } from "./dashboard-service-link-repository.js"; - -export function createCurrentDashboardServiceLinkRepository(): DashboardServiceLinkRepository { - assertRepositoryRolloutDomainEnabled("dashboard_service_links"); - - return new DashboardServiceLinkRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("dashboard_service_link_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-dismissed-alert-repository.ts b/src/backend/database/repositories/current-dismissed-alert-repository.ts deleted file mode 100644 index 9d5f0c4a..00000000 --- a/src/backend/database/repositories/current-dismissed-alert-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { DismissedAlertRepository } from "./dismissed-alert-repository.js"; - -export function createCurrentDismissedAlertRepository(): DismissedAlertRepository { - assertRepositoryRolloutDomainEnabled("dismissed_alerts"); - - return new DismissedAlertRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("dismissed_alert_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-file-manager-bookmark-repository.ts b/src/backend/database/repositories/current-file-manager-bookmark-repository.ts deleted file mode 100644 index 3171ad7c..00000000 --- a/src/backend/database/repositories/current-file-manager-bookmark-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { FileManagerBookmarkRepository } from "./file-manager-bookmark-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentFileManagerBookmarkRepository(): FileManagerBookmarkRepository { - assertRepositoryRolloutDomainEnabled("file_manager_bookmarks"); - - return new FileManagerBookmarkRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("file_manager_bookmarks_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-homepage-item-repository.ts b/src/backend/database/repositories/current-homepage-item-repository.ts deleted file mode 100644 index 98657d9e..00000000 --- a/src/backend/database/repositories/current-homepage-item-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { HomepageItemRepository } from "./homepage-item-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentHomepageItemRepository(): HomepageItemRepository { - assertRepositoryRolloutDomainEnabled("homepage_items"); - - return new HomepageItemRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("homepage_item_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-homepage-layout-repository.ts b/src/backend/database/repositories/current-homepage-layout-repository.ts deleted file mode 100644 index f6c223b4..00000000 --- a/src/backend/database/repositories/current-homepage-layout-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { HomepageLayoutRepository } from "./homepage-layout-repository.js"; - -export function createCurrentHomepageLayoutRepository(): HomepageLayoutRepository { - assertRepositoryRolloutDomainEnabled("homepage_layouts"); - - return new HomepageLayoutRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("homepage_layout_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-host-folder-repository.ts b/src/backend/database/repositories/current-host-folder-repository.ts deleted file mode 100644 index 0319d9e6..00000000 --- a/src/backend/database/repositories/current-host-folder-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { HostFolderRepository } from "./host-folder-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentHostFolderRepository(): HostFolderRepository { - assertRepositoryRolloutDomainEnabled("host_folders"); - - return new HostFolderRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("host_folder_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-host-health-repository.ts b/src/backend/database/repositories/current-host-health-repository.ts deleted file mode 100644 index 12f7b809..00000000 --- a/src/backend/database/repositories/current-host-health-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { HostHealthRepository } from "./host-health-repository.js"; - -export function createCurrentHostHealthRepository(): HostHealthRepository { - assertRepositoryRolloutDomainEnabled("host_health"); - - return new HostHealthRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("host_health_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-host-metrics-history-repository.ts b/src/backend/database/repositories/current-host-metrics-history-repository.ts deleted file mode 100644 index 507cdc3f..00000000 --- a/src/backend/database/repositories/current-host-metrics-history-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { HostMetricsHistoryRepository } from "./host-metrics-history-repository.js"; - -export function createCurrentHostMetricsHistoryRepository(): HostMetricsHistoryRepository { - assertRepositoryRolloutDomainEnabled("host_metrics_history"); - - return new HostMetricsHistoryRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("host_metrics_history_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-host-metrics-preference-repository.ts b/src/backend/database/repositories/current-host-metrics-preference-repository.ts deleted file mode 100644 index b3ed0d69..00000000 --- a/src/backend/database/repositories/current-host-metrics-preference-repository.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { HostMetricsPreferenceRepository } from "./host-metrics-preference-repository.js"; - -export function createCurrentHostMetricsPreferenceRepository(): HostMetricsPreferenceRepository { - assertRepositoryRolloutDomainEnabled("host_metrics_preferences"); - - return new HostMetricsPreferenceRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook( - "host_metrics_preference_repository_write", - ), - ); -} diff --git a/src/backend/database/repositories/current-host-repository.ts b/src/backend/database/repositories/current-host-repository.ts deleted file mode 100644 index f8edfb0a..00000000 --- a/src/backend/database/repositories/current-host-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { HostRepository } from "./host-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentHostRepository(): HostRepository { - assertRepositoryRolloutDomainEnabled("hosts"); - - return new HostRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("host_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-host-resolution-repository.ts b/src/backend/database/repositories/current-host-resolution-repository.ts deleted file mode 100644 index 29239794..00000000 --- a/src/backend/database/repositories/current-host-resolution-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { HostResolutionRepository } from "./host-resolution-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentHostResolutionRepository(): HostResolutionRepository { - assertRepositoryRolloutDomainEnabled("host_resolution"); - - return new HostResolutionRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("host_resolution_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-network-topology-repository.ts b/src/backend/database/repositories/current-network-topology-repository.ts deleted file mode 100644 index 3d915af1..00000000 --- a/src/backend/database/repositories/current-network-topology-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { NetworkTopologyRepository } from "./network-topology-repository.js"; - -export function createCurrentNetworkTopologyRepository(): NetworkTopologyRepository { - assertRepositoryRolloutDomainEnabled("network_topology"); - - return new NetworkTopologyRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("network_topology_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-open-tab-repository.ts b/src/backend/database/repositories/current-open-tab-repository.ts deleted file mode 100644 index 081fc920..00000000 --- a/src/backend/database/repositories/current-open-tab-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { OpenTabRepository } from "./open-tab-repository.js"; - -export function createCurrentOpenTabRepository(): OpenTabRepository { - assertRepositoryRolloutDomainEnabled("open_tabs"); - - return new OpenTabRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("open_tab_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-opkssh-token-repository.ts b/src/backend/database/repositories/current-opkssh-token-repository.ts deleted file mode 100644 index bc1bd8ed..00000000 --- a/src/backend/database/repositories/current-opkssh-token-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { OpksshTokenRepository } from "./opkssh-token-repository.js"; - -export function createCurrentOpksshTokenRepository(): OpksshTokenRepository { - assertRepositoryRolloutDomainEnabled("opkssh_tokens"); - - return new OpksshTokenRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("opkssh_token_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-rbac-access-repository.ts b/src/backend/database/repositories/current-rbac-access-repository.ts deleted file mode 100644 index 78711bdf..00000000 --- a/src/backend/database/repositories/current-rbac-access-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { RbacAccessRepository } from "./rbac-access-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; - -export function createCurrentRbacAccessRepository(): RbacAccessRepository { - assertRepositoryRolloutDomainEnabled("rbac_access"); - - return new RbacAccessRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("rbac_access_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-recent-activity-repository.ts b/src/backend/database/repositories/current-recent-activity-repository.ts deleted file mode 100644 index 136a9954..00000000 --- a/src/backend/database/repositories/current-recent-activity-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { RecentActivityRepository } from "./recent-activity-repository.js"; - -export function createCurrentRecentActivityRepository(): RecentActivityRepository { - assertRepositoryRolloutDomainEnabled("recent_activity"); - - return new RecentActivityRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("recent_activity_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-repository-runtime.ts b/src/backend/database/repositories/current-repository-runtime.ts deleted file mode 100644 index c4f7fa0f..00000000 --- a/src/backend/database/repositories/current-repository-runtime.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js"; -import { getDb, getSqlite } from "../db/index.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; - -export function createCurrentRepositoryContext(): DatabaseContext { - return { - dialect: "sqlite", - drizzle: getDb(), - sqlite: getSqlite(), - }; -} - -export function createCurrentRepositoryWriteHook( - reason: string, -): () => Promise { - return () => DatabaseSaveTrigger.forceSave(reason); -} - -export function getCurrentRepositorySqlite() { - return getSqlite(); -} diff --git a/src/backend/database/repositories/current-role-repository.ts b/src/backend/database/repositories/current-role-repository.ts deleted file mode 100644 index 94b38d09..00000000 --- a/src/backend/database/repositories/current-role-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { RoleRepository } from "./role-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentRoleRepository(): RoleRepository { - assertRepositoryRolloutDomainEnabled("roles"); - - return new RoleRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("role_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-session-recording-repository.ts b/src/backend/database/repositories/current-session-recording-repository.ts deleted file mode 100644 index ce6d21dd..00000000 --- a/src/backend/database/repositories/current-session-recording-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { SessionRecordingRepository } from "./session-recording-repository.js"; - -export function createCurrentSessionRecordingRepository(): SessionRecordingRepository { - assertRepositoryRolloutDomainEnabled("session_recordings"); - - return new SessionRecordingRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("session_recording_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-session-repository.ts b/src/backend/database/repositories/current-session-repository.ts deleted file mode 100644 index c1f1c97f..00000000 --- a/src/backend/database/repositories/current-session-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SessionRepository } from "./session-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentSessionRepository(): SessionRepository { - assertRepositoryRolloutDomainEnabled("sessions"); - - return new SessionRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("session_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-settings-repository.ts b/src/backend/database/repositories/current-settings-repository.ts deleted file mode 100644 index 6c386028..00000000 --- a/src/backend/database/repositories/current-settings-repository.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { SettingsRepository } from "./settings-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, - getCurrentRepositorySqlite, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentSettingsRepository(): SettingsRepository { - assertRepositoryRolloutDomainEnabled("settings"); - - return new SettingsRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("settings_repository_write"), - ); -} - -export function getCurrentSettingValue(key: string): string | null { - assertRepositoryRolloutDomainEnabled("settings"); - - const row = getCurrentRepositorySqlite() - .prepare("SELECT value FROM settings WHERE key = ?") - .get(key) as { value?: string } | undefined; - - return row?.value ?? null; -} diff --git a/src/backend/database/repositories/current-shared-credential-repository.ts b/src/backend/database/repositories/current-shared-credential-repository.ts deleted file mode 100644 index b91336f9..00000000 --- a/src/backend/database/repositories/current-shared-credential-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { SharedCredentialRepository } from "./shared-credential-repository.js"; - -export function createCurrentSharedCredentialRepository(): SharedCredentialRepository { - assertRepositoryRolloutDomainEnabled("shared_credentials"); - - return new SharedCredentialRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("shared_credential_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-snippet-repository.ts b/src/backend/database/repositories/current-snippet-repository.ts deleted file mode 100644 index 023ad6cd..00000000 --- a/src/backend/database/repositories/current-snippet-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SnippetRepository } from "./snippet-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentSnippetRepository(): SnippetRepository { - assertRepositoryRolloutDomainEnabled("snippets"); - - return new SnippetRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("snippet_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-ssh-credential-usage-repository.ts b/src/backend/database/repositories/current-ssh-credential-usage-repository.ts deleted file mode 100644 index 4c5bfd53..00000000 --- a/src/backend/database/repositories/current-ssh-credential-usage-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { SshCredentialUsageRepository } from "./ssh-credential-usage-repository.js"; - -export function createCurrentSshCredentialUsageRepository(): SshCredentialUsageRepository { - assertRepositoryRolloutDomainEnabled("ssh_credential_usage"); - - return new SshCredentialUsageRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("ssh_credential_usage_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-sso-provider-repository.ts b/src/backend/database/repositories/current-sso-provider-repository.ts deleted file mode 100644 index 514d1fa0..00000000 --- a/src/backend/database/repositories/current-sso-provider-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { SsoProviderRepository } from "./sso-provider-repository.js"; - -export function createCurrentSsoProviderRepository(): SsoProviderRepository { - assertRepositoryRolloutDomainEnabled("sso_providers"); - - return new SsoProviderRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("sso_provider_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-termix-identity-ca-repository.ts b/src/backend/database/repositories/current-termix-identity-ca-repository.ts deleted file mode 100644 index d2447420..00000000 --- a/src/backend/database/repositories/current-termix-identity-ca-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { TermixIdentityCaRepository } from "./termix-identity-ca-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentTermixIdentityCaRepository(): TermixIdentityCaRepository { - assertRepositoryRolloutDomainEnabled("termix_identity_ca"); - - return new TermixIdentityCaRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("termix_identity_ca_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-termix-identity-repository.ts b/src/backend/database/repositories/current-termix-identity-repository.ts deleted file mode 100644 index 435dbc57..00000000 --- a/src/backend/database/repositories/current-termix-identity-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { TermixIdentityRepository } from "./termix-identity-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentTermixIdentityRepository(): TermixIdentityRepository { - assertRepositoryRolloutDomainEnabled("termix_identity"); - - return new TermixIdentityRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("termix_identity_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-tmux-session-tag-repository.ts b/src/backend/database/repositories/current-tmux-session-tag-repository.ts deleted file mode 100644 index 7d0a7bcf..00000000 --- a/src/backend/database/repositories/current-tmux-session-tag-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { TmuxSessionTagRepository } from "./tmux-session-tag-repository.js"; - -export function createCurrentTmuxSessionTagRepository(): TmuxSessionTagRepository { - assertRepositoryRolloutDomainEnabled("tmux_session_tags"); - - return new TmuxSessionTagRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("tmux_session_tag_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-transfer-recent-repository.ts b/src/backend/database/repositories/current-transfer-recent-repository.ts deleted file mode 100644 index a9b07eb3..00000000 --- a/src/backend/database/repositories/current-transfer-recent-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { TransferRecentRepository } from "./transfer-recent-repository.js"; - -export function createCurrentTransferRecentRepository(): TransferRecentRepository { - assertRepositoryRolloutDomainEnabled("transfer_recent"); - - return new TransferRecentRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("transfer_recent_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-trusted-device-repository.ts b/src/backend/database/repositories/current-trusted-device-repository.ts deleted file mode 100644 index 10c9e811..00000000 --- a/src/backend/database/repositories/current-trusted-device-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { TrustedDeviceRepository } from "./trusted-device-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentTrustedDeviceRepository(): TrustedDeviceRepository { - assertRepositoryRolloutDomainEnabled("trusted_devices"); - - return new TrustedDeviceRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("trusted_device_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-user-data-export-repository.ts b/src/backend/database/repositories/current-user-data-export-repository.ts deleted file mode 100644 index 89ab141c..00000000 --- a/src/backend/database/repositories/current-user-data-export-repository.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { createCurrentRepositoryContext } from "./current-repository-runtime.js"; -import { UserDataExportRepository } from "./user-data-export-repository.js"; - -export function createCurrentUserDataExportRepository(): UserDataExportRepository { - assertRepositoryRolloutDomainEnabled("user_data_exports"); - - return new UserDataExportRepository(createCurrentRepositoryContext()); -} diff --git a/src/backend/database/repositories/current-user-preference-repository.ts b/src/backend/database/repositories/current-user-preference-repository.ts deleted file mode 100644 index 787dbb3d..00000000 --- a/src/backend/database/repositories/current-user-preference-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { UserPreferenceRepository } from "./user-preference-repository.js"; - -export function createCurrentUserPreferenceRepository(): UserPreferenceRepository { - assertRepositoryRolloutDomainEnabled("user_preferences"); - - return new UserPreferenceRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("user_preference_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-user-repository.ts b/src/backend/database/repositories/current-user-repository.ts deleted file mode 100644 index 617faeec..00000000 --- a/src/backend/database/repositories/current-user-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { UserRepository } from "./user-repository.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; - -export function createCurrentUserRepository(): UserRepository { - assertRepositoryRolloutDomainEnabled("users"); - - return new UserRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("user_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-vault-profile-repository.ts b/src/backend/database/repositories/current-vault-profile-repository.ts deleted file mode 100644 index 567dd91d..00000000 --- a/src/backend/database/repositories/current-vault-profile-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { VaultProfileRepository } from "./vault-profile-repository.js"; - -export function createCurrentVaultProfileRepository(): VaultProfileRepository { - assertRepositoryRolloutDomainEnabled("vault_profiles"); - - return new VaultProfileRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("vault_profile_repository_write"), - ); -} diff --git a/src/backend/database/repositories/current-vault-token-repository.ts b/src/backend/database/repositories/current-vault-token-repository.ts deleted file mode 100644 index 8fbac47e..00000000 --- a/src/backend/database/repositories/current-vault-token-repository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { assertRepositoryRolloutDomainEnabled } from "./repository-rollout.js"; -import { - createCurrentRepositoryContext, - createCurrentRepositoryWriteHook, -} from "./current-repository-runtime.js"; -import { VaultTokenRepository } from "./vault-token-repository.js"; - -export function createCurrentVaultTokenRepository(): VaultTokenRepository { - assertRepositoryRolloutDomainEnabled("vault_tokens"); - - return new VaultTokenRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("vault_token_repository_write"), - ); -} diff --git a/src/backend/database/repositories/dashboard-service-link-repository.test.ts b/src/backend/database/repositories/dashboard-service-link-repository.test.ts index 3f35ede0..057c52ae 100644 --- a/src/backend/database/repositories/dashboard-service-link-repository.test.ts +++ b/src/backend/database/repositories/dashboard-service-link-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { DashboardServiceLinkRepository } from "./dashboard-service-link-repository.js"; describe("DashboardServiceLinkRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("DashboardServiceLinkRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/dashboard-service-link-repository.ts b/src/backend/database/repositories/dashboard-service-link-repository.ts index 10683adc..a12079e0 100644 --- a/src/backend/database/repositories/dashboard-service-link-repository.ts +++ b/src/backend/database/repositories/dashboard-service-link-repository.ts @@ -1,6 +1,6 @@ import { and, asc, eq } from "drizzle-orm"; import { dashboardServiceLinks } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type DashboardServiceLinkRecord = typeof dashboardServiceLinks.$inferSelect; diff --git a/src/backend/database/repositories/database-context.ts b/src/backend/database/repositories/database-context.ts new file mode 100644 index 00000000..7666cc41 --- /dev/null +++ b/src/backend/database/repositories/database-context.ts @@ -0,0 +1,9 @@ +import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3"; +import type { Database as BetterSqliteDatabase } from "better-sqlite3"; +import type * as schema from "../db/schema.js"; + +export interface DatabaseContext { + dialect: "sqlite"; + drizzle: BetterSQLite3Database; + sqlite?: BetterSqliteDatabase; +} diff --git a/src/backend/database/repositories/dismissed-alert-repository.test.ts b/src/backend/database/repositories/dismissed-alert-repository.test.ts index 5528a706..1c1a3f19 100644 --- a/src/backend/database/repositories/dismissed-alert-repository.test.ts +++ b/src/backend/database/repositories/dismissed-alert-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { DismissedAlertRepository } from "./dismissed-alert-repository.js"; describe("DismissedAlertRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("DismissedAlertRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/dismissed-alert-repository.ts b/src/backend/database/repositories/dismissed-alert-repository.ts index 41d0c168..e44d20b1 100644 --- a/src/backend/database/repositories/dismissed-alert-repository.ts +++ b/src/backend/database/repositories/dismissed-alert-repository.ts @@ -1,6 +1,6 @@ import { and, eq } from "drizzle-orm"; import { dismissedAlerts } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type DismissedAlertRecord = typeof dismissedAlerts.$inferSelect; diff --git a/src/backend/database/repositories/factory.ts b/src/backend/database/repositories/factory.ts new file mode 100644 index 00000000..02f07aad --- /dev/null +++ b/src/backend/database/repositories/factory.ts @@ -0,0 +1,348 @@ +import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js"; +import { getDb, getSqlite } from "../db/index.js"; +import type { DatabaseContext } from "./database-context.js"; +import { AlertRepository } from "./alert-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 { 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 { HostRepository } from "./host-repository.js"; +import { HostResolutionRepository } from "./host-resolution-repository.js"; +import { NetworkTopologyRepository } from "./network-topology-repository.js"; +import { OpenTabRepository } from "./open-tab-repository.js"; +import { OpksshTokenRepository } from "./opkssh-token-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 { SettingsRepository } from "./settings-repository.js"; +import { SharedCredentialRepository } from "./shared-credential-repository.js"; +import { SnippetRepository } from "./snippet-repository.js"; +import { SshCredentialUsageRepository } from "./ssh-credential-usage-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"; + +export function createCurrentRepositoryContext(): DatabaseContext { + return { + dialect: "sqlite", + drizzle: getDb(), + sqlite: getSqlite(), + }; +} + +export function createCurrentRepositoryWriteHook( + reason: string, +): () => Promise { + return () => DatabaseSaveTrigger.forceSave(reason); +} + +export function getCurrentRepositorySqlite() { + return getSqlite(); +} + +export function getCurrentSettingValue(key: string): string | null { + const row = getCurrentRepositorySqlite() + .prepare("SELECT value FROM settings WHERE key = ?") + .get(key) as { value?: string } | undefined; + + return row?.value ?? null; +} + +export function createCurrentAlertRepository(): AlertRepository { + return new AlertRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook("alert_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 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 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(), + createCurrentRepositoryWriteHook("host_metrics_history_repository_write"), + ); +} + +export function createCurrentHostMetricsPreferenceRepository(): HostMetricsPreferenceRepository { + return new HostMetricsPreferenceRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook( + "host_metrics_preference_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"), + ); +} + +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 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 createCurrentSettingsRepository(): SettingsRepository { + return new SettingsRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook("settings_repository_write"), + ); +} + +export function createCurrentSharedCredentialRepository(): SharedCredentialRepository { + return new SharedCredentialRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook("shared_credential_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 createCurrentVaultProfileRepository(): VaultProfileRepository { + return new VaultProfileRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook("vault_profile_repository_write"), + ); +} + +export function createCurrentVaultTokenRepository(): VaultTokenRepository { + return new VaultTokenRepository( + createCurrentRepositoryContext(), + createCurrentRepositoryWriteHook("vault_token_repository_write"), + ); +} diff --git a/src/backend/database/repositories/file-manager-bookmark-repository.test.ts b/src/backend/database/repositories/file-manager-bookmark-repository.test.ts index d0875071..81a576f2 100644 --- a/src/backend/database/repositories/file-manager-bookmark-repository.test.ts +++ b/src/backend/database/repositories/file-manager-bookmark-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { FileManagerBookmarkRepository } from "./file-manager-bookmark-repository.js"; describe("FileManagerBookmarkRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("FileManagerBookmarkRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/file-manager-bookmark-repository.ts b/src/backend/database/repositories/file-manager-bookmark-repository.ts index 4f3bcdb4..dfd68b3f 100644 --- a/src/backend/database/repositories/file-manager-bookmark-repository.ts +++ b/src/backend/database/repositories/file-manager-bookmark-repository.ts @@ -4,7 +4,7 @@ import { fileManagerRecent, fileManagerShortcuts, } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type FileManagerRecentRecord = typeof fileManagerRecent.$inferSelect; export type FileManagerPinnedRecord = typeof fileManagerPinned.$inferSelect; diff --git a/src/backend/database/repositories/homepage-item-repository.test.ts b/src/backend/database/repositories/homepage-item-repository.test.ts index 299aec92..ed692126 100644 --- a/src/backend/database/repositories/homepage-item-repository.test.ts +++ b/src/backend/database/repositories/homepage-item-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { HomepageItemRepository } from "./homepage-item-repository.js"; describe("HomepageItemRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("HomepageItemRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/homepage-item-repository.ts b/src/backend/database/repositories/homepage-item-repository.ts index b3e1f025..1dc8efea 100644 --- a/src/backend/database/repositories/homepage-item-repository.ts +++ b/src/backend/database/repositories/homepage-item-repository.ts @@ -1,6 +1,6 @@ import { and, asc, eq } from "drizzle-orm"; import { homepageItems } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type HomepageItemRecord = typeof homepageItems.$inferSelect; diff --git a/src/backend/database/repositories/homepage-layout-repository.test.ts b/src/backend/database/repositories/homepage-layout-repository.test.ts index 49709b36..31d4ddcb 100644 --- a/src/backend/database/repositories/homepage-layout-repository.test.ts +++ b/src/backend/database/repositories/homepage-layout-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { HomepageLayoutRepository } from "./homepage-layout-repository.js"; describe("HomepageLayoutRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("HomepageLayoutRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/homepage-layout-repository.ts b/src/backend/database/repositories/homepage-layout-repository.ts index 453dd882..bb10c453 100644 --- a/src/backend/database/repositories/homepage-layout-repository.ts +++ b/src/backend/database/repositories/homepage-layout-repository.ts @@ -1,6 +1,6 @@ import { eq } from "drizzle-orm"; import { homepageLayouts } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type HomepageLayoutRecord = typeof homepageLayouts.$inferSelect; diff --git a/src/backend/database/repositories/host-credential-repositories.test.ts b/src/backend/database/repositories/host-credential-repositories.test.ts index 8282d2d5..3a8112c9 100644 --- a/src/backend/database/repositories/host-credential-repositories.test.ts +++ b/src/backend/database/repositories/host-credential-repositories.test.ts @@ -1,12 +1,12 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { CredentialRepository } from "./credential-repository.js"; import { HostRepository } from "./host-repository.js"; import { DataCrypto } from "../../utils/data-crypto.js"; import { SystemCrypto } from "../../utils/system-crypto.js"; describe("HostRepository and CredentialRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { vi.restoreAllMocks(); @@ -23,14 +23,10 @@ describe("HostRepository and CredentialRepository", () => { credentials: CredentialRepository; hosts: HostRepository; sqlite: NonNullable< - Awaited>["sqlite"] + Awaited>["sqlite"] >; }> { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/host-folder-repository.test.ts b/src/backend/database/repositories/host-folder-repository.test.ts index a4f9c826..80c60a71 100644 --- a/src/backend/database/repositories/host-folder-repository.test.ts +++ b/src/backend/database/repositories/host-folder-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { HostFolderRepository } from "./host-folder-repository.js"; describe("HostFolderRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -17,14 +17,10 @@ describe("HostFolderRepository", () => { ): Promise<{ repository: HostFolderRepository; sqlite: NonNullable< - Awaited>["sqlite"] + Awaited>["sqlite"] >; }> { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/host-folder-repository.ts b/src/backend/database/repositories/host-folder-repository.ts index 00feaa13..50d0b169 100644 --- a/src/backend/database/repositories/host-folder-repository.ts +++ b/src/backend/database/repositories/host-folder-repository.ts @@ -1,7 +1,7 @@ import { and, eq, like, or, sql } from "drizzle-orm"; import type { SQLiteColumn } from "drizzle-orm/sqlite-core"; import { hosts, sshCredentials, sshFolders } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type HostFolderRecord = typeof sshFolders.$inferSelect; export type HostFolderHostRecord = typeof hosts.$inferSelect; diff --git a/src/backend/database/repositories/host-health-repository.test.ts b/src/backend/database/repositories/host-health-repository.test.ts index 3049c00d..18327ebc 100644 --- a/src/backend/database/repositories/host-health-repository.test.ts +++ b/src/backend/database/repositories/host-health-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { HostHealthRepository } from "./host-health-repository.js"; describe("HostHealthRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("HostHealthRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/host-health-repository.ts b/src/backend/database/repositories/host-health-repository.ts index 4e5bae87..5aac9308 100644 --- a/src/backend/database/repositories/host-health-repository.ts +++ b/src/backend/database/repositories/host-health-repository.ts @@ -1,6 +1,6 @@ import { and, desc, eq } from "drizzle-orm"; import { hostHealthChecks, hostHealthHistory } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type HostHealthCheckRecord = typeof hostHealthChecks.$inferSelect; export type HostHealthHistoryRecord = typeof hostHealthHistory.$inferSelect; diff --git a/src/backend/database/repositories/host-metrics-history-repository.test.ts b/src/backend/database/repositories/host-metrics-history-repository.test.ts index 6ae5e7e8..cb35da74 100644 --- a/src/backend/database/repositories/host-metrics-history-repository.test.ts +++ b/src/backend/database/repositories/host-metrics-history-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { HostMetricsHistoryRepository } from "./host-metrics-history-repository.js"; describe("HostMetricsHistoryRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("HostMetricsHistoryRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE hosts ( diff --git a/src/backend/database/repositories/host-metrics-history-repository.ts b/src/backend/database/repositories/host-metrics-history-repository.ts index 8a919165..cbad4e2f 100644 --- a/src/backend/database/repositories/host-metrics-history-repository.ts +++ b/src/backend/database/repositories/host-metrics-history-repository.ts @@ -1,6 +1,6 @@ import { and, asc, eq, gte, lte } from "drizzle-orm"; import { hostMetricsHistory } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type HostMetricsHistoryRecord = typeof hostMetricsHistory.$inferSelect; diff --git a/src/backend/database/repositories/host-metrics-preference-repository.test.ts b/src/backend/database/repositories/host-metrics-preference-repository.test.ts index 91f52533..efc16836 100644 --- a/src/backend/database/repositories/host-metrics-preference-repository.test.ts +++ b/src/backend/database/repositories/host-metrics-preference-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { HostMetricsPreferenceRepository } from "./host-metrics-preference-repository.js"; describe("HostMetricsPreferenceRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("HostMetricsPreferenceRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/host-metrics-preference-repository.ts b/src/backend/database/repositories/host-metrics-preference-repository.ts index 1a5d3eff..070063d7 100644 --- a/src/backend/database/repositories/host-metrics-preference-repository.ts +++ b/src/backend/database/repositories/host-metrics-preference-repository.ts @@ -1,6 +1,6 @@ import { and, eq } from "drizzle-orm"; import { hostMetricsPreferences, hosts } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type HostMetricsPreferenceRecord = typeof hostMetricsPreferences.$inferSelect; diff --git a/src/backend/database/repositories/host-repository.ts b/src/backend/database/repositories/host-repository.ts index d8ee5086..62ebf843 100644 --- a/src/backend/database/repositories/host-repository.ts +++ b/src/backend/database/repositories/host-repository.ts @@ -1,6 +1,6 @@ import { and, eq, inArray } from "drizzle-orm"; import { hostAccess, hosts } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; import { DataCrypto } from "../../utils/data-crypto.js"; export type HostRecord = typeof hosts.$inferSelect; diff --git a/src/backend/database/repositories/host-resolution-repository.test.ts b/src/backend/database/repositories/host-resolution-repository.test.ts index 5a2a0ee4..777be65d 100644 --- a/src/backend/database/repositories/host-resolution-repository.test.ts +++ b/src/backend/database/repositories/host-resolution-repository.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { DataCrypto } from "../../utils/data-crypto.js"; import { HostResolutionRepository } from "./host-resolution-repository.js"; @@ -11,7 +11,7 @@ vi.mock("../../utils/data-crypto.js", () => ({ })); describe("HostResolutionRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { vi.mocked(DataCrypto.getUserDataKey).mockReset(); @@ -25,11 +25,7 @@ describe("HostResolutionRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/host-resolution-repository.ts b/src/backend/database/repositories/host-resolution-repository.ts index e3a229c5..f97ee713 100644 --- a/src/backend/database/repositories/host-resolution-repository.ts +++ b/src/backend/database/repositories/host-resolution-repository.ts @@ -1,6 +1,6 @@ import { and, eq, inArray, isNotNull } from "drizzle-orm"; import { hostAccess, hosts, sshCredentials } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; import { DataCrypto } from "../../utils/data-crypto.js"; export type HostResolutionHostRecord = typeof hosts.$inferSelect; diff --git a/src/backend/database/repositories/network-topology-repository.test.ts b/src/backend/database/repositories/network-topology-repository.test.ts index b18accd2..547c8486 100644 --- a/src/backend/database/repositories/network-topology-repository.test.ts +++ b/src/backend/database/repositories/network-topology-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { NetworkTopologyRepository } from "./network-topology-repository.js"; describe("NetworkTopologyRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("NetworkTopologyRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/network-topology-repository.ts b/src/backend/database/repositories/network-topology-repository.ts index 5a56b7c9..fa8024fb 100644 --- a/src/backend/database/repositories/network-topology-repository.ts +++ b/src/backend/database/repositories/network-topology-repository.ts @@ -1,6 +1,6 @@ import { eq } from "drizzle-orm"; import { networkTopology } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type NetworkTopologyRecord = typeof networkTopology.$inferSelect; diff --git a/src/backend/database/repositories/open-tab-repository.test.ts b/src/backend/database/repositories/open-tab-repository.test.ts index 60020bdb..774f96ca 100644 --- a/src/backend/database/repositories/open-tab-repository.test.ts +++ b/src/backend/database/repositories/open-tab-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { OpenTabRepository } from "./open-tab-repository.js"; describe("OpenTabRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("OpenTabRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/open-tab-repository.ts b/src/backend/database/repositories/open-tab-repository.ts index 219d7bcf..5cad21de 100644 --- a/src/backend/database/repositories/open-tab-repository.ts +++ b/src/backend/database/repositories/open-tab-repository.ts @@ -1,6 +1,6 @@ import { and, eq, gt } from "drizzle-orm"; import { userOpenTabs } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type OpenTabRecord = typeof userOpenTabs.$inferSelect; export type NewOpenTabRecord = typeof userOpenTabs.$inferInsert; diff --git a/src/backend/database/repositories/opkssh-token-repository.test.ts b/src/backend/database/repositories/opkssh-token-repository.test.ts index 03d538e3..37b115d2 100644 --- a/src/backend/database/repositories/opkssh-token-repository.test.ts +++ b/src/backend/database/repositories/opkssh-token-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { OpksshTokenRepository } from "./opkssh-token-repository.js"; describe("OpksshTokenRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("OpksshTokenRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/opkssh-token-repository.ts b/src/backend/database/repositories/opkssh-token-repository.ts index cf7186cc..0c15bddd 100644 --- a/src/backend/database/repositories/opkssh-token-repository.ts +++ b/src/backend/database/repositories/opkssh-token-repository.ts @@ -1,6 +1,6 @@ import { and, eq } from "drizzle-orm"; import { opksshTokens } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type OpksshTokenRecord = typeof opksshTokens.$inferSelect; diff --git a/src/backend/database/repositories/rbac-access-repository.test.ts b/src/backend/database/repositories/rbac-access-repository.test.ts index 6017b416..e85597ee 100644 --- a/src/backend/database/repositories/rbac-access-repository.test.ts +++ b/src/backend/database/repositories/rbac-access-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { RbacAccessRepository } from "./rbac-access-repository.js"; describe("RbacAccessRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; const activeAccessTime = "2026-06-26T12:00:00.000Z"; afterEach(async () => { @@ -16,11 +16,7 @@ describe("RbacAccessRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/rbac-access-repository.ts b/src/backend/database/repositories/rbac-access-repository.ts index cff70ba2..9a91f9ac 100644 --- a/src/backend/database/repositories/rbac-access-repository.ts +++ b/src/backend/database/repositories/rbac-access-repository.ts @@ -8,7 +8,7 @@ import { snippets, users, } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type RbacAccessTargetType = "user" | "role"; diff --git a/src/backend/database/repositories/recent-activity-repository.test.ts b/src/backend/database/repositories/recent-activity-repository.test.ts index b277b6db..94621665 100644 --- a/src/backend/database/repositories/recent-activity-repository.test.ts +++ b/src/backend/database/repositories/recent-activity-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { RecentActivityRepository } from "./recent-activity-repository.js"; describe("RecentActivityRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -17,14 +17,10 @@ describe("RecentActivityRepository", () => { ): Promise<{ repository: RecentActivityRepository; sqlite: NonNullable< - Awaited>["sqlite"] + Awaited>["sqlite"] >; }> { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/recent-activity-repository.ts b/src/backend/database/repositories/recent-activity-repository.ts index 6f5d1597..bd7979fc 100644 --- a/src/backend/database/repositories/recent-activity-repository.ts +++ b/src/backend/database/repositories/recent-activity-repository.ts @@ -1,6 +1,6 @@ import { desc, eq, inArray } from "drizzle-orm"; import { recentActivity } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type RecentActivityRecord = typeof recentActivity.$inferSelect; export type NewRecentActivityRecord = typeof recentActivity.$inferInsert; diff --git a/src/backend/database/repositories/repository-rollout.test.ts b/src/backend/database/repositories/repository-rollout.test.ts deleted file mode 100644 index 312c4d83..00000000 --- a/src/backend/database/repositories/repository-rollout.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - getRepositoryRolloutStatus, - getRepositoryRolloutWarnings, - isRepositoryRolloutDomainEnabled, - parseRepositoryRolloutConfig, - REPOSITORY_ROLLOUT_ENV, -} from "./repository-rollout.js"; - -describe("parseRepositoryRolloutConfig", () => { - it("defaults to the current migrated repository slice", () => { - const config = parseRepositoryRolloutConfig({}); - - expect(config).toEqual({ - mode: "all", - enabledDomains: [ - "settings", - "users", - "sessions", - "api_keys", - "trusted_devices", - "credentials", - "termix_identity", - "termix_identity_ca", - "hosts", - "snippets", - "roles", - "rbac_access", - "shared_credentials", - "sso_providers", - "audit_logs", - "user_preferences", - "open_tabs", - "dismissed_alerts", - "homepage_layouts", - "homepage_items", - "network_topology", - "dashboard_service_links", - "session_recordings", - "command_history", - "recent_activity", - "ssh_credential_usage", - "transfer_recent", - "file_manager_bookmarks", - "c2s_tunnel_presets", - "tmux_session_tags", - "opkssh_tokens", - "vault_tokens", - "vault_profiles", - "host_metrics_preferences", - "host_health", - "host_metrics_history", - "alerts", - "user_data_exports", - "host_folders", - "host_resolution", - ], - explicit: false, - }); - }); - - it("allows explicitly disabling all migrated repository domains", () => { - const config = parseRepositoryRolloutConfig({ - [REPOSITORY_ROLLOUT_ENV]: "off", - }); - - expect(config).toEqual({ - mode: "none", - enabledDomains: [], - explicit: true, - }); - }); - - it("accepts a partial domain allowlist with aliases", () => { - const config = parseRepositoryRolloutConfig({ - [REPOSITORY_ROLLOUT_ENV]: - "settings,user,api-key,credential,termix-id,termix-ca,host,dismissed,alerts,layout,items,topology,dashboard-link,recordings,history,activity,usage,transfer,user-data-export,ssh-folder,host-resolver,shared-credentials,bookmarks,c2s,tmux,opkssh,vault-token,vault-profile,metrics-preferences,host-health,metrics-history", - }); - - expect(config).toEqual({ - mode: "partial", - enabledDomains: [ - "settings", - "users", - "api_keys", - "credentials", - "termix_identity", - "termix_identity_ca", - "hosts", - "dismissed_alerts", - "alerts", - "homepage_layouts", - "homepage_items", - "network_topology", - "dashboard_service_links", - "session_recordings", - "command_history", - "recent_activity", - "ssh_credential_usage", - "transfer_recent", - "user_data_exports", - "host_folders", - "host_resolution", - "shared_credentials", - "file_manager_bookmarks", - "c2s_tunnel_presets", - "tmux_session_tags", - "opkssh_tokens", - "vault_tokens", - "vault_profiles", - "host_metrics_preferences", - "host_health", - "host_metrics_history", - ], - explicit: true, - }); - }); - - it("deduplicates allowlisted domains", () => { - const config = parseRepositoryRolloutConfig({ - [REPOSITORY_ROLLOUT_ENV]: "users,user,users", - }); - - expect(config.enabledDomains).toEqual(["users"]); - }); - - it("rejects unknown domains", () => { - expect(() => - parseRepositoryRolloutConfig({ - [REPOSITORY_ROLLOUT_ENV]: "settings,unknown-domain", - }), - ).toThrow("Unsupported DATABASE_LAYER_REPOSITORY_ROLLOUT domain"); - }); - - it("checks whether an individual domain is enabled", () => { - const env = { [REPOSITORY_ROLLOUT_ENV]: "sessions" }; - - expect(isRepositoryRolloutDomainEnabled("sessions", env)).toBe(true); - expect(isRepositoryRolloutDomainEnabled("users", env)).toBe(false); - }); - - it("builds a status payload for admin visibility", () => { - const status = getRepositoryRolloutStatus({ - [REPOSITORY_ROLLOUT_ENV]: "settings,sessions", - }); - - expect(status).toEqual({ - mode: "partial", - enabledDomains: ["settings", "sessions"], - explicit: true, - envKey: REPOSITORY_ROLLOUT_ENV, - supportedDomains: [ - "settings", - "users", - "sessions", - "api_keys", - "trusted_devices", - "credentials", - "termix_identity", - "termix_identity_ca", - "hosts", - "snippets", - "roles", - "rbac_access", - "shared_credentials", - "sso_providers", - "audit_logs", - "user_preferences", - "open_tabs", - "dismissed_alerts", - "homepage_layouts", - "homepage_items", - "network_topology", - "dashboard_service_links", - "session_recordings", - "command_history", - "recent_activity", - "ssh_credential_usage", - "transfer_recent", - "file_manager_bookmarks", - "c2s_tunnel_presets", - "tmux_session_tags", - "opkssh_tokens", - "vault_tokens", - "vault_profiles", - "host_metrics_preferences", - "host_health", - "host_metrics_history", - "alerts", - "user_data_exports", - "host_folders", - "host_resolution", - ], - warnings: [ - "Partial repository rollout enabled for domains: settings, sessions.", - ], - }); - }); - - it("warns when gray rollout is implicit", () => { - const warnings = getRepositoryRolloutWarnings( - parseRepositoryRolloutConfig({}), - ); - - expect(warnings).toEqual([ - "DATABASE_LAYER_REPOSITORY_ROLLOUT is not explicitly set; gray targets should set it so rollout state is visible in deployment config.", - ]); - }); - - it("warns when migrated repository domains are disabled", () => { - const warnings = getRepositoryRolloutWarnings( - parseRepositoryRolloutConfig({ [REPOSITORY_ROLLOUT_ENV]: "off" }), - ); - - expect(warnings).toEqual([ - "All migrated repository domains are disabled; migrated auth/settings/session paths will fail closed.", - ]); - }); -}); diff --git a/src/backend/database/repositories/repository-rollout.ts b/src/backend/database/repositories/repository-rollout.ts deleted file mode 100644 index d380bc66..00000000 --- a/src/backend/database/repositories/repository-rollout.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { databaseLogger } from "../../utils/logger.js"; - -export const REPOSITORY_ROLLOUT_ENV = "DATABASE_LAYER_REPOSITORY_ROLLOUT"; - -export const REPOSITORY_ROLLOUT_DOMAINS = [ - "settings", - "users", - "sessions", - "api_keys", - "trusted_devices", - "credentials", - "termix_identity", - "termix_identity_ca", - "hosts", - "snippets", - "roles", - "rbac_access", - "shared_credentials", - "sso_providers", - "audit_logs", - "user_preferences", - "open_tabs", - "dismissed_alerts", - "homepage_layouts", - "homepage_items", - "network_topology", - "dashboard_service_links", - "session_recordings", - "command_history", - "recent_activity", - "ssh_credential_usage", - "transfer_recent", - "file_manager_bookmarks", - "c2s_tunnel_presets", - "tmux_session_tags", - "opkssh_tokens", - "vault_tokens", - "vault_profiles", - "host_metrics_preferences", - "host_health", - "host_metrics_history", - "alerts", - "user_data_exports", - "host_folders", - "host_resolution", -] as const; - -export type RepositoryRolloutDomain = - (typeof REPOSITORY_ROLLOUT_DOMAINS)[number]; - -export interface RepositoryRolloutConfig { - mode: "all" | "none" | "partial"; - enabledDomains: RepositoryRolloutDomain[]; - explicit: boolean; -} - -export interface RepositoryRolloutStatus extends RepositoryRolloutConfig { - envKey: typeof REPOSITORY_ROLLOUT_ENV; - supportedDomains: RepositoryRolloutDomain[]; - warnings: string[]; -} - -type EnvLike = Record; - -const DOMAIN_ALIASES: Record = { - api: "api_keys", - api_key: "api_keys", - api_keys: "api_keys", - apikey: "api_keys", - apikeys: "api_keys", - audit: "audit_logs", - audit_log: "audit_logs", - audit_logs: "audit_logs", - auditlog: "audit_logs", - auditlogs: "audit_logs", - alert: "alerts", - alerts: "alerts", - dashboard_link: "dashboard_service_links", - dashboard_links: "dashboard_service_links", - dashboard_service_link: "dashboard_service_links", - dashboard_service_links: "dashboard_service_links", - dashboardlink: "dashboard_service_links", - dashboardlinks: "dashboard_service_links", - command_history: "command_history", - commandhistory: "command_history", - credential: "credentials", - credentials: "credentials", - ca: "termix_identity_ca", - history: "command_history", - ssh_credential: "credentials", - ssh_credentials: "credentials", - sshcredential: "credentials", - sshcredentials: "credentials", - terminal_history: "command_history", - termix_ca: "termix_identity_ca", - termix_id: "termix_identity", - termix_id_ca: "termix_identity_ca", - termix_identity: "termix_identity", - termix_identity_ca: "termix_identity_ca", - termix_identity_cas: "termix_identity_ca", - termix_identity_key: "termix_identity", - termix_identity_keys: "termix_identity", - termix_identity_public_keys: "termix_identity", - termix_identities: "termix_identity", - termixidca: "termix_identity_ca", - termixidentity: "termix_identity", - dismissed_alert: "dismissed_alerts", - dismissed_alerts: "dismissed_alerts", - dismissedalert: "dismissed_alerts", - dismissedalerts: "dismissed_alerts", - dismissed: "dismissed_alerts", - homepage_layout: "homepage_layouts", - homepage_layouts: "homepage_layouts", - homepagelayout: "homepage_layouts", - homepagelayouts: "homepage_layouts", - layout: "homepage_layouts", - layouts: "homepage_layouts", - homepage_item: "homepage_items", - homepage_items: "homepage_items", - homepageitem: "homepage_items", - homepageitems: "homepage_items", - host_folder: "host_folders", - host_folders: "host_folders", - hostfolder: "host_folders", - hostfolders: "host_folders", - host: "hosts", - hosts: "hosts", - snippet: "snippets", - snippets: "snippets", - host_resolution: "host_resolution", - host_resolver: "host_resolution", - hostresolution: "host_resolution", - hostresolver: "host_resolution", - resolver: "host_resolution", - ssh_folder: "host_folders", - ssh_folders: "host_folders", - sshfolder: "host_folders", - sshfolders: "host_folders", - item: "homepage_items", - items: "homepage_items", - recording: "session_recordings", - recordings: "session_recordings", - session_recording: "session_recordings", - session_recordings: "session_recordings", - sessionrecording: "session_recordings", - sessionrecordings: "session_recordings", - network_topologies: "network_topology", - network_topology: "network_topology", - networktopologies: "network_topology", - networktopology: "network_topology", - topology: "network_topology", - activity: "recent_activity", - recent_activities: "recent_activity", - recent_activity: "recent_activity", - recentactivity: "recent_activity", - credential_usage: "ssh_credential_usage", - ssh_credential_usage: "ssh_credential_usage", - sshcredentialusage: "ssh_credential_usage", - usage: "ssh_credential_usage", - transfer: "transfer_recent", - transfer_recent: "transfer_recent", - transferrecent: "transfer_recent", - export: "user_data_exports", - exports: "user_data_exports", - user_data_export: "user_data_exports", - user_data_exports: "user_data_exports", - userdataexport: "user_data_exports", - userdataexports: "user_data_exports", - bookmark: "file_manager_bookmarks", - bookmarks: "file_manager_bookmarks", - file_bookmarks: "file_manager_bookmarks", - file_manager_bookmark: "file_manager_bookmarks", - file_manager_bookmarks: "file_manager_bookmarks", - filemanagerbookmarks: "file_manager_bookmarks", - c2s: "c2s_tunnel_presets", - c2s_preset: "c2s_tunnel_presets", - c2s_presets: "c2s_tunnel_presets", - c2s_tunnel_preset: "c2s_tunnel_presets", - c2s_tunnel_presets: "c2s_tunnel_presets", - c2stunnelpresets: "c2s_tunnel_presets", - tmux: "tmux_session_tags", - tmux_tag: "tmux_session_tags", - tmux_tags: "tmux_session_tags", - tmux_session_tag: "tmux_session_tags", - tmux_session_tags: "tmux_session_tags", - tmuxsessiontags: "tmux_session_tags", - opkssh: "opkssh_tokens", - opkssh_token: "opkssh_tokens", - opkssh_tokens: "opkssh_tokens", - opksshtoken: "opkssh_tokens", - opksshtokens: "opkssh_tokens", - vault_token: "vault_tokens", - vault_tokens: "vault_tokens", - vaulttoken: "vault_tokens", - vaulttokens: "vault_tokens", - vault_profile: "vault_profiles", - vault_profiles: "vault_profiles", - vaultprofile: "vault_profiles", - vaultprofiles: "vault_profiles", - host_metrics_preference: "host_metrics_preferences", - host_metrics_preferences: "host_metrics_preferences", - hostmetricspreference: "host_metrics_preferences", - hostmetricspreferences: "host_metrics_preferences", - metrics_preferences: "host_metrics_preferences", - health: "host_health", - host_health: "host_health", - host_health_checks: "host_health", - host_health_history: "host_health", - hosthealth: "host_health", - host_metrics_history: "host_metrics_history", - hostmetricshistory: "host_metrics_history", - metrics_history: "host_metrics_history", - open_tab: "open_tabs", - open_tabs: "open_tabs", - opentab: "open_tabs", - opentabs: "open_tabs", - setting: "settings", - settings: "settings", - session: "sessions", - sessions: "sessions", - trusted_device: "trusted_devices", - trusted_devices: "trusted_devices", - trusteddevice: "trusted_devices", - trusteddevices: "trusted_devices", - preference: "user_preferences", - preferences: "user_preferences", - user_preference: "user_preferences", - user_preferences: "user_preferences", - userpreference: "user_preferences", - userpreferences: "user_preferences", - role: "roles", - roles: "roles", - rbac: "rbac_access", - rbac_access: "rbac_access", - rbacaccess: "rbac_access", - shared_credential: "shared_credentials", - shared_credentials: "shared_credentials", - sharedcredential: "shared_credentials", - sharedcredentials: "shared_credentials", - sso: "sso_providers", - sso_provider: "sso_providers", - sso_providers: "sso_providers", - ssoprovider: "sso_providers", - ssoproviders: "sso_providers", - user: "users", - users: "users", -}; - -const DISABLED_VALUES = new Set(["0", "false", "none", "off", "disabled"]); -const ENABLED_VALUES = new Set(["1", "true", "all", "on", "enabled"]); - -function parseDomainList(value: string): RepositoryRolloutDomain[] { - const domains = value - .split(",") - .map((part) => part.trim().toLowerCase().replaceAll("-", "_")) - .filter(Boolean) - .map((part) => { - const domain = DOMAIN_ALIASES[part]; - if (!domain) { - throw new Error( - `Unsupported ${REPOSITORY_ROLLOUT_ENV} domain '${part}'. Expected one of: ${REPOSITORY_ROLLOUT_DOMAINS.join(", ")}.`, - ); - } - return domain; - }); - - return Array.from(new Set(domains)); -} - -export function parseRepositoryRolloutConfig( - env: EnvLike = process.env, -): RepositoryRolloutConfig { - const raw = env[REPOSITORY_ROLLOUT_ENV]; - const normalized = raw?.trim().toLowerCase(); - - if (!normalized) { - return { - mode: "all", - enabledDomains: [...REPOSITORY_ROLLOUT_DOMAINS], - explicit: false, - }; - } - - if (ENABLED_VALUES.has(normalized)) { - return { - mode: "all", - enabledDomains: [...REPOSITORY_ROLLOUT_DOMAINS], - explicit: true, - }; - } - - if (DISABLED_VALUES.has(normalized)) { - return { mode: "none", enabledDomains: [], explicit: true }; - } - - const enabledDomains = parseDomainList(normalized); - return { - mode: - enabledDomains.length === REPOSITORY_ROLLOUT_DOMAINS.length - ? "all" - : "partial", - enabledDomains, - explicit: true, - }; -} - -export function isRepositoryRolloutDomainEnabled( - domain: RepositoryRolloutDomain, - env: EnvLike = process.env, -): boolean { - return parseRepositoryRolloutConfig(env).enabledDomains.includes(domain); -} - -export function getRepositoryRolloutStatus( - env: EnvLike = process.env, -): RepositoryRolloutStatus { - const config = parseRepositoryRolloutConfig(env); - return { - ...config, - envKey: REPOSITORY_ROLLOUT_ENV, - supportedDomains: [...REPOSITORY_ROLLOUT_DOMAINS], - warnings: getRepositoryRolloutWarnings(config), - }; -} - -export function getRepositoryRolloutWarnings( - config: RepositoryRolloutConfig, -): string[] { - const warnings: string[] = []; - - if (!config.explicit) { - warnings.push( - `${REPOSITORY_ROLLOUT_ENV} is not explicitly set; gray targets should set it so rollout state is visible in deployment config.`, - ); - } - - if (config.mode === "none") { - warnings.push( - "All migrated repository domains are disabled; migrated auth/settings/session paths will fail closed.", - ); - } - - if (config.mode === "partial") { - warnings.push( - `Partial repository rollout enabled for domains: ${config.enabledDomains.join(", ")}.`, - ); - } - - return warnings; -} - -export function assertRepositoryRolloutDomainEnabled( - domain: RepositoryRolloutDomain, -): void { - if (isRepositoryRolloutDomainEnabled(domain)) return; - - throw new Error( - `Repository domain '${domain}' is disabled by ${REPOSITORY_ROLLOUT_ENV}.`, - ); -} - -export function logRepositoryRolloutConfig(env: EnvLike = process.env): void { - const config = getRepositoryRolloutStatus(env); - databaseLogger.info("Database repository rollout configuration loaded", { - operation: "repository_rollout_config", - mode: config.mode, - enabledDomains: config.enabledDomains, - explicit: config.explicit, - envKey: REPOSITORY_ROLLOUT_ENV, - }); - - for (const warning of config.warnings) { - databaseLogger.warn(warning, { - operation: "repository_rollout_warning", - mode: config.mode, - enabledDomains: config.enabledDomains, - explicit: config.explicit, - envKey: REPOSITORY_ROLLOUT_ENV, - }); - } -} diff --git a/src/backend/database/repositories/role-repository.test.ts b/src/backend/database/repositories/role-repository.test.ts index f60bd45b..9ffd9711 100644 --- a/src/backend/database/repositories/role-repository.test.ts +++ b/src/backend/database/repositories/role-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { RoleRepository } from "./role-repository.js"; describe("RoleRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("RoleRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/role-repository.ts b/src/backend/database/repositories/role-repository.ts index c47c68f8..47f53d76 100644 --- a/src/backend/database/repositories/role-repository.ts +++ b/src/backend/database/repositories/role-repository.ts @@ -1,6 +1,6 @@ import { and, eq, inArray } from "drizzle-orm"; import { hostAccess, roles, userRoles } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type RoleRecord = typeof roles.$inferSelect; export type NewRoleRecord = typeof roles.$inferInsert; diff --git a/src/backend/database/repositories/session-recording-repository.test.ts b/src/backend/database/repositories/session-recording-repository.test.ts index 96025ab2..f821acb6 100644 --- a/src/backend/database/repositories/session-recording-repository.test.ts +++ b/src/backend/database/repositories/session-recording-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { SessionRecordingRepository } from "./session-recording-repository.js"; describe("SessionRecordingRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("SessionRecordingRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/session-recording-repository.ts b/src/backend/database/repositories/session-recording-repository.ts index 295e1922..8fb10b34 100644 --- a/src/backend/database/repositories/session-recording-repository.ts +++ b/src/backend/database/repositories/session-recording-repository.ts @@ -1,6 +1,6 @@ import { and, desc, eq, inArray, lt } from "drizzle-orm"; import { hosts, sessionRecordings } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type SessionRecordingRecord = typeof sessionRecordings.$inferSelect; @@ -74,9 +74,7 @@ export class SessionRecordingRepository { return rows[0] ?? null; } - async findPathById( - id: number, - ): Promise< + async findPathById(id: number): Promise< | (SessionRecordingPathRecord & { userId: string; format?: string | null; diff --git a/src/backend/database/repositories/session-repository.ts b/src/backend/database/repositories/session-repository.ts index 8197db3c..ff83c46e 100644 --- a/src/backend/database/repositories/session-repository.ts +++ b/src/backend/database/repositories/session-repository.ts @@ -1,6 +1,6 @@ import { and, eq, lte, ne } from "drizzle-orm"; import { sessions } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type SessionRecord = typeof sessions.$inferSelect; export type NewSessionRecord = typeof sessions.$inferInsert; diff --git a/src/backend/database/repositories/settings-repository.test.ts b/src/backend/database/repositories/settings-repository.test.ts index 831f093a..961e60f2 100644 --- a/src/backend/database/repositories/settings-repository.test.ts +++ b/src/backend/database/repositories/settings-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { SettingsRepository } from "./settings-repository.js"; describe("SettingsRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -13,11 +13,7 @@ describe("SettingsRepository", () => { }); async function createRepository(): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE settings ( diff --git a/src/backend/database/repositories/settings-repository.ts b/src/backend/database/repositories/settings-repository.ts index 95d05b72..3b69fbbc 100644 --- a/src/backend/database/repositories/settings-repository.ts +++ b/src/backend/database/repositories/settings-repository.ts @@ -1,6 +1,6 @@ import { eq, like } from "drizzle-orm"; import { settings } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export class SettingsRepository { constructor( diff --git a/src/backend/database/repositories/shared-credential-repository.test.ts b/src/backend/database/repositories/shared-credential-repository.test.ts index 480e7539..d604727a 100644 --- a/src/backend/database/repositories/shared-credential-repository.test.ts +++ b/src/backend/database/repositories/shared-credential-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { SharedCredentialRepository } from "./shared-credential-repository.js"; describe("SharedCredentialRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -17,14 +17,10 @@ describe("SharedCredentialRepository", () => { ): Promise<{ repository: SharedCredentialRepository; sqlite: NonNullable< - Awaited>["sqlite"] + Awaited>["sqlite"] >; }> { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE shared_credentials ( diff --git a/src/backend/database/repositories/shared-credential-repository.ts b/src/backend/database/repositories/shared-credential-repository.ts index 38df52b3..f5fe3397 100644 --- a/src/backend/database/repositories/shared-credential-repository.ts +++ b/src/backend/database/repositories/shared-credential-repository.ts @@ -1,6 +1,6 @@ import { and, eq } from "drizzle-orm"; import { sharedCredentials } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type SharedCredentialRecord = typeof sharedCredentials.$inferSelect; export type NewSharedCredentialRecord = typeof sharedCredentials.$inferInsert; diff --git a/src/backend/database/repositories/snippet-repository.test.ts b/src/backend/database/repositories/snippet-repository.test.ts index 645b034b..ef270cc2 100644 --- a/src/backend/database/repositories/snippet-repository.test.ts +++ b/src/backend/database/repositories/snippet-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { SnippetRepository } from "./snippet-repository.js"; describe("SnippetRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,14 +15,10 @@ describe("SnippetRepository", () => { async function createRepository(onWrite?: () => void): Promise<{ repository: SnippetRepository; sqlite: NonNullable< - Awaited>["sqlite"] + Awaited>["sqlite"] >; }> { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE snippets ( diff --git a/src/backend/database/repositories/snippet-repository.ts b/src/backend/database/repositories/snippet-repository.ts index b6de04bd..5196ff38 100644 --- a/src/backend/database/repositories/snippet-repository.ts +++ b/src/backend/database/repositories/snippet-repository.ts @@ -1,6 +1,6 @@ import { and, asc, eq, sql } from "drizzle-orm"; import { snippetFolders, snippets } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type SnippetRecord = typeof snippets.$inferSelect; export type SnippetFolderRecord = typeof snippetFolders.$inferSelect; diff --git a/src/backend/database/runtime/sqlite-foreign-key-boundary.ts b/src/backend/database/repositories/sqlite-foreign-keys.ts similarity index 85% rename from src/backend/database/runtime/sqlite-foreign-key-boundary.ts rename to src/backend/database/repositories/sqlite-foreign-keys.ts index 52f24f0f..09abf115 100644 --- a/src/backend/database/runtime/sqlite-foreign-key-boundary.ts +++ b/src/backend/database/repositories/sqlite-foreign-keys.ts @@ -1,4 +1,4 @@ -import { getCurrentRepositorySqlite } from "../repositories/current-repository-runtime.js"; +import { getCurrentRepositorySqlite } from "./factory.js"; export interface SqliteForeignKeyClient { exec(sql: string): unknown; diff --git a/src/backend/database/repositories/ssh-credential-usage-repository.test.ts b/src/backend/database/repositories/ssh-credential-usage-repository.test.ts index bbd0f364..48f943c8 100644 --- a/src/backend/database/repositories/ssh-credential-usage-repository.test.ts +++ b/src/backend/database/repositories/ssh-credential-usage-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { SshCredentialUsageRepository } from "./ssh-credential-usage-repository.js"; describe("SshCredentialUsageRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("SshCredentialUsageRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/ssh-credential-usage-repository.ts b/src/backend/database/repositories/ssh-credential-usage-repository.ts index b5bea336..97d1aa0e 100644 --- a/src/backend/database/repositories/ssh-credential-usage-repository.ts +++ b/src/backend/database/repositories/ssh-credential-usage-repository.ts @@ -1,6 +1,6 @@ import { eq, inArray } from "drizzle-orm"; import { sshCredentialUsage } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type SshCredentialUsageRecord = typeof sshCredentialUsage.$inferSelect; diff --git a/src/backend/database/repositories/sso-provider-repository.test.ts b/src/backend/database/repositories/sso-provider-repository.test.ts index 6b9641a1..080d96d1 100644 --- a/src/backend/database/repositories/sso-provider-repository.test.ts +++ b/src/backend/database/repositories/sso-provider-repository.test.ts @@ -1,10 +1,10 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { SsoProviderRepository } from "./sso-provider-repository.js"; describe("SsoProviderRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; - let sqlite: Awaited>["sqlite"]; + let adapter: TestSqliteDatabase | null = null; + let sqlite: Awaited>["sqlite"]; afterEach(async () => { if (adapter) { @@ -17,11 +17,7 @@ describe("SsoProviderRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); sqlite = context.sqlite; context.sqlite?.exec(` diff --git a/src/backend/database/repositories/sso-provider-repository.ts b/src/backend/database/repositories/sso-provider-repository.ts index 35063e87..a6f2e430 100644 --- a/src/backend/database/repositories/sso-provider-repository.ts +++ b/src/backend/database/repositories/sso-provider-repository.ts @@ -1,6 +1,6 @@ import { asc, eq } from "drizzle-orm"; import { ssoProviders, users } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type SsoProviderRecord = typeof ssoProviders.$inferSelect; export type NewSsoProviderRecord = typeof ssoProviders.$inferInsert; diff --git a/src/backend/database/repositories/termix-identity-ca-repository.test.ts b/src/backend/database/repositories/termix-identity-ca-repository.test.ts index b57cd8d0..4b5fc08b 100644 --- a/src/backend/database/repositories/termix-identity-ca-repository.test.ts +++ b/src/backend/database/repositories/termix-identity-ca-repository.test.ts @@ -1,10 +1,10 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { DataCrypto } from "../../utils/data-crypto.js"; import { TermixIdentityCaRepository } from "./termix-identity-ca-repository.js"; describe("TermixIdentityCaRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { vi.restoreAllMocks(); @@ -17,15 +17,11 @@ describe("TermixIdentityCaRepository", () => { async function createRepository(onWrite = vi.fn()): Promise<{ repo: TermixIdentityCaRepository; sqlite: NonNullable< - Awaited>["sqlite"] + Awaited>["sqlite"] >; onWrite: ReturnType; }> { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/termix-identity-ca-repository.ts b/src/backend/database/repositories/termix-identity-ca-repository.ts index 7660ebd5..ea1fcd3b 100644 --- a/src/backend/database/repositories/termix-identity-ca-repository.ts +++ b/src/backend/database/repositories/termix-identity-ca-repository.ts @@ -1,6 +1,6 @@ import { eq } from "drizzle-orm"; import { termixIdentityCa } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; import { DataCrypto } from "../../utils/data-crypto.js"; export type TermixIdentityCaRecord = typeof termixIdentityCa.$inferSelect; diff --git a/src/backend/database/repositories/termix-identity-repository.test.ts b/src/backend/database/repositories/termix-identity-repository.test.ts index 75197133..20342336 100644 --- a/src/backend/database/repositories/termix-identity-repository.test.ts +++ b/src/backend/database/repositories/termix-identity-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { TermixIdentityRepository } from "./termix-identity-repository.js"; describe("TermixIdentityRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -16,11 +16,7 @@ describe("TermixIdentityRepository", () => { repo: TermixIdentityRepository; onWrite: ReturnType; }> { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/termix-identity-repository.ts b/src/backend/database/repositories/termix-identity-repository.ts index 596d8ffa..9badeedd 100644 --- a/src/backend/database/repositories/termix-identity-repository.ts +++ b/src/backend/database/repositories/termix-identity-repository.ts @@ -1,6 +1,6 @@ import { and, asc, eq } from "drizzle-orm"; import { termixIdentities, termixIdentityKeys } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type TermixIdentityRecord = typeof termixIdentities.$inferSelect; export type NewTermixIdentityRecord = typeof termixIdentities.$inferInsert; diff --git a/src/backend/database/repositories/test-support.ts b/src/backend/database/repositories/test-support.ts new file mode 100644 index 00000000..8da979a6 --- /dev/null +++ b/src/backend/database/repositories/test-support.ts @@ -0,0 +1,31 @@ +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import * as schema from "../db/schema.js"; +import type { DatabaseContext } from "./database-context.js"; + +export class TestSqliteDatabase { + private sqlite: Database.Database | null = null; + private context: DatabaseContext | null = null; + + async connect(): Promise { + if (this.context) return this.context; + + this.sqlite = new Database(":memory:"); + this.sqlite.exec("PRAGMA foreign_keys = ON"); + this.context = { + dialect: "sqlite", + drizzle: drizzle(this.sqlite, { schema }), + sqlite: this.sqlite, + }; + + return this.context; + } + + async close(): Promise { + if (this.sqlite) { + this.sqlite.close(); + this.sqlite = null; + this.context = null; + } + } +} diff --git a/src/backend/database/repositories/tmux-session-tag-repository.test.ts b/src/backend/database/repositories/tmux-session-tag-repository.test.ts index e5737ce2..01e74fd3 100644 --- a/src/backend/database/repositories/tmux-session-tag-repository.test.ts +++ b/src/backend/database/repositories/tmux-session-tag-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { TmuxSessionTagRepository } from "./tmux-session-tag-repository.js"; describe("TmuxSessionTagRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("TmuxSessionTagRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/tmux-session-tag-repository.ts b/src/backend/database/repositories/tmux-session-tag-repository.ts index 836da4b3..3d89d856 100644 --- a/src/backend/database/repositories/tmux-session-tag-repository.ts +++ b/src/backend/database/repositories/tmux-session-tag-repository.ts @@ -1,6 +1,6 @@ import { and, eq } from "drizzle-orm"; import { tmuxSessionTags } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type TmuxSessionTagRecord = typeof tmuxSessionTags.$inferSelect; diff --git a/src/backend/database/repositories/transfer-recent-repository.test.ts b/src/backend/database/repositories/transfer-recent-repository.test.ts index 9dc29d8e..e26c8306 100644 --- a/src/backend/database/repositories/transfer-recent-repository.test.ts +++ b/src/backend/database/repositories/transfer-recent-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { TransferRecentRepository } from "./transfer-recent-repository.js"; describe("TransferRecentRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("TransferRecentRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/transfer-recent-repository.ts b/src/backend/database/repositories/transfer-recent-repository.ts index 4c804e5a..309b270e 100644 --- a/src/backend/database/repositories/transfer-recent-repository.ts +++ b/src/backend/database/repositories/transfer-recent-repository.ts @@ -1,6 +1,6 @@ import { and, desc, eq, inArray, or } from "drizzle-orm"; import { transferRecent } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type TransferRecentRecord = typeof transferRecent.$inferSelect; diff --git a/src/backend/database/repositories/trusted-device-repository.test.ts b/src/backend/database/repositories/trusted-device-repository.test.ts index 475a865c..26e6e947 100644 --- a/src/backend/database/repositories/trusted-device-repository.test.ts +++ b/src/backend/database/repositories/trusted-device-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { TrustedDeviceRepository } from "./trusted-device-repository.js"; describe("TrustedDeviceRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("TrustedDeviceRepository", () => { async function createRepository(onWrite?: () => void): Promise<{ trustedDevices: TrustedDeviceRepository; }> { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/trusted-device-repository.ts b/src/backend/database/repositories/trusted-device-repository.ts index 187096c0..ee0f4b7a 100644 --- a/src/backend/database/repositories/trusted-device-repository.ts +++ b/src/backend/database/repositories/trusted-device-repository.ts @@ -1,6 +1,6 @@ import { and, eq } from "drizzle-orm"; import { trustedDevices } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type TrustedDeviceRecord = typeof trustedDevices.$inferSelect; export type NewTrustedDeviceRecord = typeof trustedDevices.$inferInsert; diff --git a/src/backend/database/repositories/user-data-export-repository.test.ts b/src/backend/database/repositories/user-data-export-repository.test.ts index 046f6185..7ec817a4 100644 --- a/src/backend/database/repositories/user-data-export-repository.test.ts +++ b/src/backend/database/repositories/user-data-export-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { UserDataExportRepository } from "./user-data-export-repository.js"; describe("UserDataExportRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -13,11 +13,7 @@ describe("UserDataExportRepository", () => { }); async function createRepository(): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/user-data-export-repository.ts b/src/backend/database/repositories/user-data-export-repository.ts index ce3a3157..da72c26c 100644 --- a/src/backend/database/repositories/user-data-export-repository.ts +++ b/src/backend/database/repositories/user-data-export-repository.ts @@ -1,6 +1,6 @@ import { eq } from "drizzle-orm"; import { hosts, sshCredentials } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type UserDataExportHostRecord = typeof hosts.$inferSelect; export type UserDataExportCredentialRecord = typeof sshCredentials.$inferSelect; diff --git a/src/backend/database/repositories/user-preference-repository.test.ts b/src/backend/database/repositories/user-preference-repository.test.ts index de3ede49..269a0c7d 100644 --- a/src/backend/database/repositories/user-preference-repository.test.ts +++ b/src/backend/database/repositories/user-preference-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { UserPreferenceRepository } from "./user-preference-repository.js"; describe("UserPreferenceRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("UserPreferenceRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/user-preference-repository.ts b/src/backend/database/repositories/user-preference-repository.ts index d96703da..7163c284 100644 --- a/src/backend/database/repositories/user-preference-repository.ts +++ b/src/backend/database/repositories/user-preference-repository.ts @@ -1,6 +1,6 @@ import { eq } from "drizzle-orm"; import { userPreferences } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type UserPreferenceRecord = typeof userPreferences.$inferSelect; export type NewUserPreferenceRecord = typeof userPreferences.$inferInsert; diff --git a/src/backend/database/repositories/user-repository.ts b/src/backend/database/repositories/user-repository.ts index b36a4483..68442f4f 100644 --- a/src/backend/database/repositories/user-repository.ts +++ b/src/backend/database/repositories/user-repository.ts @@ -1,6 +1,6 @@ import { eq, inArray } from "drizzle-orm"; import { users } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type UserRecord = typeof users.$inferSelect; export type NewUserRecord = typeof users.$inferInsert; diff --git a/src/backend/database/repositories/user-session-repositories.test.ts b/src/backend/database/repositories/user-session-repositories.test.ts index a90a8af6..93d2cfbc 100644 --- a/src/backend/database/repositories/user-session-repositories.test.ts +++ b/src/backend/database/repositories/user-session-repositories.test.ts @@ -1,10 +1,10 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { SessionRepository } from "./session-repository.js"; import { UserRepository } from "./user-repository.js"; describe("UserRepository and SessionRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -33,11 +33,7 @@ describe("UserRepository and SessionRepository", () => { users: UserRepository; sessions: SessionRepository; }> { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/vault-profile-repository.test.ts b/src/backend/database/repositories/vault-profile-repository.test.ts index 027d87c6..e5cb35a4 100644 --- a/src/backend/database/repositories/vault-profile-repository.test.ts +++ b/src/backend/database/repositories/vault-profile-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { VaultProfileRepository } from "./vault-profile-repository.js"; describe("VaultProfileRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("VaultProfileRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/vault-profile-repository.ts b/src/backend/database/repositories/vault-profile-repository.ts index cf063cbe..319a7b4d 100644 --- a/src/backend/database/repositories/vault-profile-repository.ts +++ b/src/backend/database/repositories/vault-profile-repository.ts @@ -1,6 +1,6 @@ import { desc, eq, or } from "drizzle-orm"; import { vaultProfiles } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type VaultProfileRecord = typeof vaultProfiles.$inferSelect; diff --git a/src/backend/database/repositories/vault-token-repository.test.ts b/src/backend/database/repositories/vault-token-repository.test.ts index ce9c01ce..f11667cf 100644 --- a/src/backend/database/repositories/vault-token-repository.test.ts +++ b/src/backend/database/repositories/vault-token-repository.test.ts @@ -1,9 +1,9 @@ import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "../runtime/sqlite-adapter.js"; +import { TestSqliteDatabase } from "./test-support.js"; import { VaultTokenRepository } from "./vault-token-repository.js"; describe("VaultTokenRepository", () => { - let adapter: SqliteDatabaseAdapter | null = null; + let adapter: TestSqliteDatabase | null = null; afterEach(async () => { if (adapter) { @@ -15,11 +15,7 @@ describe("VaultTokenRepository", () => { async function createRepository( onWrite?: () => void | Promise, ): Promise { - adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); + adapter = new TestSqliteDatabase(); const context = await adapter.connect(); context.sqlite?.exec(` CREATE TABLE users ( diff --git a/src/backend/database/repositories/vault-token-repository.ts b/src/backend/database/repositories/vault-token-repository.ts index a40d67f2..1db262f0 100644 --- a/src/backend/database/repositories/vault-token-repository.ts +++ b/src/backend/database/repositories/vault-token-repository.ts @@ -1,6 +1,6 @@ import { and, eq } from "drizzle-orm"; import { vaultTokens } from "../db/schema.js"; -import type { DatabaseContext } from "../runtime/adapter.js"; +import type { DatabaseContext } from "./database-context.js"; export type VaultTokenRecord = typeof vaultTokens.$inferSelect; diff --git a/src/backend/database/routes/acme-ssl-routes.ts b/src/backend/database/routes/acme-ssl-routes.ts index d402877f..317a08a9 100644 --- a/src/backend/database/routes/acme-ssl-routes.ts +++ b/src/backend/database/routes/acme-ssl-routes.ts @@ -5,8 +5,10 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import type { RequestHandler, Router } from "express"; import { authLogger } from "../../utils/logger.js"; import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; -import { createCurrentSettingsRepository } from "../repositories/current-settings-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentSettingsRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; import type { UserRecord } from "../repositories/user-repository.js"; const DATA_DIR = process.env.DATA_DIR || "./db/data"; diff --git a/src/backend/database/routes/alert-rules-routes.ts b/src/backend/database/routes/alert-rules-routes.ts index 6bfccf90..5ce379a4 100644 --- a/src/backend/database/routes/alert-rules-routes.ts +++ b/src/backend/database/routes/alert-rules-routes.ts @@ -1,6 +1,6 @@ import express, { type Request, type Response } from "express"; import type { AuthenticatedRequest } from "../../../types/index.js"; -import { createCurrentAlertRepository } from "../repositories/current-alert-repository.js"; +import { createCurrentAlertRepository } from "../repositories/factory.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { databaseLogger } from "../../utils/logger.js"; import { sendWebhook, sendNtfy } from "../../utils/notification-sender.js"; diff --git a/src/backend/database/routes/alerts.ts b/src/backend/database/routes/alerts.ts index faed11ea..5cbff237 100644 --- a/src/backend/database/routes/alerts.ts +++ b/src/backend/database/routes/alerts.ts @@ -7,7 +7,7 @@ import express from "express"; import { authLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { getProxyAgent } from "../../utils/proxy-agent.js"; -import { createCurrentDismissedAlertRepository } from "../repositories/current-dismissed-alert-repository.js"; +import { createCurrentDismissedAlertRepository } from "../repositories/factory.js"; class AlertCache { private cache: Map = new Map(); diff --git a/src/backend/database/routes/audit-log-routes.ts b/src/backend/database/routes/audit-log-routes.ts index 1d93f2d2..91273b91 100644 --- a/src/backend/database/routes/audit-log-routes.ts +++ b/src/backend/database/routes/audit-log-routes.ts @@ -1,7 +1,9 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import type { RequestHandler, Router } from "express"; -import { createCurrentAuditLogRepository } from "../repositories/current-audit-log-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentAuditLogRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; import { apiLogger } from "../../utils/logger.js"; async function isAdminUser(userId: string | undefined): Promise { diff --git a/src/backend/database/routes/c2s-tunnel-presets.ts b/src/backend/database/routes/c2s-tunnel-presets.ts index cc51321f..9eccc34a 100644 --- a/src/backend/database/routes/c2s-tunnel-presets.ts +++ b/src/backend/database/routes/c2s-tunnel-presets.ts @@ -7,7 +7,7 @@ import type { Request, Response } from "express"; import { authLogger, databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import type { C2sTunnelPresetRecord } from "../repositories/c2s-tunnel-preset-repository.js"; -import { createCurrentC2sTunnelPresetRepository } from "../repositories/current-c2s-tunnel-preset-repository.js"; +import { createCurrentC2sTunnelPresetRepository } from "../repositories/factory.js"; const router = express.Router(); diff --git a/src/backend/database/routes/credential-deploy-routes.ts b/src/backend/database/routes/credential-deploy-routes.ts index fecac74d..1e85386f 100644 --- a/src/backend/database/routes/credential-deploy-routes.ts +++ b/src/backend/database/routes/credential-deploy-routes.ts @@ -4,7 +4,7 @@ import type { } from "../../../types/index.js"; import type { Request, RequestHandler, Response, Router } from "express"; import ssh2Pkg from "ssh2"; -import { createCurrentHostResolutionRepository } from "../repositories/current-host-resolution-repository.js"; +import { createCurrentHostResolutionRepository } from "../repositories/factory.js"; import { preparePrivateKeyForSSH2 } from "../../utils/ssh-key-utils.js"; const { Client } = ssh2Pkg; diff --git a/src/backend/database/routes/credentials.ts b/src/backend/database/routes/credentials.ts index 221f4d6d..19284b93 100644 --- a/src/backend/database/routes/credentials.ts +++ b/src/backend/database/routes/credentials.ts @@ -7,11 +7,13 @@ import { parseSSHKey } from "../../utils/ssh-key-utils.js"; import { registerCredentialKeyRoutes } from "./credential-key-routes.js"; import { registerCredentialDeployRoutes } from "./credential-deploy-routes.js"; import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; -import { createCurrentRbacAccessRepository } from "../repositories/current-rbac-access-repository.js"; -import { createCurrentCredentialRepository } from "../repositories/current-credential-repository.js"; -import { createCurrentHostResolutionRepository } from "../repositories/current-host-resolution-repository.js"; -import { createCurrentHostRepository } from "../repositories/current-host-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentRbacAccessRepository, + createCurrentCredentialRepository, + createCurrentHostResolutionRepository, + createCurrentHostRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; const router = express.Router(); diff --git a/src/backend/database/routes/dashboard-service-links-routes.ts b/src/backend/database/routes/dashboard-service-links-routes.ts index 9cad34cb..34829356 100644 --- a/src/backend/database/routes/dashboard-service-links-routes.ts +++ b/src/backend/database/routes/dashboard-service-links-routes.ts @@ -4,7 +4,7 @@ import { dashboardLogger } from "../../utils/logger.js"; import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js"; import { isNonEmptyString } from "./host-normalizers.js"; import express from "express"; -import { createCurrentDashboardServiceLinkRepository } from "../repositories/current-dashboard-service-link-repository.js"; +import { createCurrentDashboardServiceLinkRepository } from "../repositories/factory.js"; export const dashboardServiceLinksRouter = express.Router(); diff --git a/src/backend/database/routes/delete-user-data.ts b/src/backend/database/routes/delete-user-data.ts index f09279b4..c6693983 100644 --- a/src/backend/database/routes/delete-user-data.ts +++ b/src/backend/database/routes/delete-user-data.ts @@ -1,40 +1,42 @@ import { authLogger } from "../../utils/logger.js"; -import { createCurrentAlertRepository } from "../repositories/current-alert-repository.js"; -import { createCurrentApiKeyRepository } from "../repositories/current-api-key-repository.js"; -import { createCurrentAuditLogRepository } from "../repositories/current-audit-log-repository.js"; -import { createCurrentC2sTunnelPresetRepository } from "../repositories/current-c2s-tunnel-preset-repository.js"; -import { createCurrentCommandHistoryRepository } from "../repositories/current-command-history-repository.js"; -import { createCurrentCredentialRepository } from "../repositories/current-credential-repository.js"; -import { createCurrentDashboardServiceLinkRepository } from "../repositories/current-dashboard-service-link-repository.js"; -import { createCurrentDismissedAlertRepository } from "../repositories/current-dismissed-alert-repository.js"; -import { createCurrentFileManagerBookmarkRepository } from "../repositories/current-file-manager-bookmark-repository.js"; -import { createCurrentHomepageItemRepository } from "../repositories/current-homepage-item-repository.js"; -import { createCurrentHomepageLayoutRepository } from "../repositories/current-homepage-layout-repository.js"; -import { createCurrentHostHealthRepository } from "../repositories/current-host-health-repository.js"; -import { createCurrentHostFolderRepository } from "../repositories/current-host-folder-repository.js"; -import { createCurrentHostMetricsPreferenceRepository } from "../repositories/current-host-metrics-preference-repository.js"; -import { createCurrentHostRepository } from "../repositories/current-host-repository.js"; -import { createCurrentNetworkTopologyRepository } from "../repositories/current-network-topology-repository.js"; -import { createCurrentOpksshTokenRepository } from "../repositories/current-opkssh-token-repository.js"; -import { createCurrentOpenTabRepository } from "../repositories/current-open-tab-repository.js"; -import { createCurrentRecentActivityRepository } from "../repositories/current-recent-activity-repository.js"; -import { createCurrentRbacAccessRepository } from "../repositories/current-rbac-access-repository.js"; -import { createCurrentRoleRepository } from "../repositories/current-role-repository.js"; -import { createCurrentSessionRepository } from "../repositories/current-session-repository.js"; -import { createCurrentSessionRecordingRepository } from "../repositories/current-session-recording-repository.js"; -import { createCurrentSettingsRepository } from "../repositories/current-settings-repository.js"; -import { createCurrentSharedCredentialRepository } from "../repositories/current-shared-credential-repository.js"; -import { createCurrentSnippetRepository } from "../repositories/current-snippet-repository.js"; -import { createCurrentSshCredentialUsageRepository } from "../repositories/current-ssh-credential-usage-repository.js"; -import { createCurrentTermixIdentityCaRepository } from "../repositories/current-termix-identity-ca-repository.js"; -import { createCurrentTermixIdentityRepository } from "../repositories/current-termix-identity-repository.js"; -import { createCurrentTmuxSessionTagRepository } from "../repositories/current-tmux-session-tag-repository.js"; -import { createCurrentTrustedDeviceRepository } from "../repositories/current-trusted-device-repository.js"; -import { createCurrentUserPreferenceRepository } from "../repositories/current-user-preference-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; -import { createCurrentTransferRecentRepository } from "../repositories/current-transfer-recent-repository.js"; -import { createCurrentVaultProfileRepository } from "../repositories/current-vault-profile-repository.js"; -import { createCurrentVaultTokenRepository } from "../repositories/current-vault-token-repository.js"; +import { + createCurrentAlertRepository, + createCurrentApiKeyRepository, + createCurrentAuditLogRepository, + createCurrentC2sTunnelPresetRepository, + createCurrentCommandHistoryRepository, + createCurrentCredentialRepository, + createCurrentDashboardServiceLinkRepository, + createCurrentDismissedAlertRepository, + createCurrentFileManagerBookmarkRepository, + createCurrentHomepageItemRepository, + createCurrentHomepageLayoutRepository, + createCurrentHostHealthRepository, + createCurrentHostFolderRepository, + createCurrentHostMetricsPreferenceRepository, + createCurrentHostRepository, + createCurrentNetworkTopologyRepository, + createCurrentOpksshTokenRepository, + createCurrentOpenTabRepository, + createCurrentRecentActivityRepository, + createCurrentRbacAccessRepository, + createCurrentRoleRepository, + createCurrentSessionRepository, + createCurrentSessionRecordingRepository, + createCurrentSettingsRepository, + createCurrentSharedCredentialRepository, + createCurrentSnippetRepository, + createCurrentSshCredentialUsageRepository, + createCurrentTermixIdentityCaRepository, + createCurrentTermixIdentityRepository, + createCurrentTmuxSessionTagRepository, + createCurrentTrustedDeviceRepository, + createCurrentUserPreferenceRepository, + createCurrentUserRepository, + createCurrentTransferRecentRepository, + createCurrentVaultProfileRepository, + createCurrentVaultTokenRepository, +} from "../repositories/factory.js"; export async function deleteUserAndRelatedData(userId: string): Promise { try { diff --git a/src/backend/database/routes/homepage-items-routes.ts b/src/backend/database/routes/homepage-items-routes.ts index fa6583b9..5ea49791 100644 --- a/src/backend/database/routes/homepage-items-routes.ts +++ b/src/backend/database/routes/homepage-items-routes.ts @@ -1,7 +1,7 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import type { Request, Response } from "express"; import { homepageLogger } from "../../utils/logger.js"; -import { createCurrentHomepageItemRepository } from "../repositories/current-homepage-item-repository.js"; +import { createCurrentHomepageItemRepository } from "../repositories/factory.js"; import express from "express"; export const homepageItemsRouter = express.Router(); diff --git a/src/backend/database/routes/homepage-layout-routes.ts b/src/backend/database/routes/homepage-layout-routes.ts index 16566a7b..a02ba5a0 100644 --- a/src/backend/database/routes/homepage-layout-routes.ts +++ b/src/backend/database/routes/homepage-layout-routes.ts @@ -2,7 +2,7 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import type { Request, Response } from "express"; import { homepageLogger } from "../../utils/logger.js"; import express from "express"; -import { createCurrentHomepageLayoutRepository } from "../repositories/current-homepage-layout-repository.js"; +import { createCurrentHomepageLayoutRepository } from "../repositories/factory.js"; export const homepageLayoutRouter = express.Router(); diff --git a/src/backend/database/routes/host-autostart-routes.ts b/src/backend/database/routes/host-autostart-routes.ts index 61d2881b..42aa4276 100644 --- a/src/backend/database/routes/host-autostart-routes.ts +++ b/src/backend/database/routes/host-autostart-routes.ts @@ -2,7 +2,7 @@ import type { Request, RequestHandler, Response, Router } from "express"; import type { AuthenticatedRequest } from "../../../types/index.js"; import { DataCrypto } from "../../utils/data-crypto.js"; import { sshLogger } from "../../utils/logger.js"; -import { createCurrentHostRepository } from "../repositories/current-host-repository.js"; +import { createCurrentHostRepository } from "../repositories/factory.js"; type HostAutostartRoutesDeps = { authenticateJWT: RequestHandler; diff --git a/src/backend/database/routes/host-bulk-routes.ts b/src/backend/database/routes/host-bulk-routes.ts index 0bd0b205..2838a4b5 100644 --- a/src/backend/database/routes/host-bulk-routes.ts +++ b/src/backend/database/routes/host-bulk-routes.ts @@ -1,9 +1,11 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import type { Request, RequestHandler, Response, Router } from "express"; import { sshLogger } from "../../utils/logger.js"; -import { createCurrentCredentialRepository } from "../repositories/current-credential-repository.js"; -import { createCurrentHostRepository } from "../repositories/current-host-repository.js"; -import { createCurrentHostResolutionRepository } from "../repositories/current-host-resolution-repository.js"; +import { + createCurrentCredentialRepository, + createCurrentHostRepository, + createCurrentHostResolutionRepository, +} from "../repositories/factory.js"; import { isNonEmptyString, isValidPort, diff --git a/src/backend/database/routes/host-command-history-routes.ts b/src/backend/database/routes/host-command-history-routes.ts index 31bf1992..0f704044 100644 --- a/src/backend/database/routes/host-command-history-routes.ts +++ b/src/backend/database/routes/host-command-history-routes.ts @@ -1,7 +1,7 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import type { Request, RequestHandler, Response, Router } from "express"; import { sshLogger } from "../../utils/logger.js"; -import { createCurrentCommandHistoryRepository } from "../repositories/current-command-history-repository.js"; +import { createCurrentCommandHistoryRepository } from "../repositories/factory.js"; import { isNonEmptyString } from "./host-normalizers.js"; export function registerHostCommandHistoryRoutes( diff --git a/src/backend/database/routes/host-file-manager-bookmark-routes.ts b/src/backend/database/routes/host-file-manager-bookmark-routes.ts index 276b1eec..a56f299c 100644 --- a/src/backend/database/routes/host-file-manager-bookmark-routes.ts +++ b/src/backend/database/routes/host-file-manager-bookmark-routes.ts @@ -1,7 +1,7 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import type { Request, RequestHandler, Response, Router } from "express"; import { sshLogger } from "../../utils/logger.js"; -import { createCurrentFileManagerBookmarkRepository } from "../repositories/current-file-manager-bookmark-repository.js"; +import { createCurrentFileManagerBookmarkRepository } from "../repositories/factory.js"; import { isNonEmptyString } from "./host-normalizers.js"; export function registerHostFileManagerBookmarkRoutes( diff --git a/src/backend/database/routes/host-folder-routes.ts b/src/backend/database/routes/host-folder-routes.ts index de855442..dfde1491 100644 --- a/src/backend/database/routes/host-folder-routes.ts +++ b/src/backend/database/routes/host-folder-routes.ts @@ -1,14 +1,16 @@ import type { Request, RequestHandler, Response, Router } from "express"; import type { AuthenticatedRequest } from "../../../types/index.js"; import { databaseLogger, sshLogger } from "../../utils/logger.js"; -import { createCurrentCommandHistoryRepository } from "../repositories/current-command-history-repository.js"; -import { createCurrentFileManagerBookmarkRepository } from "../repositories/current-file-manager-bookmark-repository.js"; -import { createCurrentHostFolderRepository } from "../repositories/current-host-folder-repository.js"; -import { createCurrentRecentActivityRepository } from "../repositories/current-recent-activity-repository.js"; -import { createCurrentRbacAccessRepository } from "../repositories/current-rbac-access-repository.js"; -import { createCurrentSshCredentialUsageRepository } from "../repositories/current-ssh-credential-usage-repository.js"; -import { createCurrentSessionRecordingRepository } from "../repositories/current-session-recording-repository.js"; -import { createCurrentTransferRecentRepository } from "../repositories/current-transfer-recent-repository.js"; +import { + createCurrentCommandHistoryRepository, + createCurrentFileManagerBookmarkRepository, + createCurrentHostFolderRepository, + createCurrentRecentActivityRepository, + createCurrentRbacAccessRepository, + createCurrentSshCredentialUsageRepository, + createCurrentSessionRecordingRepository, + createCurrentTransferRecentRepository, +} from "../repositories/factory.js"; import { isNonEmptyString } from "./host-normalizers.js"; type HostFolderRoutesDeps = { diff --git a/src/backend/database/routes/host-internal-routes.ts b/src/backend/database/routes/host-internal-routes.ts index 392e12e3..504e4028 100644 --- a/src/backend/database/routes/host-internal-routes.ts +++ b/src/backend/database/routes/host-internal-routes.ts @@ -1,7 +1,7 @@ import type { Request, Response, Router } from "express"; import { SystemCrypto } from "../../utils/system-crypto.js"; import { sshLogger } from "../../utils/logger.js"; -import { createCurrentHostResolutionRepository } from "../repositories/current-host-resolution-repository.js"; +import { createCurrentHostResolutionRepository } from "../repositories/factory.js"; export function registerHostInternalRoutes(router: Router): void { /** diff --git a/src/backend/database/routes/host-network-routes.ts b/src/backend/database/routes/host-network-routes.ts index db5ca0c0..6050efe5 100644 --- a/src/backend/database/routes/host-network-routes.ts +++ b/src/backend/database/routes/host-network-routes.ts @@ -2,7 +2,7 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import type { Request, RequestHandler, Response, Router } from "express"; import { sendWakeOnLan, isValidMac } from "../../utils/wake-on-lan.js"; import { sshLogger } from "../../utils/logger.js"; -import { createCurrentHostResolutionRepository } from "../repositories/current-host-resolution-repository.js"; +import { createCurrentHostResolutionRepository } from "../repositories/factory.js"; interface HostNetworkRoutesDeps { authenticateJWT: RequestHandler; diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts index 92c7cae0..c1bfc133 100644 --- a/src/backend/database/routes/host.ts +++ b/src/backend/database/routes/host.ts @@ -12,13 +12,20 @@ import { pickResolvedPassword, pickResolvedUsername, } from "../../ssh/credential-username.js"; -import { createCurrentCommandHistoryRepository } from "../repositories/current-command-history-repository.js"; -import { createCurrentFileManagerBookmarkRepository } from "../repositories/current-file-manager-bookmark-repository.js"; -import { createCurrentOpksshTokenRepository } from "../repositories/current-opkssh-token-repository.js"; -import { createCurrentRecentActivityRepository } from "../repositories/current-recent-activity-repository.js"; -import { createCurrentSshCredentialUsageRepository } from "../repositories/current-ssh-credential-usage-repository.js"; -import { createCurrentSessionRecordingRepository } from "../repositories/current-session-recording-repository.js"; -import { createCurrentTransferRecentRepository } from "../repositories/current-transfer-recent-repository.js"; +import { + createCurrentCommandHistoryRepository, + createCurrentFileManagerBookmarkRepository, + createCurrentOpksshTokenRepository, + createCurrentRecentActivityRepository, + createCurrentSshCredentialUsageRepository, + createCurrentSessionRecordingRepository, + createCurrentTransferRecentRepository, + createCurrentRbacAccessRepository, + createCurrentRoleRepository, + createCurrentHostResolutionRepository, + createCurrentHostRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; import { isNonEmptyString, isValidPort, @@ -38,11 +45,6 @@ import { requireHostEnrollmentAccessForPath, } from "./host-enrollment-auth.js"; import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; -import { createCurrentRbacAccessRepository } from "../repositories/current-rbac-access-repository.js"; -import { createCurrentRoleRepository } from "../repositories/current-role-repository.js"; -import { createCurrentHostResolutionRepository } from "../repositories/current-host-resolution-repository.js"; -import { createCurrentHostRepository } from "../repositories/current-host-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; const router = express.Router(); diff --git a/src/backend/database/routes/ldap-auth-routes.ts b/src/backend/database/routes/ldap-auth-routes.ts index 799ddb94..b7cbf7fc 100644 --- a/src/backend/database/routes/ldap-auth-routes.ts +++ b/src/backend/database/routes/ldap-auth-routes.ts @@ -6,10 +6,12 @@ import { AuthManager } from "../../utils/auth-manager.js"; import { parseUserAgent } from "../../utils/user-agent-parser.js"; import { isOIDCUserAllowed, loadProviderConfig } from "./user-oidc-utils.js"; import ldap from "ldapjs"; -import { createCurrentRoleRepository } from "../repositories/current-role-repository.js"; -import { createCurrentSettingsRepository } from "../repositories/current-settings-repository.js"; -import { createCurrentSsoProviderRepository } from "../repositories/current-sso-provider-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentRoleRepository, + createCurrentSettingsRepository, + createCurrentSsoProviderRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; const authManager = AuthManager.getInstance(); diff --git a/src/backend/database/routes/network-topology.ts b/src/backend/database/routes/network-topology.ts index 77744b33..95167f23 100644 --- a/src/backend/database/routes/network-topology.ts +++ b/src/backend/database/routes/network-topology.ts @@ -2,7 +2,7 @@ import express from "express"; import { AuthManager } from "../../utils/auth-manager.js"; import type { AuthenticatedRequest } from "../../../types/index.js"; import { databaseLogger } from "../../utils/logger.js"; -import { createCurrentNetworkTopologyRepository } from "../repositories/current-network-topology-repository.js"; +import { createCurrentNetworkTopologyRepository } from "../repositories/factory.js"; const router = express.Router(); const authManager = AuthManager.getInstance(); diff --git a/src/backend/database/routes/open-tabs.ts b/src/backend/database/routes/open-tabs.ts index 182bbc11..18577c5c 100644 --- a/src/backend/database/routes/open-tabs.ts +++ b/src/backend/database/routes/open-tabs.ts @@ -4,8 +4,10 @@ import type { Request, Response } from "express"; import { databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { sessionManager } from "../../ssh/terminal-session-manager.js"; -import { getCurrentSettingValue } from "../repositories/current-settings-repository.js"; -import { createCurrentOpenTabRepository } from "../repositories/current-open-tab-repository.js"; +import { + getCurrentSettingValue, + createCurrentOpenTabRepository, +} from "../repositories/factory.js"; const router = express.Router(); const authManager = AuthManager.getInstance(); diff --git a/src/backend/database/routes/rbac.ts b/src/backend/database/routes/rbac.ts index 364e12a4..e7ee35c1 100644 --- a/src/backend/database/routes/rbac.ts +++ b/src/backend/database/routes/rbac.ts @@ -4,12 +4,14 @@ import type { Response } from "express"; import { databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { PermissionManager } from "../../utils/permission-manager.js"; -import { createCurrentCredentialRepository } from "../repositories/current-credential-repository.js"; -import { createCurrentHostResolutionRepository } from "../repositories/current-host-resolution-repository.js"; -import { createCurrentRbacAccessRepository } from "../repositories/current-rbac-access-repository.js"; -import { createCurrentRoleRepository } from "../repositories/current-role-repository.js"; -import { createCurrentSnippetRepository } from "../repositories/current-snippet-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentCredentialRepository, + createCurrentHostResolutionRepository, + createCurrentRbacAccessRepository, + createCurrentRoleRepository, + createCurrentSnippetRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; const router = express.Router(); diff --git a/src/backend/database/routes/session-log-routes.ts b/src/backend/database/routes/session-log-routes.ts index e25620ca..bc723e2e 100644 --- a/src/backend/database/routes/session-log-routes.ts +++ b/src/backend/database/routes/session-log-routes.ts @@ -6,7 +6,7 @@ import { apiLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import type { Request, Response } from "express"; import { PermissionManager } from "../../utils/permission-manager.js"; -import { createCurrentSessionRecordingRepository } from "../repositories/current-session-recording-repository.js"; +import { createCurrentSessionRecordingRepository } from "../repositories/factory.js"; import { getDb } from "../db/index.js"; const router = express.Router(); @@ -29,8 +29,8 @@ function getRetentionDays(): number { 10, ); try { - const row = getDb().$client - .prepare( + const row = getDb() + .$client.prepare( "SELECT value FROM settings WHERE key = 'session_recording_retention_days'", ) .get() as { value?: string } | undefined; diff --git a/src/backend/database/routes/snippets.ts b/src/backend/database/routes/snippets.ts index 6cfcaf73..2fbc4068 100644 --- a/src/backend/database/routes/snippets.ts +++ b/src/backend/database/routes/snippets.ts @@ -6,11 +6,13 @@ import { AuthManager } from "../../utils/auth-manager.js"; import { SSH_ALGORITHMS } from "../../utils/ssh-algorithms.js"; import { extractSnippetReorderUpdates } from "./snippets-reorder.js"; import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; -import { createCurrentHostResolutionRepository } from "../repositories/current-host-resolution-repository.js"; -import { createCurrentRbacAccessRepository } from "../repositories/current-rbac-access-repository.js"; -import { createCurrentRoleRepository } from "../repositories/current-role-repository.js"; -import { createCurrentSnippetRepository } from "../repositories/current-snippet-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentHostResolutionRepository, + createCurrentRbacAccessRepository, + createCurrentRoleRepository, + createCurrentSnippetRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; const router = express.Router(); diff --git a/src/backend/database/routes/sso-provider-routes.ts b/src/backend/database/routes/sso-provider-routes.ts index f06f77ad..fc29e095 100644 --- a/src/backend/database/routes/sso-provider-routes.ts +++ b/src/backend/database/routes/sso-provider-routes.ts @@ -6,7 +6,7 @@ import type { Router } from "express"; import { authLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import type { SSOProviderType } from "../../../types/index.js"; -import { createCurrentSsoProviderRepository } from "../repositories/current-sso-provider-repository.js"; +import { createCurrentSsoProviderRepository } from "../repositories/factory.js"; import { getOIDCConfigFromEnv } from "./user-oidc-utils.js"; const authManager = AuthManager.getInstance(); diff --git a/src/backend/database/routes/tailscale-routes.ts b/src/backend/database/routes/tailscale-routes.ts index 2052c988..924fc2b5 100644 --- a/src/backend/database/routes/tailscale-routes.ts +++ b/src/backend/database/routes/tailscale-routes.ts @@ -2,7 +2,7 @@ import { Router } from "express"; import type { RequestHandler, Router as ExpressRouter } from "express"; import { apiLogger } from "../../utils/logger.js"; import { getProxyAgent } from "../../utils/proxy-agent.js"; -import { createCurrentSettingsRepository } from "../repositories/current-settings-repository.js"; +import { createCurrentSettingsRepository } from "../repositories/factory.js"; interface TailscaleDevice { id: string; diff --git a/src/backend/database/routes/terminal.ts b/src/backend/database/routes/terminal.ts index 6bc3d16c..cdc3799b 100644 --- a/src/backend/database/routes/terminal.ts +++ b/src/backend/database/routes/terminal.ts @@ -3,9 +3,11 @@ import express from "express"; import type { Request, Response } from "express"; import { authLogger, databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; -import { createCurrentCommandHistoryRepository } from "../repositories/current-command-history-repository.js"; -import { createCurrentHostResolutionRepository } from "../repositories/current-host-resolution-repository.js"; -import { createCurrentSettingsRepository } from "../repositories/current-settings-repository.js"; +import { + createCurrentCommandHistoryRepository, + createCurrentHostResolutionRepository, + createCurrentSettingsRepository, +} from "../repositories/factory.js"; const router = express.Router(); diff --git a/src/backend/database/routes/termix-id.test.ts b/src/backend/database/routes/termix-id.test.ts index c1e0b3b1..eba98459 100644 --- a/src/backend/database/routes/termix-id.test.ts +++ b/src/backend/database/routes/termix-id.test.ts @@ -53,7 +53,7 @@ vi.mock("../../utils/user-crypto.js", () => ({ UserCrypto: { getInstance: () => ({ getUserKey: vi.fn() }) }, })); -vi.mock("../repositories/current-termix-identity-ca-repository.js", () => ({ +vi.mock("../repositories/factory.js", () => ({ createCurrentTermixIdentityCaRepository: vi.fn(() => ({ findPublicByIdentityId: vi.fn(), findDecryptedByIdentityId: vi.fn(), @@ -61,9 +61,6 @@ vi.mock("../repositories/current-termix-identity-ca-repository.js", () => ({ updateEncryptedForIdentity: vi.fn(), deleteByIdentityId: vi.fn(), })), -})); - -vi.mock("../repositories/current-termix-identity-repository.js", () => ({ createCurrentTermixIdentityRepository: vi.fn(() => ({ findIdentityForUser: vi.fn(), findIdentityByHandle: vi.fn(), diff --git a/src/backend/database/routes/termix-id.ts b/src/backend/database/routes/termix-id.ts index 1e4fd5cd..83b76789 100644 --- a/src/backend/database/routes/termix-id.ts +++ b/src/backend/database/routes/termix-id.ts @@ -1,10 +1,12 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import express from "express"; import type { Request, Response } from "express"; -import { createCurrentCredentialRepository } from "../repositories/current-credential-repository.js"; -import { createCurrentTermixIdentityRepository } from "../repositories/current-termix-identity-repository.js"; -import { createCurrentTermixIdentityCaRepository } from "../repositories/current-termix-identity-ca-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentCredentialRepository, + createCurrentTermixIdentityRepository, + createCurrentTermixIdentityCaRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; // ssh2 is CommonJS; Node's cjs-module-lexer does not surface its `utils` named // export, so we use a default import (esModuleInterop) and read `.utils` off it. import ssh2 from "ssh2"; diff --git a/src/backend/database/routes/user-admin-routes.ts b/src/backend/database/routes/user-admin-routes.ts index 8148ec21..437d6de3 100644 --- a/src/backend/database/routes/user-admin-routes.ts +++ b/src/backend/database/routes/user-admin-routes.ts @@ -6,8 +6,10 @@ import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; import bcrypt from "bcryptjs"; import { nanoid } from "nanoid"; import { AuthManager } from "../../utils/auth-manager.js"; -import { createCurrentRoleRepository } from "../repositories/current-role-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentRoleRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; import type { UserRecord, UserRepository, diff --git a/src/backend/database/routes/user-api-key-routes.ts b/src/backend/database/routes/user-api-key-routes.ts index c7a582cf..f09b3a2a 100644 --- a/src/backend/database/routes/user-api-key-routes.ts +++ b/src/backend/database/routes/user-api-key-routes.ts @@ -5,8 +5,10 @@ import bcrypt from "bcryptjs"; import { nanoid } from "nanoid"; import { authLogger } from "../../utils/logger.js"; import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; -import { createCurrentApiKeyRepository } from "../repositories/current-api-key-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentApiKeyRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; export function registerUserApiKeyRoutes( router: Router, diff --git a/src/backend/database/routes/user-oidc-account-routes.ts b/src/backend/database/routes/user-oidc-account-routes.ts index 5a1c42c6..24ca6b93 100644 --- a/src/backend/database/routes/user-oidc-account-routes.ts +++ b/src/backend/database/routes/user-oidc-account-routes.ts @@ -3,7 +3,7 @@ import type { RequestHandler, Router } from "express"; import { AuthManager } from "../../utils/auth-manager.js"; import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js"; import { authLogger } from "../../utils/logger.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { createCurrentUserRepository } from "../repositories/factory.js"; import { deleteUserAndRelatedData } from "./delete-user-data.js"; type UserOidcAccountRoutesDeps = { diff --git a/src/backend/database/routes/user-oidc-utils.ts b/src/backend/database/routes/user-oidc-utils.ts index 038b53ae..aaaf1bfb 100644 --- a/src/backend/database/routes/user-oidc-utils.ts +++ b/src/backend/database/routes/user-oidc-utils.ts @@ -5,8 +5,10 @@ import { Agent } from "undici"; import { eq } from "drizzle-orm"; import { getDb } from "../db/index.js"; import { ssoProviders } from "../db/schema.js"; -import { createCurrentSettingsRepository } from "../repositories/current-settings-repository.js"; -import { createCurrentSsoProviderRepository } from "../repositories/current-sso-provider-repository.js"; +import { + createCurrentSettingsRepository, + createCurrentSsoProviderRepository, +} from "../repositories/factory.js"; const BACKCHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout"; diff --git a/src/backend/database/routes/user-password-reset-routes.ts b/src/backend/database/routes/user-password-reset-routes.ts index fdbd7664..22924574 100644 --- a/src/backend/database/routes/user-password-reset-routes.ts +++ b/src/backend/database/routes/user-password-reset-routes.ts @@ -5,15 +5,17 @@ import { nanoid } from "nanoid"; import { AuthManager } from "../../utils/auth-manager.js"; import { authLogger } from "../../utils/logger.js"; import { loginRateLimiter } from "../../utils/login-rate-limiter.js"; -import { createCurrentCredentialRepository } from "../repositories/current-credential-repository.js"; -import { createCurrentDismissedAlertRepository } from "../repositories/current-dismissed-alert-repository.js"; -import { createCurrentFileManagerBookmarkRepository } from "../repositories/current-file-manager-bookmark-repository.js"; -import { createCurrentHostRepository } from "../repositories/current-host-repository.js"; -import { createCurrentRecentActivityRepository } from "../repositories/current-recent-activity-repository.js"; -import { createCurrentSettingsRepository } from "../repositories/current-settings-repository.js"; -import { createCurrentSnippetRepository } from "../repositories/current-snippet-repository.js"; -import { createCurrentSshCredentialUsageRepository } from "../repositories/current-ssh-credential-usage-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentCredentialRepository, + createCurrentDismissedAlertRepository, + createCurrentFileManagerBookmarkRepository, + createCurrentHostRepository, + createCurrentRecentActivityRepository, + createCurrentSettingsRepository, + createCurrentSnippetRepository, + createCurrentSshCredentialUsageRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; interface UserPasswordResetRoutesDeps { authManager: AuthManager; diff --git a/src/backend/database/routes/user-preferences.ts b/src/backend/database/routes/user-preferences.ts index 36a2a14a..87b5ae73 100644 --- a/src/backend/database/routes/user-preferences.ts +++ b/src/backend/database/routes/user-preferences.ts @@ -3,7 +3,7 @@ import express from "express"; import type { Request, Response } from "express"; import { databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; -import { createCurrentUserPreferenceRepository } from "../repositories/current-user-preference-repository.js"; +import { createCurrentUserPreferenceRepository } from "../repositories/factory.js"; import type { UserPreferenceRecord, UserPreferenceUpdate, diff --git a/src/backend/database/routes/user-session-routes.ts b/src/backend/database/routes/user-session-routes.ts index 2b9f9ab0..e832a8c4 100644 --- a/src/backend/database/routes/user-session-routes.ts +++ b/src/backend/database/routes/user-session-routes.ts @@ -3,8 +3,10 @@ import type { RequestHandler, Router } from "express"; import { AuthManager } from "../../utils/auth-manager.js"; import { authLogger } from "../../utils/logger.js"; import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; -import { createCurrentSessionRepository } from "../repositories/current-session-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentSessionRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; type UserSessionRoutesDeps = { authenticateJWT: RequestHandler; diff --git a/src/backend/database/routes/user-settings-routes.ts b/src/backend/database/routes/user-settings-routes.ts index 8c7f4d86..a3589563 100644 --- a/src/backend/database/routes/user-settings-routes.ts +++ b/src/backend/database/routes/user-settings-routes.ts @@ -7,8 +7,10 @@ import { setGlobalLogLevel, } from "../../utils/logger.js"; import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; -import { createCurrentSettingsRepository } from "../repositories/current-settings-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentSettingsRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; import type { UserRecord } from "../repositories/user-repository.js"; function getDefaultGuacUrl(): string { diff --git a/src/backend/database/routes/user-totp-routes.test.ts b/src/backend/database/routes/user-totp-routes.test.ts index 617a2272..ff4595d2 100644 --- a/src/backend/database/routes/user-totp-routes.test.ts +++ b/src/backend/database/routes/user-totp-routes.test.ts @@ -6,7 +6,7 @@ import speakeasy from "speakeasy"; // importing stays inert. const userRepositoryUpdate = vi.fn().mockResolvedValue(null); -vi.mock("../repositories/current-user-repository.js", () => ({ +vi.mock("../repositories/factory.js", () => ({ createCurrentUserRepository: () => ({ update: userRepositoryUpdate, }), diff --git a/src/backend/database/routes/user-totp-routes.ts b/src/backend/database/routes/user-totp-routes.ts index bae0dd2e..d5df38e0 100644 --- a/src/backend/database/routes/user-totp-routes.ts +++ b/src/backend/database/routes/user-totp-routes.ts @@ -13,10 +13,12 @@ import { generateDeviceFingerprint, parseUserAgent, } from "../../utils/user-agent-parser.js"; -import { createCurrentSessionRepository } from "../repositories/current-session-repository.js"; -import { createCurrentSettingsRepository } from "../repositories/current-settings-repository.js"; -import { createCurrentTrustedDeviceRepository } from "../repositories/current-trusted-device-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentSessionRepository, + createCurrentSettingsRepository, + createCurrentTrustedDeviceRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; import type { UserRecord } from "../repositories/user-repository.js"; type NativeAppRequestChecker = (req: Request) => boolean; diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index ae290897..59f202a4 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -43,9 +43,9 @@ import { logAudit, getRequestMeta } from "../../utils/audit-logger.js"; import { createCurrentSettingsRepository, getCurrentSettingValue, -} from "../repositories/current-settings-repository.js"; -import { createCurrentRoleRepository } from "../repositories/current-role-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; + createCurrentRoleRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; import type { UserRecord } from "../repositories/user-repository.js"; const authManager = AuthManager.getInstance(); diff --git a/src/backend/database/routes/vault.ts b/src/backend/database/routes/vault.ts index 9a43ad69..e4d22cd8 100644 --- a/src/backend/database/routes/vault.ts +++ b/src/backend/database/routes/vault.ts @@ -1,7 +1,9 @@ import express from "express"; import type { Request, Response } from "express"; -import { createCurrentVaultProfileRepository } from "../repositories/current-vault-profile-repository.js"; -import { createCurrentUserRepository } from "../repositories/current-user-repository.js"; +import { + createCurrentVaultProfileRepository, + createCurrentUserRepository, +} from "../repositories/factory.js"; import type { VaultProfileUpdateInput } from "../repositories/vault-profile-repository.js"; import type { AuthenticatedRequest } from "../../../types/index.js"; import { authLogger } from "../../utils/logger.js"; diff --git a/src/backend/database/runtime/adapter.ts b/src/backend/database/runtime/adapter.ts deleted file mode 100644 index 76198359..00000000 --- a/src/backend/database/runtime/adapter.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3"; -import type { Database as BetterSqliteDatabase } from "better-sqlite3"; -import type * as schema from "../db/schema.js"; -import type { DatabaseDialect, DatabaseRuntimeConfig } from "./config.js"; -import { SqliteDatabaseAdapter } from "./sqlite-adapter.js"; - -export interface DatabaseContext { - dialect: DatabaseDialect; - drizzle: BetterSQLite3Database; - sqlite?: BetterSqliteDatabase; -} - -export interface DatabaseAdapter { - readonly dialect: DatabaseDialect; - connect(): Promise; - migrate(): Promise; - transaction(fn: (tx: DatabaseContext) => T | Promise): Promise; - close(): Promise; -} - -export function createDatabaseAdapter( - config: DatabaseRuntimeConfig, -): DatabaseAdapter { - if (config.dialect === "sqlite") { - return new SqliteDatabaseAdapter(config); - } - - throw new Error( - `${config.dialect} adapter is not implemented yet. Use sqlite for the first runtime slice.`, - ); -} diff --git a/src/backend/database/runtime/config.test.ts b/src/backend/database/runtime/config.test.ts deleted file mode 100644 index e3718cf6..00000000 --- a/src/backend/database/runtime/config.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import path from "path"; -import { describe, expect, it } from "vitest"; -import { isExternalDatabase, parseDatabaseRuntimeConfig } from "./config.js"; - -describe("parseDatabaseRuntimeConfig", () => { - it("defaults to sqlite under DATA_DIR", () => { - const config = parseDatabaseRuntimeConfig({ DATA_DIR: "/app/data" }); - - const sqlitePath = path.join("/app/data", "termix.sqlite"); - expect(config).toEqual({ - dialect: "sqlite", - url: `file:${sqlitePath}`, - sqlitePath, - }); - }); - - it("accepts explicit sqlite file URLs", () => { - const config = parseDatabaseRuntimeConfig({ - DB_TYPE: "sqlite", - DATABASE_URL: "file:/tmp/termix.sqlite", - }); - - expect(config.sqlitePath).toBe("/tmp/termix.sqlite"); - }); - - it("normalizes postgres aliases", () => { - const config = parseDatabaseRuntimeConfig({ - DB_TYPE: "postgresql", - DATABASE_URL: "postgres://termix:pass@db:5432/termix", - }); - - expect(config.dialect).toBe("postgres"); - expect(isExternalDatabase(config.dialect)).toBe(true); - }); - - it("normalizes mariadb as mysql", () => { - const config = parseDatabaseRuntimeConfig({ - DB_TYPE: "mariadb", - DATABASE_URL: "mysql://termix:pass@db:3306/termix", - }); - - expect(config.dialect).toBe("mysql"); - expect(isExternalDatabase(config.dialect)).toBe(true); - }); - - it("requires DATABASE_URL for external databases", () => { - expect(() => parseDatabaseRuntimeConfig({ DB_TYPE: "postgres" })).toThrow( - "DATABASE_URL is required", - ); - }); - - it("rejects unsupported database types", () => { - expect(() => parseDatabaseRuntimeConfig({ DB_TYPE: "oracle" })).toThrow( - "Unsupported DB_TYPE", - ); - }); -}); diff --git a/src/backend/database/runtime/config.ts b/src/backend/database/runtime/config.ts deleted file mode 100644 index 25cc0465..00000000 --- a/src/backend/database/runtime/config.ts +++ /dev/null @@ -1,58 +0,0 @@ -import path from "path"; - -export type DatabaseDialect = "sqlite" | "postgres" | "mysql"; - -export interface DatabaseRuntimeConfig { - dialect: DatabaseDialect; - url: string; - sqlitePath?: string; -} - -type EnvLike = Record; - -function normalizeDialect(value: string | undefined): DatabaseDialect { - const raw = (value || "sqlite").trim().toLowerCase(); - if (raw === "sqlite") return "sqlite"; - if (raw === "postgres" || raw === "postgresql") return "postgres"; - if (raw === "mysql" || raw === "mariadb") return "mysql"; - throw new Error( - `Unsupported DB_TYPE '${value}'. Expected sqlite, postgres, or mysql.`, - ); -} - -function defaultSqliteUrl(env: EnvLike): string { - const dataDir = env.DATA_DIR || "./db/data"; - return `file:${path.join(dataDir, "termix.sqlite")}`; -} - -function sqlitePathFromUrl(url: string): string { - if (url === ":memory:") return url; - if (url.startsWith("file:")) return url.slice("file:".length); - return url; -} - -export function parseDatabaseRuntimeConfig( - env: EnvLike = process.env, -): DatabaseRuntimeConfig { - const dialect = normalizeDialect(env.DB_TYPE); - const url = - env.DATABASE_URL || (dialect === "sqlite" ? defaultSqliteUrl(env) : ""); - - if (!url) { - throw new Error(`DATABASE_URL is required when DB_TYPE is '${dialect}'.`); - } - - if (dialect === "sqlite") { - return { - dialect, - url, - sqlitePath: sqlitePathFromUrl(url), - }; - } - - return { dialect, url }; -} - -export function isExternalDatabase(dialect: DatabaseDialect): boolean { - return dialect === "postgres" || dialect === "mysql"; -} diff --git a/src/backend/database/runtime/sqlite-adapter.test.ts b/src/backend/database/runtime/sqlite-adapter.test.ts deleted file mode 100644 index a473e532..00000000 --- a/src/backend/database/runtime/sqlite-adapter.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { mkdtempSync, rmSync } from "fs"; -import os from "os"; -import path from "path"; -import { afterEach, describe, expect, it } from "vitest"; -import { SqliteDatabaseAdapter } from "./sqlite-adapter.js"; - -describe("SqliteDatabaseAdapter", () => { - let tempDir: string | null = null; - - afterEach(() => { - if (tempDir) { - rmSync(tempDir, { recursive: true, force: true }); - tempDir = null; - } - }); - - it("opens a persistent sqlite database and creates migration metadata tables", async () => { - tempDir = mkdtempSync(path.join(os.tmpdir(), "termix-sqlite-adapter-")); - const dbPath = path.join(tempDir, "termix.sqlite"); - const adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: `file:${dbPath}`, - sqlitePath: dbPath, - }); - - const context = await adapter.connect(); - await adapter.migrate(); - - const migrationTable = context.sqlite - ?.prepare( - "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", - ) - .get("schema_migrations") as { name: string } | undefined; - - expect(context.dialect).toBe("sqlite"); - expect(migrationTable?.name).toBe("schema_migrations"); - - await adapter.close(); - }); - - it("rejects async sqlite transaction callbacks for now", async () => { - const adapter = new SqliteDatabaseAdapter({ - dialect: "sqlite", - url: ":memory:", - sqlitePath: ":memory:", - }); - await adapter.connect(); - - await expect(adapter.transaction(async () => "done")).rejects.toThrow( - "must not return a Promise", - ); - - await adapter.close(); - }); -}); diff --git a/src/backend/database/runtime/sqlite-adapter.ts b/src/backend/database/runtime/sqlite-adapter.ts deleted file mode 100644 index be7e90d2..00000000 --- a/src/backend/database/runtime/sqlite-adapter.ts +++ /dev/null @@ -1,92 +0,0 @@ -import fs from "fs"; -import path from "path"; -import Database from "better-sqlite3"; -import { drizzle } from "drizzle-orm/better-sqlite3"; -import * as schema from "../db/schema.js"; -import type { DatabaseRuntimeConfig } from "./config.js"; -import type { DatabaseAdapter, DatabaseContext } from "./adapter.js"; - -export class SqliteDatabaseAdapter implements DatabaseAdapter { - readonly dialect = "sqlite" as const; - private sqlite: Database.Database | null = null; - private context: DatabaseContext | null = null; - - constructor(private readonly config: DatabaseRuntimeConfig) { - if (config.dialect !== "sqlite") { - throw new Error("SqliteDatabaseAdapter requires sqlite config."); - } - } - - async connect(): Promise { - if (this.context) return this.context; - - const sqlitePath = this.config.sqlitePath || ":memory:"; - if (sqlitePath !== ":memory:") { - fs.mkdirSync(path.dirname(sqlitePath), { recursive: true }); - } - - this.sqlite = new Database(sqlitePath); - this.sqlite.exec("PRAGMA foreign_keys = ON"); - if (sqlitePath !== ":memory:") { - this.sqlite.exec("PRAGMA journal_mode = WAL"); - } - - this.context = { - dialect: this.dialect, - drizzle: drizzle(this.sqlite, { schema }), - sqlite: this.sqlite, - }; - - return this.context; - } - - async migrate(): Promise { - if (!this.sqlite) { - throw new Error("SQLite adapter must be connected before migration."); - } - - this.sqlite.exec(` - CREATE TABLE IF NOT EXISTS schema_migrations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - version TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - checksum TEXT, - applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE IF NOT EXISTS system_migrations ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - `); - } - - async transaction( - fn: (tx: DatabaseContext) => T | Promise, - ): Promise { - const context = await this.connect(); - if (!this.sqlite) { - throw new Error("SQLite adapter is not connected."); - } - - const run = this.sqlite.transaction(() => { - const result = fn(context); - if (result instanceof Promise) { - throw new Error( - "SQLite adapter transactions must not return a Promise until async transaction support is introduced.", - ); - } - return result; - }); - return run(); - } - - async close(): Promise { - if (this.sqlite) { - this.sqlite.close(); - this.sqlite = null; - this.context = null; - } - } -} diff --git a/src/backend/database/runtime/sqlite-foreign-key-boundary.test.ts b/src/backend/database/runtime/sqlite-foreign-key-boundary.test.ts deleted file mode 100644 index 95063cf4..00000000 --- a/src/backend/database/runtime/sqlite-foreign-key-boundary.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { withSqliteForeignKeysDisabled } from "./sqlite-foreign-key-boundary.js"; - -describe("withSqliteForeignKeysDisabled", () => { - it("disables foreign keys for an operation and restores them afterwards", async () => { - const exec = vi.fn(); - - await expect( - withSqliteForeignKeysDisabled({ exec }, async () => "done"), - ).resolves.toBe("done"); - - expect(exec).toHaveBeenNthCalledWith(1, "PRAGMA foreign_keys = OFF"); - expect(exec).toHaveBeenNthCalledWith(2, "PRAGMA foreign_keys = ON"); - }); - - it("restores foreign keys when the operation fails", async () => { - const exec = vi.fn(); - - await expect( - withSqliteForeignKeysDisabled({ exec }, async () => { - throw new Error("boom"); - }), - ).rejects.toThrow("boom"); - - expect(exec).toHaveBeenNthCalledWith(1, "PRAGMA foreign_keys = OFF"); - expect(exec).toHaveBeenNthCalledWith(2, "PRAGMA foreign_keys = ON"); - }); -}); diff --git a/src/backend/guacamole/guacamole-server.ts b/src/backend/guacamole/guacamole-server.ts index 7aa4536a..27ca03e5 100644 --- a/src/backend/guacamole/guacamole-server.ts +++ b/src/backend/guacamole/guacamole-server.ts @@ -1,7 +1,7 @@ import GuacamoleLite from "guacamole-lite"; import { guacLogger } from "../utils/logger.js"; import { GuacamoleTokenService } from "./token-service.js"; -import { getCurrentSettingValue } from "../database/repositories/current-settings-repository.js"; +import { getCurrentSettingValue } from "../database/repositories/factory.js"; import { resolveGuacdOptions } from "../utils/guacd-config.js"; import fs from "fs"; import path from "path"; diff --git a/src/backend/guacamole/routes.ts b/src/backend/guacamole/routes.ts index 4d5a0570..77d55121 100644 --- a/src/backend/guacamole/routes.ts +++ b/src/backend/guacamole/routes.ts @@ -8,8 +8,10 @@ import net from "net"; import crypto from "crypto"; import path from "path"; import type { AuthenticatedRequest } from "../../types/index.js"; -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; -import { createCurrentSettingsRepository } from "../database/repositories/current-settings-repository.js"; +import { + createCurrentHostResolutionRepository, + createCurrentSettingsRepository, +} from "../database/repositories/factory.js"; import { resolveGuacdOptions } from "../utils/guacd-config.js"; const router = express.Router(); diff --git a/src/backend/ssh/alert-engine.ts b/src/backend/ssh/alert-engine.ts index 16d57040..e5514598 100644 --- a/src/backend/ssh/alert-engine.ts +++ b/src/backend/ssh/alert-engine.ts @@ -1,4 +1,4 @@ -import { createCurrentAlertRepository } from "../database/repositories/current-alert-repository.js"; +import { createCurrentAlertRepository } from "../database/repositories/factory.js"; import { statsLogger } from "../utils/logger.js"; import { sendNotification, diff --git a/src/backend/ssh/auth-manager.ts b/src/backend/ssh/auth-manager.ts index 062c765c..9aceab71 100644 --- a/src/backend/ssh/auth-manager.ts +++ b/src/backend/ssh/auth-manager.ts @@ -1,6 +1,6 @@ import type { WebSocket } from "ws"; import { sshLogger, authLogger } from "../utils/logger.js"; -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; +import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; interface ResolvedCredentials { username: string; password?: string; diff --git a/src/backend/ssh/credential-username.test.ts b/src/backend/ssh/credential-username.test.ts index c60d9869..161519bc 100644 --- a/src/backend/ssh/credential-username.test.ts +++ b/src/backend/ssh/credential-username.test.ts @@ -63,7 +63,7 @@ describe("expandOidcUsername", () => { }); it("expands the placeholder with the user's OIDC identifier", async () => { - vi.doMock("../database/repositories/current-user-repository.js", () => ({ + vi.doMock("../database/repositories/factory.js", () => ({ createCurrentUserRepository: () => ({ findById: async () => ({ oidcIdentifier: "jdoe" }), }), @@ -75,7 +75,7 @@ describe("expandOidcUsername", () => { }); it("leaves the placeholder as-is when the user has no OIDC identifier", async () => { - vi.doMock("../database/repositories/current-user-repository.js", () => ({ + vi.doMock("../database/repositories/factory.js", () => ({ createCurrentUserRepository: () => ({ findById: async () => ({ oidcIdentifier: null }), }), @@ -89,7 +89,7 @@ describe("expandOidcUsername", () => { }); it("returns the username unchanged when the DB lookup throws", async () => { - vi.doMock("../database/repositories/current-user-repository.js", () => ({ + vi.doMock("../database/repositories/factory.js", () => ({ createCurrentUserRepository: () => { throw new Error("DB unavailable"); }, diff --git a/src/backend/ssh/credential-username.ts b/src/backend/ssh/credential-username.ts index e8ac661d..d3ca4ed7 100644 --- a/src/backend/ssh/credential-username.ts +++ b/src/backend/ssh/credential-username.ts @@ -49,7 +49,7 @@ export async function expandOidcUsername( try { const { createCurrentUserRepository } = - await import("../database/repositories/current-user-repository.js"); + await import("../database/repositories/factory.js"); const user = await createCurrentUserRepository().findById(userId); const oidcIdentifier = user?.oidcIdentifier; if (!oidcIdentifier) return username; diff --git a/src/backend/ssh/docker-console.ts b/src/backend/ssh/docker-console.ts index 7a619680..bd1b7cd3 100644 --- a/src/backend/ssh/docker-console.ts +++ b/src/backend/ssh/docker-console.ts @@ -2,7 +2,7 @@ import { Client as SSHClient } from "ssh2"; import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; import { WebSocketServer, WebSocket } from "ws"; import { AuthManager } from "../utils/auth-manager.js"; -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; +import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; import { systemLogger } from "../utils/logger.js"; import type { SSHHost } from "../../types/index.js"; import { applyAgentAuth } from "./terminal-auth-helpers.js"; diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index b9929f5f..f6f3cd10 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -4,7 +4,7 @@ import cookieParser from "cookie-parser"; import axios from "axios"; import { Client as SSHClient } from "ssh2"; import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; +import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; import { fileLogger } from "../utils/logger.js"; import { AuthManager } from "../utils/auth-manager.js"; import type { AuthenticatedRequest, ProxyNode } from "../../types/index.js"; diff --git a/src/backend/ssh/host-key-verifier.ts b/src/backend/ssh/host-key-verifier.ts index 7eb64443..111021ac 100644 --- a/src/backend/ssh/host-key-verifier.ts +++ b/src/backend/ssh/host-key-verifier.ts @@ -1,5 +1,5 @@ import type { WebSocket } from "ws"; -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; +import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; import { sshLogger } from "../utils/logger.js"; interface HostKeyVerificationData { diff --git a/src/backend/ssh/host-metrics-history-routes.ts b/src/backend/ssh/host-metrics-history-routes.ts index 5a014afc..8e8c1dcd 100644 --- a/src/backend/ssh/host-metrics-history-routes.ts +++ b/src/backend/ssh/host-metrics-history-routes.ts @@ -1,6 +1,6 @@ import type { Express, RequestHandler } from "express"; import type { AuthenticatedRequest } from "../../types/index.js"; -import { createCurrentHostMetricsHistoryRepository } from "../database/repositories/current-host-metrics-history-repository.js"; +import { createCurrentHostMetricsHistoryRepository } from "../database/repositories/factory.js"; import { statsLogger } from "../utils/logger.js"; type HistoryRoutesDeps = { diff --git a/src/backend/ssh/host-metrics-jump-hosts.ts b/src/backend/ssh/host-metrics-jump-hosts.ts index 700fa7d2..d223f433 100644 --- a/src/backend/ssh/host-metrics-jump-hosts.ts +++ b/src/backend/ssh/host-metrics-jump-hosts.ts @@ -5,7 +5,7 @@ import { createSocks5Connection, type SOCKS5Config, } from "../utils/socks5-helper.js"; -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; +import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; import { SSHHostKeyVerifier } from "./host-key-verifier.js"; import { getJumpHostSocks5Config } from "./jump-host-proxy.js"; import { applyAgentAuth } from "./terminal-auth-helpers.js"; diff --git a/src/backend/ssh/host-metrics-preferences-routes.ts b/src/backend/ssh/host-metrics-preferences-routes.ts index 16a0e054..c3210c8d 100644 --- a/src/backend/ssh/host-metrics-preferences-routes.ts +++ b/src/backend/ssh/host-metrics-preferences-routes.ts @@ -1,6 +1,6 @@ import type { Express, RequestHandler } from "express"; import type { AuthenticatedRequest } from "../../types/index.js"; -import { createCurrentHostMetricsPreferenceRepository } from "../database/repositories/current-host-metrics-preference-repository.js"; +import { createCurrentHostMetricsPreferenceRepository } from "../database/repositories/factory.js"; import { statsLogger } from "../utils/logger.js"; import { deriveEnabledWidgets, diff --git a/src/backend/ssh/host-metrics-settings-routes.ts b/src/backend/ssh/host-metrics-settings-routes.ts index ad5a789e..5540b4d2 100644 --- a/src/backend/ssh/host-metrics-settings-routes.ts +++ b/src/backend/ssh/host-metrics-settings-routes.ts @@ -1,6 +1,6 @@ import type { Express, RequestHandler } from "express"; import { statsLogger } from "../utils/logger.js"; -import { createCurrentSettingsRepository } from "../database/repositories/current-settings-repository.js"; +import { createCurrentSettingsRepository } from "../database/repositories/factory.js"; type HostMetricsSettingsConfig = { statusCheckInterval: number; diff --git a/src/backend/ssh/host-metrics.ts b/src/backend/ssh/host-metrics.ts index 967ca427..98bd26ea 100644 --- a/src/backend/ssh/host-metrics.ts +++ b/src/backend/ssh/host-metrics.ts @@ -8,9 +8,11 @@ import { pickResolvedPassword, pickResolvedUsername, } from "./credential-username.js"; -import { getCurrentSettingValue } from "../database/repositories/current-settings-repository.js"; -import { createCurrentHostMetricsHistoryRepository } from "../database/repositories/current-host-metrics-history-repository.js"; -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; +import { + getCurrentSettingValue, + createCurrentHostMetricsHistoryRepository, + createCurrentHostResolutionRepository, +} from "../database/repositories/factory.js"; import { statsLogger } from "../utils/logger.js"; import { DataCrypto } from "../utils/data-crypto.js"; import { AuthManager } from "../utils/auth-manager.js"; diff --git a/src/backend/ssh/host-resolver.ts b/src/backend/ssh/host-resolver.ts index 05b40806..df1ede58 100644 --- a/src/backend/ssh/host-resolver.ts +++ b/src/backend/ssh/host-resolver.ts @@ -1,5 +1,7 @@ -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; -import { createCurrentVaultProfileRepository } from "../database/repositories/current-vault-profile-repository.js"; +import { + createCurrentHostResolutionRepository, + createCurrentVaultProfileRepository, +} from "../database/repositories/factory.js"; import { logger } from "../utils/logger.js"; import { pickResolvedPassword, diff --git a/src/backend/ssh/jump-host-chain.ts b/src/backend/ssh/jump-host-chain.ts index 873ffeb5..c76d86f6 100644 --- a/src/backend/ssh/jump-host-chain.ts +++ b/src/backend/ssh/jump-host-chain.ts @@ -1,5 +1,5 @@ import { Client as SSHClient } from "ssh2"; -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; +import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; import { fileLogger } from "../utils/logger.js"; import { createSocks5Connection, diff --git a/src/backend/ssh/managers/health.ts b/src/backend/ssh/managers/health.ts index 2aae89fd..eb2a53b5 100644 --- a/src/backend/ssh/managers/health.ts +++ b/src/backend/ssh/managers/health.ts @@ -2,7 +2,7 @@ import type { Express } from "express"; import type { Client } from "ssh2"; import type { AuthenticatedRequest } from "../../../types/index.js"; import { execCommand } from "../widgets/common-utils.js"; -import { createCurrentHostHealthRepository } from "../../database/repositories/current-host-health-repository.js"; +import { createCurrentHostHealthRepository } from "../../database/repositories/factory.js"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; import { shellSingleQuote } from "./exec-elevated.js"; import { isValidPort } from "./validation.js"; diff --git a/src/backend/ssh/opkssh-auth.ts b/src/backend/ssh/opkssh-auth.ts index c9c8829a..2dbc14ec 100644 --- a/src/backend/ssh/opkssh-auth.ts +++ b/src/backend/ssh/opkssh-auth.ts @@ -3,7 +3,7 @@ import { randomUUID } from "crypto"; import { WebSocket } from "ws"; import { OPKSSHBinaryManager } from "../utils/opkssh-binary-manager.js"; import { sshLogger } from "../utils/logger.js"; -import { createCurrentOpksshTokenRepository } from "../database/repositories/current-opkssh-token-repository.js"; +import { createCurrentOpksshTokenRepository } from "../database/repositories/factory.js"; import { UserCrypto } from "../utils/user-crypto.js"; import { FieldCrypto } from "../utils/field-crypto.js"; import { promises as fs } from "fs"; diff --git a/src/backend/ssh/terminal-jump-hosts.ts b/src/backend/ssh/terminal-jump-hosts.ts index 66434a96..6acc9f96 100644 --- a/src/backend/ssh/terminal-jump-hosts.ts +++ b/src/backend/ssh/terminal-jump-hosts.ts @@ -1,5 +1,5 @@ import ssh2Pkg, { type Client as SSHClientType } from "ssh2"; -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; +import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; import { sshLogger } from "../utils/logger.js"; import { diff --git a/src/backend/ssh/terminal-session-manager.test.ts b/src/backend/ssh/terminal-session-manager.test.ts index 2cbaa72e..b6d71458 100644 --- a/src/backend/ssh/terminal-session-manager.test.ts +++ b/src/backend/ssh/terminal-session-manager.test.ts @@ -8,15 +8,12 @@ vi.mock("../database/db/index.js", () => ({ getDb: () => ({}), })); -vi.mock( - "../database/repositories/current-session-recording-repository.js", - () => ({ - createCurrentSessionRecordingRepository: () => ({ - create: mockCreate, - updateEnded: mockUpdateEnded, - }), +vi.mock("../database/repositories/factory.js", () => ({ + createCurrentSessionRecordingRepository: () => ({ + create: mockCreate, + updateEnded: mockUpdateEnded, }), -); +})); vi.mock("../utils/logger.js", () => ({ sshLogger: { diff --git a/src/backend/ssh/terminal-session-manager.ts b/src/backend/ssh/terminal-session-manager.ts index 25af32c1..1d83de9f 100644 --- a/src/backend/ssh/terminal-session-manager.ts +++ b/src/backend/ssh/terminal-session-manager.ts @@ -2,10 +2,11 @@ import { type Client, type ClientChannel } from "ssh2"; import { WebSocket } from "ws"; import fs from "fs"; import path from "path"; -import { eq } from "drizzle-orm"; import { sshLogger } from "../utils/logger.js"; -import { getCurrentSettingValue } from "../database/repositories/current-settings-repository.js"; -import { createCurrentSessionRecordingRepository } from "../database/repositories/current-session-recording-repository.js"; +import { + getCurrentSettingValue, + createCurrentSessionRecordingRepository, +} from "../database/repositories/factory.js"; const MAX_BUFFER_BYTES = 512 * 1024; const DATA_DIR = process.env.DATA_DIR ?? "./db/data"; diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index 07a9cf5b..8c996f7c 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -7,7 +7,7 @@ import ssh2Pkg, { const { Client, utils: ssh2Utils } = ssh2Pkg; import { buildSSHAlgorithms } from "../utils/ssh-algorithms.js"; import axios from "axios"; -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; +import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; import { sshLogger, authLogger } from "../utils/logger.js"; import { logAudit } from "../utils/audit-logger.js"; import { AuthManager } from "../utils/auth-manager.js"; diff --git a/src/backend/ssh/tmux-monitor.ts b/src/backend/ssh/tmux-monitor.ts index d65bf2d3..aca506d7 100644 --- a/src/backend/ssh/tmux-monitor.ts +++ b/src/backend/ssh/tmux-monitor.ts @@ -4,8 +4,10 @@ import { Client, type ConnectConfig } from "ssh2"; import { createCorsMiddleware } from "../utils/cors-config.js"; import { AuthManager } from "../utils/auth-manager.js"; import { DataCrypto } from "../utils/data-crypto.js"; -import { createCurrentTmuxSessionTagRepository } from "../database/repositories/current-tmux-session-tag-repository.js"; -import { createCurrentUserRepository } from "../database/repositories/current-user-repository.js"; +import { + createCurrentTmuxSessionTagRepository, + createCurrentUserRepository, +} from "../database/repositories/factory.js"; import { logAudit, getRequestMeta } from "../utils/audit-logger.js"; import { sshLogger } from "../utils/logger.js"; import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts index 528143ff..2529062a 100644 --- a/src/backend/ssh/tunnel.ts +++ b/src/backend/ssh/tunnel.ts @@ -12,7 +12,7 @@ import { WebSocketServer } from "ws"; import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; import { ChildProcess } from "child_process"; import axios from "axios"; -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; +import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; import type { SSHHost, TunnelConfig, diff --git a/src/backend/ssh/vault-oidc-auth.ts b/src/backend/ssh/vault-oidc-auth.ts index 95e6241e..ce10c385 100644 --- a/src/backend/ssh/vault-oidc-auth.ts +++ b/src/backend/ssh/vault-oidc-auth.ts @@ -11,8 +11,10 @@ // ephemeral key, cache the cert, and notify the browser to reconnect. import { WebSocket } from "ws"; -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; -import { createCurrentVaultProfileRepository } from "../database/repositories/current-vault-profile-repository.js"; +import { + createCurrentHostResolutionRepository, + createCurrentVaultProfileRepository, +} from "../database/repositories/factory.js"; import { sshLogger } from "../utils/logger.js"; import { type VaultProfileConfig, diff --git a/src/backend/ssh/vault-signer-auth.ts b/src/backend/ssh/vault-signer-auth.ts index b9b111c7..3db47614 100644 --- a/src/backend/ssh/vault-signer-auth.ts +++ b/src/backend/ssh/vault-signer-auth.ts @@ -16,7 +16,7 @@ // The pure (DB-free) signing/OIDC/cert logic lives in vault-signer-core.ts and // is re-exported here so callers have a single import surface. -import { createCurrentVaultTokenRepository } from "../database/repositories/current-vault-token-repository.js"; +import { createCurrentVaultTokenRepository } from "../database/repositories/factory.js"; import { UserCrypto } from "../utils/user-crypto.js"; import { FieldCrypto } from "../utils/field-crypto.js"; import { parseCertValidBefore } from "./vault-signer-core.js"; diff --git a/src/backend/starter.ts b/src/backend/starter.ts index 5fe654c5..15e502ba 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -97,10 +97,6 @@ import { version: version, }); - const { logRepositoryRolloutConfig } = - await import("./database/repositories/repository-rollout.js"); - logRepositoryRolloutConfig(); - const systemCrypto = SystemCrypto.getInstance(); await systemCrypto.initializeJWTSecret(); await systemCrypto.initializeDatabaseKey(); @@ -160,7 +156,7 @@ import { // Initialize log level from database settings const { getCurrentSettingValue } = - await import("./database/repositories/current-settings-repository.js"); + await import("./database/repositories/factory.js"); const logLevel = getCurrentSettingValue("log_level"); if (logLevel) { setGlobalLogLevel(logLevel); diff --git a/src/backend/utils/audit-logger.test.ts b/src/backend/utils/audit-logger.test.ts index 21c78863..d6f3543e 100644 --- a/src/backend/utils/audit-logger.test.ts +++ b/src/backend/utils/audit-logger.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; const createMock = vi.fn().mockResolvedValue(undefined); -vi.mock("../database/repositories/current-audit-log-repository.js", () => ({ +vi.mock("../database/repositories/factory.js", () => ({ createCurrentAuditLogRepository: vi.fn(() => ({ create: createMock, })), diff --git a/src/backend/utils/audit-logger.ts b/src/backend/utils/audit-logger.ts index c78d468f..c48c0380 100644 --- a/src/backend/utils/audit-logger.ts +++ b/src/backend/utils/audit-logger.ts @@ -1,5 +1,5 @@ import type { Request } from "express"; -import { createCurrentAuditLogRepository } from "../database/repositories/current-audit-log-repository.js"; +import { createCurrentAuditLogRepository } from "../database/repositories/factory.js"; export interface AuditLogParams { userId: string; diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts index 7a0f6df6..98cf3d45 100644 --- a/src/backend/utils/auth-manager.ts +++ b/src/backend/utils/auth-manager.ts @@ -12,11 +12,13 @@ import { and, eq, inArray } from "drizzle-orm"; import type { DeviceType } from "./user-agent-parser.js"; import { getDb } from "../database/db/index.js"; import { sessions } from "../database/db/schema.js"; -import { createCurrentSettingsRepository } from "../database/repositories/current-settings-repository.js"; -import { createCurrentSessionRepository } from "../database/repositories/current-session-repository.js"; -import { createCurrentUserRepository } from "../database/repositories/current-user-repository.js"; -import { createCurrentApiKeyRepository } from "../database/repositories/current-api-key-repository.js"; -import { createCurrentTrustedDeviceRepository } from "../database/repositories/current-trusted-device-repository.js"; +import { + createCurrentSettingsRepository, + createCurrentSessionRepository, + createCurrentUserRepository, + createCurrentApiKeyRepository, + createCurrentTrustedDeviceRepository, +} from "../database/repositories/factory.js"; interface AuthenticationResult { success: boolean; diff --git a/src/backend/utils/credential-system-encryption-migration.test.ts b/src/backend/utils/credential-system-encryption-migration.test.ts index e35dd03b..7ae51971 100644 --- a/src/backend/utils/credential-system-encryption-migration.test.ts +++ b/src/backend/utils/credential-system-encryption-migration.test.ts @@ -13,19 +13,13 @@ const sharedCredentialRepository = { markNeedsReEncryptionByOriginalCredentialId: vi.fn(), }; -vi.mock("../database/repositories/current-credential-repository.js", () => ({ +vi.mock("../database/repositories/factory.js", () => ({ createCurrentCredentialRepository: vi.fn(() => credentialRepository), + createCurrentSharedCredentialRepository: vi.fn( + () => sharedCredentialRepository, + ), })); -vi.mock( - "../database/repositories/current-shared-credential-repository.js", - () => ({ - createCurrentSharedCredentialRepository: vi.fn( - () => sharedCredentialRepository, - ), - }), -); - describe("CredentialSystemEncryptionMigration", () => { beforeEach(() => { vi.restoreAllMocks(); diff --git a/src/backend/utils/credential-system-encryption-migration.ts b/src/backend/utils/credential-system-encryption-migration.ts index 7443c322..63803120 100644 --- a/src/backend/utils/credential-system-encryption-migration.ts +++ b/src/backend/utils/credential-system-encryption-migration.ts @@ -1,5 +1,7 @@ -import { createCurrentCredentialRepository } from "../database/repositories/current-credential-repository.js"; -import { createCurrentSharedCredentialRepository } from "../database/repositories/current-shared-credential-repository.js"; +import { + createCurrentCredentialRepository, + createCurrentSharedCredentialRepository, +} from "../database/repositories/factory.js"; import { DataCrypto } from "./data-crypto.js"; import { SystemCrypto } from "./system-crypto.js"; import { FieldCrypto } from "./field-crypto.js"; diff --git a/src/backend/utils/database-layer-preupgrade-backup.test.ts b/src/backend/utils/database-layer-preupgrade-backup.test.ts index bb96ddb7..d7c6ba84 100644 --- a/src/backend/utils/database-layer-preupgrade-backup.test.ts +++ b/src/backend/utils/database-layer-preupgrade-backup.test.ts @@ -37,9 +37,7 @@ describe("ensureDatabaseLayerPreupgradeBackup", () => { dataDir, version: "2.5.0-test", now: new Date("2026-06-27T12:00:00.000Z"), - env: { - DATABASE_LAYER_REPOSITORY_ROLLOUT: "settings,users", - } as NodeJS.ProcessEnv, + env: {} as NodeJS.ProcessEnv, }); expect(result.status).toBe("created"); @@ -67,9 +65,8 @@ describe("ensureDatabaseLayerPreupgradeBackup", () => { const manifest = JSON.parse( fs.readFileSync(path.join(result.backupDir!, "manifest.json"), "utf8"), - ) as { sourceVersion: string; rollout: { mode: string } }; + ) as { sourceVersion: string }; expect(manifest.sourceVersion).toBe("2.5.0-test"); - expect(manifest.rollout.mode).toBe("partial"); }); it("skips when the marker already exists", () => { diff --git a/src/backend/utils/database-layer-preupgrade-backup.ts b/src/backend/utils/database-layer-preupgrade-backup.ts index 25299b51..e59b5624 100644 --- a/src/backend/utils/database-layer-preupgrade-backup.ts +++ b/src/backend/utils/database-layer-preupgrade-backup.ts @@ -1,7 +1,6 @@ import fs from "fs"; import path from "path"; import { databaseLogger } from "./logger.js"; -import { getRepositoryRolloutStatus } from "../database/repositories/repository-rollout.js"; export const DATABASE_LAYER_PREUPGRADE_BACKUP_MARKER = ".database-layer-preupgrade-backup.json"; @@ -31,7 +30,6 @@ export interface PreupgradeBackupManifest { sourceVersion: string; dataDir: string; backupDir: string; - rollout: ReturnType; files: BackupFileEntry[]; } @@ -159,7 +157,6 @@ export function ensureDatabaseLayerPreupgradeBackup( sourceVersion: options.version ?? env.VERSION ?? "unknown", dataDir, backupDir, - rollout: getRepositoryRolloutStatus(env), files, }; diff --git a/src/backend/utils/permission-manager.ts b/src/backend/utils/permission-manager.ts index 77090514..93b0370d 100644 --- a/src/backend/utils/permission-manager.ts +++ b/src/backend/utils/permission-manager.ts @@ -1,8 +1,10 @@ import type { Request, Response, NextFunction } from "express"; -import { createCurrentRbacAccessRepository } from "../database/repositories/current-rbac-access-repository.js"; -import { createCurrentRoleRepository } from "../database/repositories/current-role-repository.js"; -import { createCurrentUserRepository } from "../database/repositories/current-user-repository.js"; -import { createCurrentHostResolutionRepository } from "../database/repositories/current-host-resolution-repository.js"; +import { + createCurrentRbacAccessRepository, + createCurrentRoleRepository, + createCurrentUserRepository, + createCurrentHostResolutionRepository, +} from "../database/repositories/factory.js"; import { databaseLogger } from "./logger.js"; interface AuthenticatedRequest extends Request { diff --git a/src/backend/utils/shared-credential-manager.ts b/src/backend/utils/shared-credential-manager.ts index 978f178f..23b3e094 100644 --- a/src/backend/utils/shared-credential-manager.ts +++ b/src/backend/utils/shared-credential-manager.ts @@ -1,7 +1,9 @@ -import { createCurrentCredentialRepository } from "../database/repositories/current-credential-repository.js"; -import { createCurrentRbacAccessRepository } from "../database/repositories/current-rbac-access-repository.js"; -import { createCurrentRoleRepository } from "../database/repositories/current-role-repository.js"; -import { createCurrentSharedCredentialRepository } from "../database/repositories/current-shared-credential-repository.js"; +import { + createCurrentCredentialRepository, + createCurrentRbacAccessRepository, + createCurrentRoleRepository, + createCurrentSharedCredentialRepository, +} from "../database/repositories/factory.js"; import type { SharedCredentialRecord } from "../database/repositories/shared-credential-repository.js"; import { DataCrypto } from "./data-crypto.js"; import { FieldCrypto } from "./field-crypto.js"; diff --git a/src/backend/utils/user-crypto.ts b/src/backend/utils/user-crypto.ts index 5c106f39..b068346d 100644 --- a/src/backend/utils/user-crypto.ts +++ b/src/backend/utils/user-crypto.ts @@ -1,6 +1,6 @@ import crypto from "crypto"; import { databaseLogger } from "./logger.js"; -import { createCurrentSettingsRepository } from "../database/repositories/current-settings-repository.js"; +import { createCurrentSettingsRepository } from "../database/repositories/factory.js"; import { SystemCrypto } from "./system-crypto.js"; interface KEKSalt { diff --git a/src/backend/utils/user-data-export.ts b/src/backend/utils/user-data-export.ts index 1419cdac..42c39c79 100644 --- a/src/backend/utils/user-data-export.ts +++ b/src/backend/utils/user-data-export.ts @@ -1,8 +1,10 @@ -import { createCurrentDismissedAlertRepository } from "../database/repositories/current-dismissed-alert-repository.js"; -import { createCurrentFileManagerBookmarkRepository } from "../database/repositories/current-file-manager-bookmark-repository.js"; -import { createCurrentTransferRecentRepository } from "../database/repositories/current-transfer-recent-repository.js"; -import { createCurrentUserDataExportRepository } from "../database/repositories/current-user-data-export-repository.js"; -import { createCurrentUserRepository } from "../database/repositories/current-user-repository.js"; +import { + createCurrentDismissedAlertRepository, + createCurrentFileManagerBookmarkRepository, + createCurrentTransferRecentRepository, + createCurrentUserDataExportRepository, + createCurrentUserRepository, +} from "../database/repositories/factory.js"; import { DataCrypto } from "./data-crypto.js"; import { databaseLogger } from "./logger.js"; diff --git a/src/backend/utils/user-encryption-migration-store.ts b/src/backend/utils/user-encryption-migration-store.ts index 03c17af8..112eb326 100644 --- a/src/backend/utils/user-encryption-migration-store.ts +++ b/src/backend/utils/user-encryption-migration-store.ts @@ -1,4 +1,4 @@ -import { getCurrentRepositorySqlite } from "../database/repositories/current-repository-runtime.js"; +import { getCurrentRepositorySqlite } from "../database/repositories/factory.js"; export interface UserEncryptionMigrationRecord { id: number | string;