Files
suaveplan/openspec/archive/2026-04-23-enterprise-db/tasks.md
2026-05-27 23:29:53 +01:00

12 KiB

Tasks: enterprise-db

1. Scaffold

  • 1.1 Create directory universal/database/db/ with package.json (name: "@suaveplan/db", version: "0.1.0", sideEffects: false). Add deps: @suaveplan/error workspace:^, @suaveplan/types workspace:^, @suaveplan/events workspace:^, zod catalog:forms, @opentelemetry/api catalog:observability, tslib catalog:build. Add dev deps: vitest catalog:testing, @vitest/coverage-istanbul catalog:testing, fast-check catalog:testing, mitata catalog:testing.
  • 1.2 Create tsconfig.json extending @suaveplan/typescript-config/library.json. Create vitest.config.ts using createTestConfig() from @suaveplan/testing/config with pool: 'forks', provider: 'istanbul', thresholds 100/100/100/100.
  • 1.3 Add Turborepo pipeline entries (lint, typecheck, test, test:coverage, build) in turbo.json if not already covered by root glob. Verify universal/database/* is in the root package.json workspace glob.
  • 1.4 Add @suaveplan/db to CLAUDE.md package listing and README.md table in the repo root.

2. Types and errors

  • 2.1 Create src/errors/db-codes.ts: call defineCodes from @suaveplan/error to register DB.CONNECTION_FAILED, DB.NOT_CONNECTED, DB.QUERY_FAILED, DB.TRANSACTION_FAILED, DB.MIGRATION_FAILED with stable integer codes and human-readable descriptions.
  • 2.2 Create src/errors/database-error.ts: DatabaseError extends SuaveplanError with readonly code: string and typed cause. Constructor accepts (message, code, cause?). Export from src/errors/index.ts.
  • 2.3 Create src/errors/migration-error.ts: MigrationError extends DatabaseError with readonly migration: string. Constructor sets code DB.MIGRATION_FAILED and formats message as "Migration '<name>' failed: <detail>". Export from barrel.
  • 2.4 Write src/errors/database-error.test.ts and src/errors/migration-error.test.ts: verify instanceof SuaveplanError, stable code value, cause chain, name property, and message format. { timeout: 5000 }.
  • 2.5 Write src/errors/database-error.md and src/errors/migration-error.md (≥200 words each, all required sections).
  • 2.6 Create src/types/database-config.ts: DatabaseConfig interface (filename?, url?, readonlyMode?). Create src/types/migration-config.ts: MigrationConfig (migrationsFolder?, migrationsTable?). Create src/types/migration-result.ts: MigrationResult (applied: number, migrations: string[]) and MigrationStatus (name: string, applied: boolean, appliedAt?: Date). Export all from src/types/index.ts.
  • 2.7 Create src/types/database-config.schemas.ts and src/types/migration-config.schemas.ts with Zod schemas DatabaseConfigSchema and MigrationConfigSchema. Include z.object(...) with .optional() fields and sensible .default() coercions where applicable.
  • 2.8 Write src/types/types.test.ts: parse valid and invalid inputs against both schemas; verify Zod error messages and coerced defaults. { timeout: 5000 }.
  • 2.9 Write src/types/database-config.md and src/types/migration-config.md (≥200 words each).

3. DatabaseAdapter abstract interface

  • 3.1 Create src/adapter/database-adapter.ts: define DatabaseAdapter extending DatabaseLike (from @suaveplan/types). Full signature:
    interface DatabaseAdapter extends DatabaseLike {
        connect(config?: Partial<DatabaseConfig>, opts?: { signal?: AbortSignal }): Promise<void>;
        disconnect(opts?: { signal?: AbortSignal }): Promise<void>;
        query<T = Record<string, unknown>>(sql: string, params?: readonly unknown[], opts?: { signal?: AbortSignal }): Promise<T[]>;
        execute(sql: string, params?: readonly unknown[], opts?: { signal?: AbortSignal }): Promise<{ rowsAffected: number }>;
        transaction<T>(fn: (tx: DatabaseLike) => Promise<T>, opts?: { signal?: AbortSignal }): Promise<T>;
        migrate(config?: MigrationConfig, opts?: { signal?: AbortSignal }): Promise<MigrationResult>;
        readonly isConnected: boolean;
    }
    
    Note: disconnect maps to DatabaseLike.close — DatabaseAdapter must satisfy DatabaseLike structurally; add a close(): Promise<void> alias declaration if needed.
  • 3.2 Create src/adapter/index.ts barrel re-exporting DatabaseAdapter.
  • 3.3 Write src/adapter/database-adapter.test.ts: create a minimal in-memory stub (no vi.mock) that implements every method; verify structural compatibility with DatabaseLike; verify isConnected transitions on connect/disconnect; verify AbortSignal respected by the stub. { timeout: 5000 }.
  • 3.4 Write src/adapter/database-adapter.md (≥200 words, full API reference, both examples).

4. Connection singleton helpers

  • 4.1 Create src/connection/singleton.ts: implement getSingleton, closeSingleton, hasSingleton, closeAllSingletons. Each function accepts an optional trailing registry?: Map<string, DatabaseAdapter> parameter; when omitted, fall back to a module-level default Map. getSingleton validates config with DatabaseConfigSchema before calling adapter.connect. Wrap connect in a try/catch; throw DatabaseError with DB.CONNECTION_FAILED on failure.
  • 4.2 Create src/connection/index.ts barrel.
  • 4.3 Write src/connection/singleton.test.ts:
    • Pass an isolated registry Map to every call — no cross-test bleed.
    • getSingleton creates and caches; second call returns same reference.
    • hasSingleton reflects registry state accurately.
    • closeSingleton calls disconnect and removes entry.
    • closeAllSingletons drains the registry even when one disconnect rejects (test error-isolation path).
    • getSingleton with already-connected adapter skips connect.
    • Abort via AbortSignal propagated to connect.
    • { timeout: 30000 } for async tests.
  • 4.4 Write src/connection/singleton.md (≥200 words, registry-isolation example mandatory in advanced section).

5. Migration types and runner interface

  • 5.1 Create src/migration/migration-runner.ts: define MigrationRunner interface:
    interface MigrationRunner {
        pendingMigrations(config?: MigrationConfig): Promise<MigrationStatus[]>;
        applyMigration(name: string, sql: string, opts?: { signal?: AbortSignal }): Promise<void>;
        run(config?: MigrationConfig, opts?: { signal?: AbortSignal }): Promise<MigrationResult>;
    }
    
    This is the contract DatabaseAdapter.migrate delegates to in concrete adapters.
  • 5.2 Create src/migration/index.ts barrel re-exporting MigrationRunner, MigrationStatus, MigrationResult, MigrationConfig, and MigrationError.
  • 5.3 Write src/migration/migration-runner.test.ts: stub MigrationRunner implementation; verify run aggregates applyMigration calls; verify MigrationError thrown with correct migration field on failure; verify AbortSignal halts mid-run. { timeout: 30000 }.
  • 5.4 Write src/migration/migration-runner.md (≥200 words).

6. OTel instrumentation

  • 6.1 Create src/telemetry/instruments.ts: lazy-initialise (using a module-level singleton guard, NOT top-level getMeter() calls) the following instruments via @opentelemetry/api:
    • Histogram db.query.duration_ms — attributes: db.operation (query | execute)
    • UpDownCounter db.connection.count — attributes: db.name
    • Counter db.transaction.count — attributes: db.outcome (committed | rolled_back)
    • Counter db.migration.count — attributes: db.migration.name
  • 6.2 Create src/telemetry/trace.ts: helper withDbSpan<T>(tracer, name, fn, attrs?) that creates a span, calls fn(span), sets span status on success/error, and ends the span. Used internally by singleton helpers and the migration runner helper.
  • 6.3 Create src/telemetry/index.ts barrel.
  • 6.4 Write src/telemetry/instruments.test.ts and src/telemetry/trace.test.ts: verify no OTel error thrown when no SDK is registered (no-op provider); verify span name and attributes passed correctly; verify histogram records timing. { timeout: 5000 }.
  • 6.5 Write src/telemetry/instruments.md and src/telemetry/trace.md (≥200 words each).

7. Benchmarks

  • 7.1 Create src/__benchmarks__/singleton.bench.ts (mitata): benchmark getSingleton cache-hit path (no connect), hasSingleton lookup, and DatabaseConfigSchema.parse for a valid config. Publish numbers in README.md.
  • 7.2 Create src/__benchmarks__/otel.bench.ts (mitata): benchmark withDbSpan overhead against a raw async call using the OTel no-op provider.

8. Property-based tests (fast-check)

  • 8.1 Create src/__tests__/state-machine.property.test.ts: fast-check arbitraries for:
    • Connection-state invariant: any sequence of connect/disconnect on a stub adapter leaves isConnected consistent with the last operation.
    • Registry invariant: any sequence of getSingleton / closeSingleton / closeAllSingletons calls on an isolated registry map never leaves a dangling reference.
    • DatabaseConfigSchema round-trip: arbitrary partial config objects parse without throwing when all present fields are valid.
    • getSingleton idempotency: calling with the same name twice on a connected registry returns === the same object.
    • { timeout: 30000 }

9. Docs and README

  • 9.1 Write README.md at the package root: description, bun add @suaveplan/db, quick-start (getSingleton + query), Modules section linking all co-located .md files, benchmark table (placeholder — fill after 7.x runs).
  • 9.2 Verify every implementation .ts (non-test, non-barrel, non-types) has a sibling .md with ≥200 words and both required code examples.
  • 9.3 Run the missing-md scan from openspec/AGENTS.md §11 — zero output required.

10. Barrel and subpath exports

  • 10.1 Create src/index.ts top-level barrel re-exporting: DatabaseAdapter, getSingleton, hasSingleton, closeSingleton, closeAllSingletons, DatabaseError, MigrationError, DatabaseConfig, MigrationConfig, MigrationResult, MigrationStatus, MigrationRunner, DatabaseConfigSchema, MigrationConfigSchema, DB error codes.
  • 10.2 Add subpath exports to package.json:
    • "." — top-level barrel
    • "./adapter/database-adapter" — adapter contract
    • "./connection/singleton" — singleton helpers
    • "./errors/database-error" — DatabaseError + MigrationError
    • "./migration/migration-runner" — MigrationRunner + migration types
    • "./telemetry/instruments" — OTel instruments
    • "./telemetry/trace" — withDbSpan helper
    • "./types/database-config" — DatabaseConfig + schema
    • "./types/migration-config" — MigrationConfig + schema
  • 10.3 Add a tree-shake test (src/__tests__/tree-shake.test.ts) that dynamically imports each subpath and asserts no unexpected side effects (global state mutations, OTel registration). Confirm sideEffects: false is set in package.json.

11. Verify and publish

  • 11.1 Run the full pre-ship verification sequence from openspec/AGENTS.md §11:
    1. bunx turbo lint typecheck test --filter=@suaveplan/db — green
    2. bunx turbo test:coverage --filter=@suaveplan/db — 100/100/100/100
    3. bunx turbo build --filter=@suaveplan/db — clean dist
    4. Banned-pattern scan — zero matches
    5. Missing-md scan — zero matches
    6. README check — present
    7. Bare-version check — zero bare versions
    8. Subpath export count matches feature module count
  • 11.2 Confirm @suaveplan/db-sqlite can be started (it depends on this package — interface stability check only; do not build db-sqlite as part of this task).
  • 11.3 bun scripts/publish.ts --package @suaveplan/db
  • 11.4 Fold spec delta into openspec/specs/database/adapter/spec.md (create if absent).
  • 11.5 Move openspec/changes/enterprise-db/ → openspec/archive/2026-04-23-enterprise-db/.