129 lines
7.7 KiB
Markdown
129 lines
7.7 KiB
Markdown
# @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:
|
|
|
|
1. `options` — Zod-validated `DbAdapterOptions` (`engine`, `url`, `pool?`,
|
|
`tenantColumn`, `migrationsDir?`, `autoInjectInserts`).
|
|
2. `engine-registry` — open registry of engine factories. The package
|
|
itself ships no engines pre-installed; host code (or a downstream
|
|
adapter package) calls `registerEngine("sqlite" | "postgres" |
|
|
"surrealdb" | <merged>, factory)` once at boot. The shape keeps the
|
|
package universally importable.
|
|
3. `tenant-scoped` — the SQL rewriter that injects `WHERE tenant_id = ?`
|
|
on every SELECT/UPDATE/DELETE, verifies / auto-injects the tenant
|
|
column on INSERT, and rejects DDL or unsupported SQL with
|
|
`DB_ADAPTER.TENANT_SCOPE_VIOLATION`. Strict / lenient INSERT mode is
|
|
`autoInjectInserts` on the options.
|
|
4. `migrations` — per-engine migration runner consuming a directory of
|
|
`*.sql` files; tracks applied versions in `__plugin_migrations` and
|
|
is idempotent on re-run. Failed migrations roll back where the engine
|
|
supports transactions.
|
|
5. `unsafe` — the `db.unsafe` escape hatch, gated on the
|
|
`db.unsafe-queries` capability flag in the plugin manifest. Every
|
|
permitted call increments `plugin.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.
|
|
|
|
<!-- begin:auto-docs -->
|
|
## Documentation tree
|
|
|
|
- [engine-registry](./src/engine-registry/engine-registry.md) — The engine registry is the extension point that lets `api-db-adapter` stay universally importable while still supportin…
|
|
- [engine-registry](./src/engine-registry/engine-registry.md) — The engine registry is the extension point that lets `api-db-adapter` stay universally importable while still supportin…
|
|
- [error-codes](./src/error-codes/error-codes.md) — This module is the single source of truth for every public failure code raised by `@suaveplan/api-db-adapter`. Codes ar…
|
|
- [error-codes](./src/error-codes/error-codes.md) — This module is the single source of truth for every public failure code raised by `@suaveplan/api-db-adapter`. Codes ar…
|
|
- [factory](./src/factory/create-db-adapter.md) — `createDbAdapter` is the public entry point for host code that wants to wire the `@suaveplan/db` engine adapter…
|
|
- [create-db-adapter](./src/factory/create-db-adapter.md) — `createDbAdapter` is the public entry point for host code that wants to wire the `@suaveplan/db` engine adapter…
|
|
- [migrations](./src/migrations/run-migrations.md) — `runMigrations` applies a plugin's pending migrations from a directory of `*.sql` files, recording applied versions in…
|
|
- [run-migrations](./src/migrations/run-migrations.md) — `runMigrations` applies a plugin's pending migrations from a directory of `*.sql` files, recording applied versions in…
|
|
- [options](./src/options/db-adapter-options.md) — `DbAdapterOptions` is the single config object accepted by every public factory in this package — `createDbAdapter`, `c…
|
|
- [db-adapter-options](./src/options/db-adapter-options.md) — `DbAdapterOptions` is the single config object accepted by every public factory in this package — `createDbAdapter`, `c…
|
|
- [proxy](./src/proxy/create-db-adapter-worker-proxy.md) — `createDbAdapterWorkerProxy` is the worker-side counterpart to `createDbAdapter`. Inside an `api-worker-host` worker, p…
|
|
- [create-db-adapter-worker-proxy](./src/proxy/create-db-adapter-worker-proxy.md) — `createDbAdapterWorkerProxy` is the worker-side counterpart to `createDbAdapter`. Inside an `api-worker-host` worker, p…
|
|
- [tenant-scoped](./src/tenant-scoped/create-tenant-scoped-wrapper.md) — This module is the load-bearing safety boundary between a plugin route handler and the underlying admin database surfac…
|
|
- [create-tenant-scoped-wrapper](./src/tenant-scoped/create-tenant-scoped-wrapper.md) — This module is the load-bearing safety boundary between a plugin route handler and the underlying admin database surfac…
|
|
- [unsafe](./src/unsafe/unsafe-query-gate.md) — `createUnsafeGate` builds the `ctx.db.unsafe` API exposed on every tenant- scoped surface. The gate is the only sanctio…
|
|
- [unsafe-query-gate](./src/unsafe/unsafe-query-gate.md) — `createUnsafeGate` builds the `ctx.db.unsafe` API exposed on every tenant- scoped surface. The gate is the only sanctio…
|
|
<!-- end:auto-docs -->
|
|
|
|
## Quick start
|
|
|
|
```ts
|
|
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 without
|
|
`db.unsafe-queries` capability.
|
|
- `DB_ADAPTER.MIGRATION_FAILED` — migration body threw; rolled back where
|
|
supported.
|
|
- `DB_ADAPTER.ENGINE_UNSUPPORTED` — `engine` value has no registered
|
|
factory.
|
|
- `DB_ADAPTER.INVALID_OPTIONS` — Zod validation failed; issues attached
|
|
to `error.context.issues`.
|
|
|