3.3 KiB
tenancy
(no submodules)
Purpose
Exports the provisionTenant and decommissionTenant callables that downstream hosts (typically @suaveplan/api-plugin-tenant) invoke on their own tenant-lifecycle events. The functions live as plain async exports rather than as descriptor hooks because the PluginDescriptor has no tenant-lifecycle hook slots — placing tenant logic on the plugin descriptor was the upstream's choice; suaveplan routes it through the host event bus instead.
Both functions accept any DatabaseAdapter so they remain unit-testable against an in-process double, and both validate tenantId against the strict /^[A-Za-z0-9_-]{1,64}$/ pattern before issuing any query so a hostile or malformed id cannot land in a SurrealQL DEFINE NAMESPACE statement.
Features
- Three strategies:
shared,namespace-per-tenant,database-per-tenant. - Strict
tenantIdvalidation; invalid input surfacesDATABASE.TENANT_INVALID. - Wraps adapter failures as
DATABASE.QUERY_FAILEDwith the strategy and tenant id incontext. - Idempotent: every DDL uses
IF NOT EXISTSso repeated provision calls are safe;REMOVEandDELETEare idempotent on missing rows in SurrealDB.
Usage: Basic
import { provisionTenant, decommissionTenant } from "@suaveplan/api-plugin-database/tenancy/tenancy";
await provisionTenant(db, { strategy: "namespace-per-tenant" }, "acme");
// ...later
await decommissionTenant(db, { strategy: "namespace-per-tenant" }, "acme");
Usage: Advanced
import {
provisionTenant,
decommissionTenant,
} from "@suaveplan/api-plugin-database";
eventBus.on("tenant:created", async ({ tenantId }) => {
const db = ctx.getService("db");
if (db === undefined) return;
await provisionTenant(db, config.tenancy, tenantId);
});
eventBus.on("tenant:deleted", async ({ tenantId }) => {
const db = ctx.getService("db");
if (db === undefined) return;
await decommissionTenant(db, config.tenancy, tenantId);
});
API Reference
provisionTenant(adapter, config, tenantId): Promise<void>
Issues the strategy-specific DEFINE … query against adapter. Throws DatabaseError(DATABASE.TENANT_INVALID) on bad ids; rejects with DatabaseError(DATABASE.QUERY_FAILED) on adapter failures.
decommissionTenant(adapter, config, tenantId): Promise<void>
Mirror of provisionTenant — issues the strategy-specific REMOVE … (or bounded DELETE …) query.
Implementation Notes
- The functions issue queries through
adapter.execute(sql)with no parameters, because SurrealDB'sDEFINE NAMESPACEandREMOVE NAMESPACEdo not accept bind variables for the namespace name. The tenant id is validated against the regex before being interpolated into the SQL — there is no path by which an unvalidated value reaches the wire. - The
sharedstrategy assumes a singletenant_scopedconvention table; consumers that name their tenant table differently should re-implement the decommission flow with their own table list. Issuing a wildcardDELETE FROM *(as upstream did) is intentionally NOT supported because it would also wipe non-tenant rows. - The functions are exported individually so callers can wire only the half they need (e.g. provision on signup, but decommission via a soft-delete background job).