/* oxlint-disable executor/no-try-catch-or-throw -- boundary: owned local database open composes the lifetime SQLite lock, crash-resumable migration, and serving DB handle */ import { dirname, join } from "node:path"; import type { FumaTables } from "@executor-js/sdk"; import { acquireDataDirOwnership } from "./data-dir-ownership"; import { createSqliteFumaDb, type SqliteFumaDb } from "./sqlite-fumadb"; import { migrateLocalV1ToV2IfNeeded, type LocalV1V2MigrationOptions, type LocalV1V2MigrationResult, } from "./v1-v2-migration"; export interface OpenOwnedLocalDatabaseOptions { readonly dataDir: string; readonly tables: TTables; readonly namespace: string; readonly tenantId: string; readonly version?: string; readonly oauthMetadataFetch?: LocalV1V2MigrationOptions["oauthMetadataFetch"]; readonly oauthMetadataTimeoutMs?: LocalV1V2MigrationOptions["oauthMetadataTimeoutMs"]; } export interface OwnedLocalDatabase { /** Real data directory that owns this handle. Symlinked input paths collapse here. */ readonly dataDir: string; readonly sqlitePath: string; readonly lockPath: string; readonly db: SqliteFumaDb; readonly migration: LocalV1V2MigrationResult; readonly close: () => Promise; } export const openOwnedLocalDatabase = async ( input: OpenOwnedLocalDatabaseOptions, ): Promise> => { const ownership = await acquireDataDirOwnership(input.dataDir); let db: SqliteFumaDb | null = null; try { // Use the real directory captured by the ownership primitive so a symlinked // input path cannot be repointed between lock acquisition and data.db open. const dataDir = dirname(ownership.lockPath); const sqlitePath = join(dataDir, "data.db"); const migration = await migrateLocalV1ToV2IfNeeded({ sqlitePath, tables: input.tables, namespace: input.namespace, tenantId: input.tenantId, ...(input.oauthMetadataFetch !== undefined ? { oauthMetadataFetch: input.oauthMetadataFetch } : {}), ...(input.oauthMetadataTimeoutMs !== undefined ? { oauthMetadataTimeoutMs: input.oauthMetadataTimeoutMs } : {}), }); const openedDb = await createSqliteFumaDb({ tables: input.tables, namespace: input.namespace, path: sqlitePath, ...(input.version !== undefined ? { version: input.version } : {}), }); db = openedDb; let closed = false; return { dataDir, sqlitePath, lockPath: ownership.lockPath, db: openedDb, migration, close: async () => { if (closed) return; closed = true; try { await openedDb.close(); } finally { await ownership.release(); } }, }; } catch (cause) { try { if (db) await db.close(); } finally { await ownership.release(); } throw cause; } };