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

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

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