first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
# 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 `GroupStore` surface: `generateId`, `create`, `getById`,
`update`, `delete`, `list`, `getChildren`, `getDescendants`,
`getRoots`, `findByIds`.
- Domain-friendly `createGroup` that computes `path` and `depth` from
the optional parent, plus a low-level `create` that takes a
pre-built `Group`.
- `moveGroup(id, newParentId, tenantId)` — reparent with depth +
path recomputation and depth-cap enforcement.
- Bare ids (`g1`) are auto-prefixed with `contact_group:`.
- Configurable `maxGroupDepth` + `enforceGroupDepth` via constructor
options.
## Usage: Basic
```ts
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
```ts
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 (`CREATE` then `UPDATE path`) is used because
SurrealDB assigns the record id at `CREATE` time, and the
materialised path needs the actual id. The temporary id is stripped
in the follow-up patch.
- `getDescendants` uses `string::contains(path, $path) AND path !=
$path` so the root itself is not included in the result set.
- Constructor option `enforceGroupDepth` defaults to `false` so the
bare-class behaviour matches the universal `InMemoryGroupStore`
in `@suaveplan/contacts`; the plugin factory sets it to `true` by
default to enforce the documented config.
- `pickFirst` raises `CREATE_FAILED` when 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.