first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user