7.8 KiB
OpenSpec Change: enterprise-db
- Change ID: enterprise-db
- Package:
@suaveplan/db - Category: universal
- Target path:
universal/database/db - Status: IN PROGRESS
- Date: 2026-04-23
- Reference spec:
/mnt/s/web/suaveplan/enterprise/db.md
Summary
@suaveplan/db is the universal relational database interface for the genesis monorepo. It ships the abstract DatabaseAdapter contract, structured errors with stable DB.* codes, Zod-validated config types, injectable connection-singleton helpers, and an AbortSignal-aware migration runner interface. The package is the stable foundation that @suaveplan/db-sqlite (and future Postgres / Turso adapters) build upon — its public API surface must be locked before any concrete adapter ships.
This genesis build replaces the main-branch implementation with a fully enterprise-quality package: DatabaseAdapter extends DatabaseLike from @suaveplan/types, errors extend @suaveplan/error with a stable code registry, every async entrypoint accepts an AbortSignal, all public types have Zod schemas, and OTel spans / histograms / counters are wired throughout. Connection singletons shed global module state in favour of injectable registry maps so they are safe in test isolation and serverless environments.
Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build. Main branch ships packages/universal/db with the surface listed below; genesis rewrites from scratch.
Available workspace deps on genesis
| Package | Ref | Role |
|---|---|---|
@suaveplan/error |
workspace:^ |
Structured error base class + defineCodes |
@suaveplan/types |
workspace:^ |
DatabaseLike minimal adapter contract |
@suaveplan/events |
workspace:^ |
Optional lifecycle event bus (connect / disconnect / error events) |
zod |
catalog:forms |
Runtime schema validation |
@opentelemetry/api |
catalog:observability |
OTel spans, counters, histograms (no-op when no exporter) |
tslib |
catalog:build |
TypeScript helpers |
Dev deps (all catalog:testing): vitest, @vitest/coverage-istanbul, fast-check, mitata.
Preserved public API surface (from main branch)
All of the following symbols are preserved verbatim or as strict supersets.
DatabaseAdapter (interface)
// main-branch shape — genesis extends this with DatabaseLike
export interface DatabaseAdapter {
connect(config?: Partial<DatabaseConfig>): Promise<void>;
disconnect(): Promise<void>;
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
execute(sql: string, params?: unknown[]): Promise<void>;
transaction<T>(fn: () => Promise<T>): Promise<T>;
migrate(config?: MigrationConfig): Promise<MigrationResult>;
readonly isConnected: boolean;
}
Connection singleton helpers
getSingleton(
name: string,
factory: () => DatabaseAdapter,
config?: Partial<DatabaseConfig>
): Promise<DatabaseAdapter>;
closeSingleton(name: string): Promise<void>;
hasSingleton(name: string): boolean;
closeAllSingletons(): Promise<void>;
Errors
class DatabaseError extends Error {
readonly code: string;
readonly cause?: unknown;
constructor(message: string, code: string, cause?: Error);
}
class MigrationError extends DatabaseError {
readonly migration: string;
constructor(migration: string, detail: string, cause?: Error);
}
Config and result types
interface DatabaseConfig {
filename?: string; // embedded (SQLite, ':memory:')
url?: string; // network (postgres://, ...)
readonlyMode?: boolean;
}
interface MigrationConfig {
migrationsFolder?: string; // default './drizzle'
migrationsTable?: string; // default '__drizzle_migrations'
}
interface MigrationStatus {
name: string;
applied: boolean;
appliedAt?: Date;
}
interface MigrationResult {
applied: number;
migrations: string[];
}
New additions for genesis
DatabaseAdapter extends DatabaseLike
DatabaseLike (from @suaveplan/types) defines query, execute, transaction, and close. The genesis DatabaseAdapter extends this contract and adds:
connect(config?, opts?: { signal?: AbortSignal }): Promise<void>— lifecycle opendisconnect(opts?: { signal?: AbortSignal }): Promise<void>— lifecycle close (maps toDatabaseLike.close)migrate(config?: MigrationConfig, opts?: { signal?: AbortSignal }): Promise<MigrationResult>readonly isConnected: boolean— connection state predicate
query, execute, and transaction gain an optional trailing options object { signal?: AbortSignal } while remaining backward-compatible.
The transaction callback receives a DatabaseLike (the active transaction handle) matching DatabaseLike's own signature — fixing the main-branch deviation where fn accepted no argument.
Structured errors extending @suaveplan/error
DatabaseError extends SuaveplanError (from @suaveplan/error) with stable codes registered via defineCodes:
| Code | When thrown |
|---|---|
DB.CONNECTION_FAILED |
connect() fails |
DB.NOT_CONNECTED |
Operation attempted before connect() |
DB.QUERY_FAILED |
query() or execute() throws |
DB.TRANSACTION_FAILED |
transaction() throws or rolls back |
DB.MIGRATION_FAILED |
migrate() fails on a specific migration |
MigrationError extends DatabaseError with DB.MIGRATION_FAILED and adds a migration: string field (the migration name).
Zod schemas
DatabaseConfigSchema and MigrationConfigSchema (exported from *.schemas.ts siblings). Validated at the boundary of getSingleton and migrate helpers.
Injectable registry for singleton helpers
getSingleton, closeSingleton, hasSingleton, and closeAllSingletons accept an optional registry: Map<string, DatabaseAdapter> parameter. When omitted a module-level default map is used (preserving the main-branch behaviour). This allows test suites to pass isolated registries with zero cross-test bleed.
OTel instrumentation
All public async entrypoints are wrapped in @opentelemetry/api spans. Additionally:
- Histogram
db.query.duration_ms— labelled bydb.operation(query/execute) - Counter
db.connection.count— incremented on successfulconnect, decremented ondisconnect - Counter
db.transaction.count— labelled by outcome (committed/rolled_back) - Counter
db.migration.count— incremented per applied migration
All instrumentation is lazy-initialised; no global OTel state at module load.
AbortSignal propagation
Every async method (connect, disconnect, query, execute, transaction, migrate) accepts opts?: { signal?: AbortSignal }. The concrete adapter is responsible for honouring the signal — the abstract contract documents the expectation. Abort throws DatabaseError with code DB.QUERY_FAILED (or the relevant operation code) and cause set to an AbortError.
Property-based tests (fast-check)
Fast-check arbitraries model the connection-state machine:
- An unconnected adapter may not have
query,execute,transaction, ormigratesucceed (invariant). connect → disconnect → connectis idempotent (no state leak).getSingletonwith the same name and registry always returns the same reference.closeAllSingletonsleaves the registry empty regardless of concurrency order.
Benchmarks (mitata)
src/__benchmarks__/adapter.bench.ts — baseline numbers for singleton lookup, OTel span overhead, schema validation. Published in README.
Impact
- Adds one new package:
universal/database/db - Adds one new spec capability:
database/adapter - Unblocks
@suaveplan/db-sqlite(direct dep) and every higher-layer package that injectsDatabaseLike - No changes to existing packages;
@suaveplan/types.DatabaseLikeis already shipped