7.7 KiB
@suaveplan/api-db-adapter — bridge from @suaveplan/db engine adapters to the plugin DatabaseLike surface. Provides AdminDatabaseLike + TenantScopedDatabaseLike factories, a per-engine migration runner, the db.unsafe-queries capability gate, an open DatabaseEngineRegistry, and a worker-proxy variant for the worker host. Both direct + proxy surfaces pass the same conformance suite.
Stage-4 bridge from the @suaveplan/db engine adapters to the
plugin AdminDatabaseLike / TenantScopedDatabaseLike surfaces exposed by
@suaveplan/api-types/db.
The package contains five orthogonal pieces:
options— Zod-validatedDbAdapterOptions(engine,url,pool?,tenantColumn,migrationsDir?,autoInjectInserts).engine-registry— open registry of engine factories. The package itself ships no engines pre-installed; host code (or a downstream adapter package) callsregisterEngine("sqlite" | "postgres" | "surrealdb" | <merged>, factory)once at boot. The shape keeps the package universally importable.tenant-scoped— the SQL rewriter that injectsWHERE tenant_id = ?on every SELECT/UPDATE/DELETE, verifies / auto-injects the tenant column on INSERT, and rejects DDL or unsupported SQL withDB_ADAPTER.TENANT_SCOPE_VIOLATION. Strict / lenient INSERT mode isautoInjectInsertson the options.migrations— per-engine migration runner consuming a directory of*.sqlfiles; tracks applied versions in__plugin_migrationsand is idempotent on re-run. Failed migrations roll back where the engine supports transactions.unsafe— thedb.unsafeescape hatch, gated on thedb.unsafe-queriescapability flag in the plugin manifest. Every permitted call incrementsplugin.unsafe_query_total{plugin}.
The package also ships a worker-side counterpart (createDbAdapterWorkerProxy)
that wires the worker-side api-db-worker-proxy client into the same surface
so direct + proxy variants are interchangeable in plugin code.
Documentation tree
- engine-registry — The engine registry is the extension point that lets
api-db-adapterstay universally importable while still supportin…- engine-registry — The engine registry is the extension point that lets
api-db-adapterstay universally importable while still supportin…
- engine-registry — The engine registry is the extension point that lets
- error-codes — This module is the single source of truth for every public failure code raised by
@suaveplan/api-db-adapter. Codes ar…- error-codes — This module is the single source of truth for every public failure code raised by
@suaveplan/api-db-adapter. Codes ar…
- error-codes — This module is the single source of truth for every public failure code raised by
- factory —
createDbAdapteris the public entry point for host code that wants to wire the@suaveplan/dbengine adapter…- create-db-adapter —
createDbAdapteris the public entry point for host code that wants to wire the@suaveplan/dbengine adapter…
- create-db-adapter —
- migrations —
runMigrationsapplies a plugin's pending migrations from a directory of*.sqlfiles, recording applied versions in…- run-migrations —
runMigrationsapplies a plugin's pending migrations from a directory of*.sqlfiles, recording applied versions in…
- run-migrations —
- options —
DbAdapterOptionsis the single config object accepted by every public factory in this package —createDbAdapter, `c…- db-adapter-options —
DbAdapterOptionsis the single config object accepted by every public factory in this package —createDbAdapter, `c…
- db-adapter-options —
- proxy —
createDbAdapterWorkerProxyis the worker-side counterpart tocreateDbAdapter. Inside anapi-worker-hostworker, p…- create-db-adapter-worker-proxy —
createDbAdapterWorkerProxyis the worker-side counterpart tocreateDbAdapter. Inside anapi-worker-hostworker, p…
- create-db-adapter-worker-proxy —
- tenant-scoped — This module is the load-bearing safety boundary between a plugin route handler and the underlying admin database surfac…
- create-tenant-scoped-wrapper — This module is the load-bearing safety boundary between a plugin route handler and the underlying admin database surfac…
- unsafe —
createUnsafeGatebuilds thectx.db.unsafeAPI exposed on every tenant- scoped surface. The gate is the only sanctio…- unsafe-query-gate —
createUnsafeGatebuilds thectx.db.unsafeAPI exposed on every tenant- scoped surface. The gate is the only sanctio…
- unsafe-query-gate —
Quick start
import { createDbAdapter } from "@suaveplan/api-db-adapter/adapter";
import { registerEngine } from "@suaveplan/api-db-adapter/engine-registry";
import { runMigrations } from "@suaveplan/api-db-adapter/migrations";
registerEngine("sqlite", async (opts) => myBunSqliteFactory(opts));
const db = await createDbAdapter({ engine: "sqlite", url: ":memory:" });
await runMigrations({
plugin: "audit",
capabilities: [],
admin: db.admin,
migrationsDir: "./db/migrations",
fs: nodeFsReader,
});
const scoped = db.scoped("acme" as TenantId);
const rows = await scoped.query<User[]>("SELECT * FROM users");
// actual SQL: SELECT * FROM users WHERE tenant_id = ?
// bound: ["acme"]
Tenant-scope semantics
The wrapper rewrites SQL based on the leading keyword (case-insensitive):
| Shape | Rewrite |
|---|---|
SELECT ... [WHERE ...] |
append WHERE/AND tenant_id = ? before any ORDER BY / LIMIT / RETURNING / GROUP BY tail |
UPDATE ... [WHERE ...] |
same |
DELETE ... [WHERE ...] |
same |
INSERT INTO t (cols) VALUES (vals) w/o tenant_id and autoInjectInserts: true |
inject column + value |
same w/o tenant_id and autoInjectInserts: false |
throw TENANT_SCOPE_VIOLATION |
INSERT ... w/ conflicting tenant_id (literal or bound) |
throw TENANT_SCOPE_VIOLATION |
| anything else (DDL, INSERT...SELECT, multi-row VALUES, record-shaped params) | throw TENANT_SCOPE_VIOLATION — use a migration or db.unsafe |
db.unsafe escape hatch
createUnsafeGate({ plugin, capabilities, admin }) returns an
UnsafeQueryApi (query, execute). The capability check is array
membership on db.unsafe-queries; absent → throw
DB_ADAPTER.UNSAFE_NOT_PERMITTED. Permitted calls increment
plugin.unsafe_query_total{plugin}.
Migration directory layout
Files are read from migrationsDir and applied lexicographically. The
recommended naming convention is NNN_<short-description>.sql:
db/migrations/
001_init.sql
002_add_invoices.sql
003_index_invoices_on_org.sql
The runner records each applied file in __plugin_migrations(plugin, version, applied_at) and is idempotent — re-running with no new files is
a no-op.
Error codes
DB_ADAPTER.TENANT_SCOPE_VIOLATION— scoped wrapper saw conflicting tenant value, DDL, or unsupported SQL shape.DB_ADAPTER.UNSAFE_NOT_PERMITTED—db.unsafe.*invoked withoutdb.unsafe-queriescapability.DB_ADAPTER.MIGRATION_FAILED— migration body threw; rolled back where supported.DB_ADAPTER.ENGINE_UNSUPPORTED—enginevalue has no registered factory.DB_ADAPTER.INVALID_OPTIONS— Zod validation failed; issues attached toerror.context.issues.