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

2.9 KiB

tenant-scoped

(no submodules)

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

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.

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).