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.
This commit is contained in:
LukeGus
2026-07-15 23:56:02 -05:00
parent 1e7c9ea53c
commit ef4969dd35
217 changed files with 843 additions and 4297 deletions
+6 -4
View File
@@ -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();
+10 -10
View File
@@ -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,
@@ -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<void>,
): Promise<AlertRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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 (
@@ -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;
@@ -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<void>,
): Promise<AuditLogRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<void>,
): Promise<C2sTunnelPresetRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<void>,
): Promise<CommandHistoryRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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";
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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",
),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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<void> {
return () => DatabaseSaveTrigger.forceSave(reason);
}
export function getCurrentRepositorySqlite() {
return getSqlite();
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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;
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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());
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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"),
);
}
@@ -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<void>,
): Promise<DashboardServiceLinkRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<typeof schema>;
sqlite?: BetterSqliteDatabase;
}
@@ -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<void>,
): Promise<DismissedAlertRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<void> {
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"),
);
}
@@ -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<void>,
): Promise<FileManagerBookmarkRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<void>,
): Promise<HomepageItemRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<void>,
): Promise<HomepageLayoutRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<ReturnType<SqliteDatabaseAdapter["connect"]>>["sqlite"]
Awaited<ReturnType<TestSqliteDatabase["connect"]>>["sqlite"]
>;
}> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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<ReturnType<SqliteDatabaseAdapter["connect"]>>["sqlite"]
Awaited<ReturnType<TestSqliteDatabase["connect"]>>["sqlite"]
>;
}> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<void>,
): Promise<HostHealthRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<void>,
): Promise<HostMetricsHistoryRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE hosts (
@@ -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;
@@ -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<void>,
): Promise<HostMetricsPreferenceRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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;
@@ -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<void>,
): Promise<HostResolutionRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<void>,
): Promise<NetworkTopologyRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<void>,
): Promise<OpenTabRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<void>,
): Promise<OpksshTokenRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<void>,
): Promise<RbacAccessRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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";
@@ -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<ReturnType<SqliteDatabaseAdapter["connect"]>>["sqlite"]
Awaited<ReturnType<TestSqliteDatabase["connect"]>>["sqlite"]
>;
}> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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.",
]);
});
});
@@ -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<string, string | undefined>;
const DOMAIN_ALIASES: Record<string, RepositoryRolloutDomain> = {
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,
});
}
}
@@ -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<void>,
): Promise<RoleRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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<void>,
): Promise<SessionRecordingRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE users (
@@ -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;
@@ -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;
@@ -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<SettingsRepository> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE settings (
@@ -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(
@@ -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<ReturnType<SqliteDatabaseAdapter["connect"]>>["sqlite"]
Awaited<ReturnType<TestSqliteDatabase["connect"]>>["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 (
@@ -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;
@@ -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<ReturnType<SqliteDatabaseAdapter["connect"]>>["sqlite"]
Awaited<ReturnType<TestSqliteDatabase["connect"]>>["sqlite"]
>;
}> {
adapter = new SqliteDatabaseAdapter({
dialect: "sqlite",
url: ":memory:",
sqlitePath: ":memory:",
});
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
context.sqlite?.exec(`
CREATE TABLE snippets (

Some files were not shown because too many files have changed in this diff Show More