Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
# `tenant-scoped`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module exposes `wrapTenantScoped(adminProxy, tenantId)` — a
worker-side type-level facade that re-presents an admin-surface
`AdminDatabaseLike` as a `TenantScopedDatabaseLike`. The facade
matters because the two surface types are deliberately
non-assignable in `@suaveplan/api-types`: a request handler declared
to accept `TenantScopedDatabaseLike` MUST NOT accidentally receive
an unscoped admin proxy, and vice versa.
**Tenant scoping is enforced on the main side.** The actual SQL
rewrite / row-level security / connection routing all live in the
host's adapter. This wrapper does NOT add policy; it asserts a
shape so the type system can guarantee the slot in the request
context has been narrowed to a tenant.
## Features
- Returns a fresh facade object on every call so the same admin
proxy can back many concurrent request scopes.
- Exposes `tenantId` on the returned object for audit logging.
- Preserves the optional `prepare` / `transaction` capabilities only
if the admin proxy advertised them; absent capabilities throw
`DB_PROXY.UNSAFE_NOT_PERMITTED`.
- `close` is intentionally omitted because
`TenantScopedDatabaseLike` does not expose it — the admin proxy
retains lifecycle ownership.
## Usage: Basic
```ts
import { wrapTenantScoped } from "@suaveplan/api-db-worker-proxy/tenant-scoped";
import type { TenantId } from "@suaveplan/api-types/tenant";
const tenantId = "acme" as TenantId;
const scoped = wrapTenantScoped(adminProxy, tenantId);
const rows = await scoped.query("SELECT * FROM invoices");
```
## Usage: Advanced
Use the facade inside a request middleware that materialises
`RequestContext.db`. The middleware constructs the wrapper from the
already-resolved `tenantId` so route handlers only ever see the
tenant-scoped type.
```ts
function attachDb(adminProxy: AdminDatabaseLike) {
return async (c: Context, next: Next): Promise<void> => {
const tenantId = c.get("tenantId") as TenantId;
const scoped = wrapTenantScoped(adminProxy, tenantId);
c.set("db", scoped);
await next();
};
}
```
## API Reference
- `wrapTenantScoped(adminProxy: AdminDatabaseLike, tenantId: TenantId): TenantScopedDatabaseLike`.
- Throws `DB_PROXY.UNSAFE_NOT_PERMITTED` from `prepare` / `transaction`
when the underlying admin proxy doesn't expose that capability.
## Implementation Notes
The wrapper rebinds `prepare` and `transaction` so the underlying
admin proxy's `this`-context survives the indirection (the methods
are looked up off the proxy each call, not captured eagerly, so
admin-side disposal is observed on the next call rather than at wrap
time).
The `tenantId` field is set on the returned object purely so audit
loggers and `RequestContext` consumers can read it directly. The
field name and type match `TenantScopedDatabaseLike.tenantId`
exactly; both are `TenantId` (a branded string).