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 usingdefinePlugin.onInitbuilds the pool, registersdbinServiceRegistry.onReadyauto-runs migrations under advisory lock.onClosedrains the pool. - ADDED:
ConnectionPool— bounded pool ofSurrealAdapterinstances with min/max sizing, idle reaping,acquireTimeoutMs, waiter queue, andsize()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 supportingshared|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 registeredDATABASE_*error metadata. - ADDED:
DATABASE_CODESregistry (8 codes viadefineCodes) + concreteDatabaseErrorextendingSuaveplanError. - ADDED:
DatabasePluginOptionsSchema/DatabasePluginConfigSchemaZod boundary on every public option bundle. - ADDED: OTel instrument bundle (
pool.checkouts_total,pool.checkins_total,pool.saturation,query.duration_mshistogram keyed by{ outcome },migrations.applied_total). - ADDED:
ServiceRegistry.dbaugmentation (DatabaseAdapterslot) 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 pooledDatabaseAdapterviactx.getService("db")and calls the canonical surface defined by@suaveplan/db. - Descriptor-level
onTenantCreate/onTenantDeletehooks — the genesisPluginDescriptorhas no tenant-lifecycle slots; tenant lifecycle ships as the callable exportsprovisionTenant/decommissionTenantplus a documented event-bus integration pattern (api-plugin-tenantinvokes 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/surrealdbSurrealAdapter. Consumers callctx.getService("db").query(sql, params: readonly unknown[])per the@suaveplan/dbDatabaseAdaptercontract.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 bySurrealAdapter's native CRUD methods exposed onDatabaseAdapter.SurrealAdapter.transaction(): Transaction(the inversion-of-control transaction handle pattern) — 🚫 superseded byDatabaseAdapter.transaction(fn, opts)callback-style transactions defined by@suaveplan/db.SurrealAdapter.use(ns, db)/.info()/.ping()— 🚫 superseded bySurrealAdapter's lifecycle (connect/disconnect/isConnected); namespace/database are set at construction time.SurrealAdapter.getClient()/.releaseClient(c)/.getPoolSize()— 🚫 platform mismatch. Genesis poolsSurrealAdapterinstances (each owning its own client); callers acquire viapool.acquire()and release viapool.release(adapter). Pool size is reported bypool.size().- Descriptor
onTenantCreate(ctx, tenantId)/onTenantDelete(ctx, tenantId)hooks — 🚫 platform constraint: genesisPluginDescriptorhas no tenant-lifecycle hook slots. Equivalent functionality ships asprovisionTenant(adapter, config, tenantId)anddecommissionTenant(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 isdb. databaseMiddlewareHono middleware — 🚫 genesis pattern isctx.getService("db")from the host; the plugin does not contribute middleware.
Verification approach
src/error-codes/error-codes.test.ts— everyDATABASE_*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—.surqldiscovery, 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 forDatabaseError,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-memoryMeterProvider.src/plugin/plugin.test.ts— full lifecycle:onInitbuilds pool + registers service,onReadymigrates under lock,onClosedrains 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.