Files
suaveplan/openspec/archive/2026-05-18-add-api-plugin-database/proposal.md
2026-05-27 23:29:53 +01:00

9.0 KiB

Add @suaveplan/api-plugin-database (Path A — wraps SurrealAdapter)

Package: @suaveplan/api-plugin-database Status: PROPOSED Type: new-package

  • Spec delta: ./specs/api-plugin-database/spec.md

Why

@suaveplan/api plugin hosts need a one-shot way to bring up a SurrealDB-backed DatabaseAdapter with production-grade orthogonals: a connection pool, an advisory-locked migration runner, tenant provisioning, retry/backoff, RFC 7807 error rendering, and a full OTel surface. Genesis already ships @suaveplan/surrealdb (which exposes SurrealAdapter implementing @suaveplan/db's DatabaseAdapter) and @suaveplan/api-migrations (which ships a real SurrealQL migration adapter and runner). Without this plugin, every consuming app must hand-wire those primitives, and the cross-cutting pool/tenancy/retry/problem-json/OTel pieces fall on application code.

The upstream ../suaveplan/packages/server/api-plugin-database/ re-implements its own SurrealAdapter, exposes an adapter surface (query/select/create/update/delete/transaction/use/info/getClient/releaseClient/getPoolSize) that is irreconcilable with genesis's DatabaseAdapter shape, and hangs tenant lifecycle off a descriptor hook (onTenantCreate / onTenantDelete) that genesis's PluginDescriptor does not carry. Path A is therefore: keep the orthogonal surface upstream contributed (pool, migrations, tenancy callable, retry, problem-json, OTel, error codes), but bind every adapter call to the canonical SurrealAdapter and expose it via ServiceRegistry["db"]: DatabaseAdapter.

What changes

  • ADDED: createDatabasePlugin(options?) — PluginDescriptor<DatabasePluginConfig> factory using definePlugin. onInit builds the pool, registers db in ServiceRegistry. onReady auto-runs migrations under advisory lock. onClose drains the pool.
  • ADDED: ConnectionPool — bounded pool of SurrealAdapter instances with min/max sizing, idle reaping, acquireTimeoutMs, waiter queue, and size() introspection.
  • ADDED: Migration ledger + advisory lock + runner integrating with @suaveplan/api-migrations (createSurrealqlAdapter + createMigrationRunner) plus a thin lock layer on top.
  • ADDED: provisionTenant(adapter, config, tenantId) / decommissionTenant(adapter, config, tenantId) callable exports supporting shared | namespace-per-tenant | database-per-tenant.
  • ADDED: withRetry(fn, options) — exponential backoff with optional jitter.
  • ADDED: respondProblemJson(c, err) — RFC 7807 renderer that consults the registered DATABASE_* error metadata.
  • ADDED: DATABASE_CODES registry (8 codes via defineCodes) + concrete DatabaseError extending SuaveplanError.
  • ADDED: DatabasePluginOptionsSchema / DatabasePluginConfigSchema Zod boundary on every public option bundle.
  • ADDED: OTel instrument bundle (pool.checkouts_total, pool.checkins_total, pool.saturation, query.duration_ms histogram keyed by { outcome }, migrations.applied_total).
  • ADDED: ServiceRegistry.db augmentation (DatabaseAdapter slot) consumed by every downstream plugin.

Out of scope

  • A re-implementation of SurrealAdapter.query/select/create/update/delete/transaction/use/info/getClient/releaseClient/getPoolSize — every consumer obtains the pooled DatabaseAdapter via ctx.getService("db") and calls the canonical surface defined by @suaveplan/db.
  • Descriptor-level onTenantCreate / onTenantDelete hooks — the genesis PluginDescriptor has no tenant-lifecycle slots; tenant lifecycle ships as the callable exports provisionTenant / decommissionTenant plus a documented event-bus integration pattern (api-plugin-tenant invokes them on its own tenant-create/delete events).

Source mapping

This change derives from ../suaveplan/enterprise/api-plugin-database.md and the upstream source tree ../suaveplan/packages/server/api-plugin-database/.

Requirement Source Notes
Connection pool src/pool/connection-pool.ts Reworked to pool SurrealAdapter instances rather than raw Surreal clients so pool members implement DatabaseAdapter.
Migration runner / ledger / lock src/migrations/runner/runner.ts, src/migrations/ledger/ledger.ts, src/migrations/lock/lock.ts, src/migrations/discovery/discovery.ts Discovery delegated to @suaveplan/api-migrations createMigrationRunner + createSurrealqlAdapter; advisory lock layered on top.
Tenancy provisioning callables src/tenancy/tenant-lifecycle.ts Re-exported as callable functions instead of descriptor hooks (platform constraint).
Retry / backoff src/retry/retry.ts Verbatim API, Zod-validated options.
RFC 7807 problem-json src/routes/problem-json.ts Bound to @suaveplan/error's formatter; uses local DATABASE_* metadata.
Plugin factory src/plugin/create-database-plugin.ts Rewritten on top of genesis definePlugin + ServiceRegistry; no onTenantCreate/Delete hooks.
DATABASE_* error codes src/errors/error-codes.ts Registered via genesis defineCodes + SuaveplanError.
OTel surface enterprise/api-plugin-database.md §3 Pool checkout/checkin, query duration histogram, migration counter (delegates to api-migrations for the migrations counter, mounts pool/query locally).
Zod boundary src/config/config-schema.ts Re-shaped under genesis Zod v4 import convention.

Excluded from this migration

  • SurrealAdapter.query(sql, vars: Record) — 🚫 superseded by @suaveplan/surrealdb SurrealAdapter. Consumers call ctx.getService("db").query(sql, params: readonly unknown[]) per the @suaveplan/db DatabaseAdapter contract.
  • SurrealAdapter.queryOne — 🚫 superseded; consumers call (await db.query(...))[0] or use the standard adapter pattern.
  • SurrealAdapter.select(table, id) / .create(table, data) / .update(table, id, data) / .delete(table, id) — 🚫 superseded by SurrealAdapter's native CRUD methods exposed on DatabaseAdapter.
  • SurrealAdapter.transaction(): Transaction (the inversion-of-control transaction handle pattern) — 🚫 superseded by DatabaseAdapter.transaction(fn, opts) callback-style transactions defined by @suaveplan/db.
  • SurrealAdapter.use(ns, db) / .info() / .ping() — 🚫 superseded by SurrealAdapter's lifecycle (connect/disconnect/isConnected); namespace/database are set at construction time.
  • SurrealAdapter.getClient() / .releaseClient(c) / .getPoolSize() — 🚫 platform mismatch. Genesis pools SurrealAdapter instances (each owning its own client); callers acquire via pool.acquire() and release via pool.release(adapter). Pool size is reported by pool.size().
  • Descriptor onTenantCreate(ctx, tenantId) / onTenantDelete(ctx, tenantId) hooks — 🚫 platform constraint: genesis PluginDescriptor has no tenant-lifecycle hook slots. Equivalent functionality ships as provisionTenant(adapter, config, tenantId) and decommissionTenant(adapter, config, tenantId) callable exports that @suaveplan/api-plugin-tenant (or any host) invokes explicitly on its own tenant-lifecycle events via the existing event bus.
  • Back-compat "surreal" service alias — 🚫 not part of the genesis contract; the canonical slot is db.
  • databaseMiddleware Hono middleware — 🚫 genesis pattern is ctx.getService("db") from the host; the plugin does not contribute middleware.

Verification approach

  • src/error-codes/error-codes.test.ts — every DATABASE_* code is thrown from a production path.
  • src/retry/retry.test.ts — exponential backoff, jitter, abort-aware retry.
  • src/pool/pool.test.ts — pool acquires/releases, max-size waiter queue, acquireTimeoutMs, idle reaper, close drain.
  • src/migrations/lock.test.ts — advisory lock with lease expiry, takeover, release.
  • src/migrations/runner.test.ts — .surql discovery, ledger apply, drift detection, advisory-lock integration.
  • src/tenancy/tenancy.test.ts — all three strategies provision/decommission with tenant-id validation.
  • src/routes/problem-json.test.ts — RFC 7807 shape for DatabaseError, HttpError, and unknown throwables.
  • src/config/config-schema.test.ts — Zod boundary accepts canonical input and rejects out-of-range knobs.
  • src/otel/instruments.test.ts — every counter/histogram emits against an in-memory MeterProvider.
  • src/plugin/plugin.test.ts — full lifecycle: onInit builds pool + registers service, onReady migrates under lock, onClose drains pool.
  • tests/integration/full-boot.test.ts — real SurrealDB testcontainer via @suaveplan/testing-integration-surrealdb, full pool + migration + tenancy roundtrip.

Migration impact

None — additive new package.

Impact

Affected dependents

No existing dependents (verified by bunx turbo ls --filter=...@suaveplan/api-plugin-database — new package). Downstream packages that will consume ServiceRegistry["db"] (e.g. api-plugin-user, future api-plugin-tenant) augment the registry themselves; this plugin only seeds db.