83 lines
3.3 KiB
Markdown
83 lines
3.3 KiB
Markdown
# Spec: database/adapter
|
|
|
|
**Package**: `@suaveplan/db`
|
|
**Version**: `0.1.0`
|
|
**Status**: Shipped 2026-04-23
|
|
|
|
## Purpose
|
|
|
|
Universal relational database adapter interface for the genesis monorepo. Provides the `DatabaseAdapter` contract (extending `DatabaseLike` from `@suaveplan/types`), injectable connection singletons, structured `DB.*` error codes, Zod-validated config types, a `MigrationRunner` interface, and full OpenTelemetry instrumentation.
|
|
|
|
## Public API
|
|
|
|
### `DatabaseAdapter` (interface, extends `DatabaseLike`)
|
|
|
|
```ts
|
|
interface DatabaseAdapter extends DatabaseLike {
|
|
connect(config?: Partial<DatabaseConfig>, opts?: DbOperationOptions): Promise<void>;
|
|
disconnect(opts?: DbOperationOptions): Promise<void>;
|
|
query<T>(sql: string, params?: readonly unknown[], opts?: DbOperationOptions): Promise<T[]>;
|
|
execute(sql: string, params?: readonly unknown[], opts?: DbOperationOptions): Promise<{ rowsAffected: number }>;
|
|
transaction<T>(fn: (tx: DatabaseLike) => Promise<T>, opts?: DbOperationOptions): Promise<T>;
|
|
migrate(config?: MigrationConfig, opts?: DbOperationOptions): Promise<MigrationResult>;
|
|
readonly isConnected: boolean;
|
|
}
|
|
```
|
|
|
|
### Connection singletons
|
|
|
|
```ts
|
|
getSingleton(name, factory, config?, opts?, registry?): Promise<DatabaseAdapter>
|
|
closeSingleton(name, opts?, registry?): Promise<void>
|
|
hasSingleton(name, registry?): boolean
|
|
closeAllSingletons(opts?, registry?): Promise<void>
|
|
```
|
|
|
|
`registry` defaults to module-level `Map`; pass an explicit `Map` for test isolation.
|
|
|
|
### Error codes (`DB_CODES`)
|
|
|
|
| Code | When thrown |
|
|
|---|---|
|
|
| `DB.CONNECTION_FAILED` | `connect()` fails |
|
|
| `DB.NOT_CONNECTED` | Operation before `connect()` |
|
|
| `DB.QUERY_FAILED` | `query()` or `execute()` throws |
|
|
| `DB.TRANSACTION_FAILED` | `transaction()` throws |
|
|
| `DB.MIGRATION_FAILED` | Migration fails |
|
|
|
|
`DatabaseError extends SuaveplanError<DbCode>` · `MigrationError extends DatabaseError` (adds `migration: string`)
|
|
|
|
### Config types
|
|
|
|
- `DatabaseConfig` — `filename?`, `url?`, `readonlyMode?` (default `false`)
|
|
- `MigrationConfig` — `migrationsFolder?` (default `"./drizzle"`), `migrationsTable?` (default `"__drizzle_migrations"`)
|
|
- `MigrationResult` — `{ applied: number, migrations: string[] }`
|
|
- `MigrationStatus` — `{ name: string, applied: boolean, appliedAt?: Date }`
|
|
|
|
Zod schemas: `DatabaseConfigSchema`, `MigrationConfigSchema` (both `.strict()`).
|
|
|
|
### `MigrationRunner` (interface)
|
|
|
|
```ts
|
|
interface MigrationRunner {
|
|
pendingMigrations(config?: MigrationConfig): Promise<MigrationStatus[]>;
|
|
applyMigration(name: string, sql: string, opts?: DbOperationOptions): Promise<void>;
|
|
run(config?: MigrationConfig, opts?: DbOperationOptions): Promise<MigrationResult>;
|
|
}
|
|
```
|
|
|
|
### OTel instrumentation
|
|
|
|
- `withDbSpan(name, fn, attrs?)` — wraps async fn in a named OTel span
|
|
- `recordQueryDuration(ms, operation)` — histogram `db.query.duration_ms`
|
|
- `recordConnectionDelta(±1, dbName)` — UpDownCounter `db.connection.count`
|
|
- `recordTransaction(outcome)` — counter `db.transaction.count`
|
|
- `recordMigration(name)` — counter `db.migration.count`
|
|
- `resetInstruments()` — clears lazy singleton (test isolation)
|
|
|
|
## Quality
|
|
|
|
- 100% Istanbul coverage (statements/branches/functions/lines)
|
|
- 63 unit tests + property-based tests (fast-check)
|
|
- Benchmarks (mitata): `src/__benchmarks__/singleton.bench.ts`, `otel.bench.ts`
|