# `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 `tenantId` validation; invalid input surfaces `DATABASE.TENANT_INVALID`. - Wraps adapter failures as `DATABASE.QUERY_FAILED` with the strategy and tenant id in `context`. - Idempotent: every DDL uses `IF NOT EXISTS` so repeated provision calls are safe; `REMOVE` and `DELETE` are idempotent on missing rows in SurrealDB. ## Usage: Basic ```ts 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 ```ts 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` 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` 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's `DEFINE NAMESPACE` and `REMOVE NAMESPACE` do 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 `shared` strategy assumes a single `tenant_scoped` convention table; consumers that name their tenant table differently should re-implement the decommission flow with their own table list. Issuing a wildcard `DELETE 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).