3.4 KiB
3.4 KiB
repositories/group-repository — SurrealDB-backed GroupStore
Purpose
Concrete implementation of the GroupStore interface from
@suaveplan/contacts, backed by SurrealAdapter. Implements the
hybrid adjacency-list + materialised-path strategy for hierarchical
groups: every row carries parentId, path, and depth, so subtree
queries become WHERE string::contains(path, $path) and lookups of
direct children become WHERE parentId = $parentId.
Enforces the plugin's configured maxGroupDepth on createGroup and
moveGroup when enforceGroupDepth is true — closing the upstream
gap where the config was advertised but never enforced.
Features
- Full
GroupStoresurface:generateId,create,getById,update,delete,list,getChildren,getDescendants,getRoots,findByIds. - Domain-friendly
createGroupthat computespathanddepthfrom the optional parent, plus a low-levelcreatethat takes a pre-builtGroup. moveGroup(id, newParentId, tenantId)— reparent with depth + path recomputation and depth-cap enforcement.- Bare ids (
g1) are auto-prefixed withcontact_group:. - Configurable
maxGroupDepth+enforceGroupDepthvia constructor options.
Usage: Basic
import { SurrealGroupRepository } from "@suaveplan/api-plugin-contacts";
const repo = new SurrealGroupRepository(adapter, {
maxGroupDepth: 10,
enforceGroupDepth: true,
});
const root = await repo.createGroup({ tenantId: "t", name: "All" });
const child = await repo.createGroup({
tenantId: "t",
name: "Engineering",
parentId: root.id,
});
Usage: Advanced
import { SurrealGroupRepository } from "@suaveplan/api-plugin-contacts";
const repo = new SurrealGroupRepository(adapter, {
maxGroupDepth: 5,
enforceGroupDepth: true,
idGenerator: () => crypto.randomUUID(),
});
// Reparent — throws GROUP_DEPTH_EXCEEDED if depth would exceed 5:
const moved = await repo.moveGroup(child.id, otherParentId, "t");
// Walk a subtree:
const descendants = await repo.getDescendants(root.path);
API Reference
interface SurrealGroupRepositoryOptions—{ maxGroupDepth?, enforceGroupDepth?, idGenerator? }.class SurrealGroupRepository implements GroupStore— constructor(db: SurrealAdapter, options?: SurrealGroupRepositoryOptions). Adds two domain methods on top of the interface:createGroup(input: CreateGroupInput): Promise<Group>— computes path/depth + enforces depth cap.moveGroup(id, newParentId, tenantId): Promise<Group>— reparent + depth cap.
Implementation Notes
- The two-step write (
CREATEthenUPDATE path) is used because SurrealDB assigns the record id atCREATEtime, and the materialised path needs the actual id. The temporary id is stripped in the follow-up patch. getDescendantsusesstring::contains(path, $path) AND path != $pathso the root itself is not included in the result set.- Constructor option
enforceGroupDepthdefaults tofalseso the bare-class behaviour matches the universalInMemoryGroupStorein@suaveplan/contacts; the plugin factory sets it totrueby default to enforce the documented config. pickFirstraisesCREATE_FAILEDwhen SurrealDB returns no row; this catches database-side rejections (assertion failure on a NOT NULL field, for example) that the driver swallowed into an empty array.