Files
2026-05-27 23:29:53 +01:00

84 lines
9.0 KiB
Markdown

# 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`.