/** * The tenant tree in the store: the parent edge on the tenants table and the * closure table beside it (`0000_vk_tenancy_tree.sql`), read and maintained here. * * Two structures, one owner. `parent_id` is the fact; `vk_tenant_closure` is the * fact unrolled — every ancestor→descendant path with its depth, the self-row * at 0 — so the questions a request asks ("the tenants under X", "the ancestors * of Y") are one indexed read each. Both are written **only** through * {@link TenantTree.setParent} / {@link TenantTree.rebuildClosure}, which * rebuild the closure wholesale for the tenant and its whole subtree: the * graphs are small and shallow, and wholesale is easier to prove right than * incremental edits. No trigger does it — a rule in a trigger runs where the * tests do not look. * * The table names are configurable ({@link TenantTreeTables}) because a service * that only *projects* tenants from another system keeps the same two structures * under its own names (a `tenant_ref` with a `parent_id`, a closure beside it) * and wants the same maintenance code rather than a copy. * * The hierarchy *rules* — what a role in a parent is worth in a child — are * `./reach.ts`, pure; {@link TenantTree.reach} and {@link TenantTree.roleIn} are * those rules fed from this store. * * @example * ```ts * const tree = createTenantTree(); // vk_tenants / vk_tenant_closure * await tree.setParent(query, schoolId, groupId); // refuses a cycle * const sites = await tree.reach(query, unpackTenantRoles(claims['custom:tenantRoles']), { inherits: isSystemRole }); * ``` */ import type { HeldTenantRole, HierarchyOptions, TenantDescendant, TenantMembership } from './reach.js'; /** * Any querier that runs SQL with positional parameters and resolves to the rows — * `@venturekit/data`'s `query`, a transaction's `query`, a `pg` client wrapped. * Row columns are read under the aliases the statements give them, so a client * that maps snake_case to camelCase and one that does not both work. */ export type TreeQuerier = (sql: string, params?: unknown[]) => Promise; /** Where the tree lives. Defaults are the package's own tables. */ export interface TenantTreeTables { /** The tenants table; must have `id` and the parent column. */ tenants: string; /** The parent edge column on {@link tenants}. */ parentColumn: string; /** The closure table: `ancestor_id`, `descendant_id`, `depth`. */ closure: string; } export declare const VK_TENANT_TREE: TenantTreeTables; /** How deep a tree may go — a guard against a runaway walk, not a product limit anyone should meet. */ export declare const TENANT_TREE_MAX_DEPTH = 16; /** A tenant's place relative to another: how many hops away. */ export interface TenantAncestor { tenantId: string; /** Hops up from the tenant asked about: the parent is `1`. */ depth: number; } export interface TenantTree { readonly tables: TenantTreeTables; /** * Make `parentId` the parent of `tenantId` (or `null` to make it a root) and * rebuild the closure for the tenant and everything under it. Refuses a tenant * as its own ancestor ({@link TenantTreeCycleError}) and a tree deeper than * {@link TENANT_TREE_MAX_DEPTH} ({@link TenantTreeDepthError}). Idempotent for * an unchanged parent — safe to call right after inserting the tenant row. */ setParent(q: TreeQuerier, tenantId: string, parentId: string | null): Promise; /** * Rewrite the closure rows of `tenantId` and its whole subtree from the parent * column as it stands — for a row whose parent was written by other means (a * projection, a backfill). What `setParent` calls after writing the edge. */ rebuildClosure(q: TreeQuerier, tenantId: string): Promise; /** The ancestors of a tenant, nearest first; empty for a root. */ ancestorsOf(q: TreeQuerier, tenantId: string): Promise; /** Every tenant under a tenant, shallowest first; empty for a leaf. */ descendantsOf(q: TreeQuerier, tenantId: string): Promise; /** The descendants of several tenants in one read, keyed by the tenant asked about (absent = none). */ descendantsOfAll(q: TreeQuerier, tenantIds: readonly string[]): Promise>; /** `reachableTenants` over this tree: every tenant a caller with these memberships may act as. One read, for the inheriting memberships only. */ reach(q: TreeQuerier, memberships: ReadonlyMap | readonly TenantMembership[], options?: HierarchyOptions): Promise; /** `inheritedTenantRole` over this tree: the role held in `tenantId`, or `null`. The store is read only when the memberships alone do not settle it. */ roleIn(q: TreeQuerier, memberships: ReadonlyMap | readonly TenantMembership[], tenantId: string, options?: HierarchyOptions): Promise; } export declare class TenantTreeCycleError extends Error { constructor(tenantId: string, parentId: string); } export declare class TenantTreeDepthError extends Error { constructor(tenantId: string, depth: number); } export declare function createTenantTree(tables?: Partial): TenantTree; //# sourceMappingURL=tree.d.ts.map