4.9 KiB
@suaveplan/api-db-adapter — Bridge from @suaveplan/db to plugin DatabaseLike
Package: @suaveplan/api-db-adapter
Status: PROPOSED
Type: new-package
Stage: 4 (Base adapters) — see umbrella openspec/changes/api-plugin-foundation/
Why
Plugins consume ctx.db typed as either AdminDatabaseLike (in PluginContext — for migrations + admin work) or TenantScopedDatabaseLike (in RequestContext — same field, narrower type). Per design doc §5.1, the tenant-scoped wrapper injects WHERE tenant_id = $1 on reads and tenant_id on writes — plugins literally cannot read another tenant's rows from a route handler. Admin access from a route is the explicit exception via the db.unsafe-queries capability flag in the plugin's manifest; every such call emits plugin.unsafe_query_total.
api-db-adapter is the bridge between the genesis @suaveplan/db adapter foundation and the plugin host. It implements:
- The
AdminDatabaseLikesurface (raw SQL/SurQL plus prepared statements). - The tenant-scoped wrapper (rejects unscoped queries at the boundary).
- A per-engine migration runner (Postgres / SQLite / Surreal) consuming each plugin's
manifest.database.migrationsdirectory. - The
unsafeescape hatch —db.unsafe.query()— gated by manifest capability declaration.
Stage 4 invariant: ships direct AND worker-proxy surfaces; both pass runDbAdapterConformance (§5.2a, §13.4). The proxy uses transferable ReadableStream for cursor streaming (large query results survive the boundary without buffering).
What changes
Net-new package depending on @suaveplan/api-db (contract), @suaveplan/api-types, @suaveplan/api-worker-rpc, @suaveplan/db, @suaveplan/error, zod.
- ADDED:
createDbAdapter(opts)— direct-surface factory; returns{ admin: AdminDatabaseLike; scoped(tenantId): TenantScopedDatabaseLike }. - ADDED:
createDbAdapterWorkerProxy(port, opts)— proxy-surface factory. - ADDED:
DbAdapterOptions— Zod schema withengine: "postgres" | "sqlite" | "surrealdb",url: string,pool: { min, max, idleMs },tenantColumn?: string(defaulttenant_id),migrationsDir?: string. - ADDED:
createTenantScopedWrapper(admin, tenantId, options)— injectsWHERE tenantColumn = $1on every read; rejects writes whose payload lackstenantColumn(or auto-injects, configurable). - ADDED:
runMigrations(plugin, capabilities)— per-engine migration runner consumingmanifest.database.migrationsdirectory; tracks applied versions in__plugin_migrationstable. - ADDED:
db.unsafe.query(sql, params)— capability-gated; throwsDB_UNSAFE_NOT_PERMITTEDifplugin.capabilitieslacksdb.unsafe-queries. On allowed calls emitsplugin.unsafe_query_total{plugin}counter. - ADDED:
DB_ADAPTER_*error codes (DB_ADAPTER_TENANT_SCOPE_VIOLATION,DB_ADAPTER_UNSAFE_NOT_PERMITTED,DB_ADAPTER_MIGRATION_FAILED,DB_ADAPTER_ENGINE_UNSUPPORTED,DB_ADAPTER_INVALID_OPTIONS).
Out of scope
- The
@suaveplan/dbadapter foundation itself (already exists at v0). - Surreal-specific features beyond CRUD + migrations —
api-db-surreal-graphfuture work. - Connection pooling internals — delegated to
@suaveplan/db. - 2PC participant role —
api-2pcStage 6. - Saga compensation —
api-sagaStage 6.
Source mapping
Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.
| Requirement | Design doc section |
|---|---|
| Same field, different type per ctx | §5.1 |
| Tenant-scoped wrapper rejects unscoped reads | §5.1 (paragraph after RequestContext) |
db.unsafe-queries capability gating |
§5.1 (last paragraph), §2.2 (sandbox row) |
plugin.unsafe_query_total counter |
§5.1 (last paragraph) |
| Per-engine migration runner | §3 (manifest database.migrations), §4 (FSM installing step) |
| Worker-proxy variant + transferable streams | §5.2a |
| Conformance both surfaces | §13.4 |
Verification approach
src/factory/create-db-adapter.test.ts— admin surface against in-memory SQLite (realbun:sqlite); read/write/transaction.src/factory/create-db-adapter.spec-conformance.test.ts—runDbAdapterConformance("db-direct", ...).src/proxy/create-db-adapter-worker-proxy.test.ts— proxy round-trip;ReadableStreamcursor preserved acrossMessageChannel.src/proxy/create-db-adapter-worker-proxy.spec-conformance.test.ts—runDbAdapterConformance("db-worker-proxy", ...).src/tenant-scoping/create-tenant-scoped-wrapper.test.ts—SELECTinjection,INSERTinjection, cross-tenant read rejection.src/migrations/run-migrations.test.ts— per-engine: Postgres (testcontainer), SQLite (memory), Surreal (testcontainer). Forward + idempotent re-run.src/unsafe/unsafe-query-gate.test.ts— capability check + counter emission.tests/integration/postgres.test.ts— real Postgres via@suaveplan/testing-integration-postgres.
Migration impact
None — additive only.