first commit
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# repositories/membership-repository — SurrealDB-backed MembershipStore
|
||||
|
||||
## Purpose
|
||||
|
||||
Concrete implementation of the `MembershipStore` interface
|
||||
from `@suaveplan/contacts`, backed by `SurrealAdapter`. Persists the
|
||||
many-to-many `(groupId, contactId)` join with a `UNIQUE` SurrealQL
|
||||
index that raises `MEMBERSHIP_EXISTS` on duplicate insertion.
|
||||
|
||||
The repository owns no business logic — idempotent add semantics are
|
||||
implemented by the route layer (`POST /groups/:id/members` checks
|
||||
`exists` before adding so the second call returns `{ added: 0 }`
|
||||
without surfacing a `MEMBERSHIP_EXISTS` error).
|
||||
|
||||
## Features
|
||||
|
||||
- Full `MembershipStore` surface: `generateId`, `add`, `remove`,
|
||||
`getByGroup`, `getByContact`, `exists`, `removeAllForContact`,
|
||||
`removeAllForGroup`, `getContactIdsByGroups`, `countByGroup`.
|
||||
- Sniffs SurrealDB unique-index violations and translates them into
|
||||
`ContactsApiError(MEMBERSHIP_EXISTS)`; other adapter errors become
|
||||
`CREATE_FAILED` / `DELETE_FAILED`.
|
||||
- `getContactIdsByGroups([])` short-circuits to avoid a useless
|
||||
database round-trip.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { SurrealMembershipRepository } from "@suaveplan/api-plugin-contacts";
|
||||
|
||||
const repo = new SurrealMembershipRepository(adapter);
|
||||
await repo.add({
|
||||
id: repo.generateId(),
|
||||
groupId: "contact_group:engineering",
|
||||
contactId: "contact:ada",
|
||||
addedAt: new Date(),
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { SurrealMembershipRepository } from "@suaveplan/api-plugin-contacts";
|
||||
|
||||
const repo = new SurrealMembershipRepository(adapter, {
|
||||
idGenerator: () => crypto.randomUUID(),
|
||||
});
|
||||
|
||||
const contactIds = await repo.getContactIdsByGroups([
|
||||
"contact_group:engineering",
|
||||
"contact_group:sales",
|
||||
]);
|
||||
const unique = Array.from(new Set(contactIds));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface SurrealMembershipRepositoryOptions` — `{ idGenerator? }`.
|
||||
- `class SurrealMembershipRepository implements MembershipStore` —
|
||||
constructor `(db: SurrealAdapter, options?)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `add` inspects the thrown error's message for the substrings
|
||||
`unique`, `UNIQUE`, or `duplicate` to classify a unique-index
|
||||
violation. SurrealDB's exact message format has drifted between
|
||||
versions; the substring approach survives the most common variants.
|
||||
- `exists` uses `count() AS count … GROUP ALL` rather than
|
||||
`SELECT … LIMIT 1` because `count` is consistently zero-or-one in
|
||||
SurrealDB and avoids deserialising the row.
|
||||
- `mapMembership` preserves the original `metadata` field shape —
|
||||
`undefined` stays `undefined` rather than being coerced to `{}`, so
|
||||
callers can distinguish "no metadata" from "empty metadata".
|
||||
Reference in New Issue
Block a user