2.6 KiB
2.6 KiB
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
MembershipStoresurface: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 becomeCREATE_FAILED/DELETE_FAILED. getContactIdsByGroups([])short-circuits to avoid a useless database round-trip.
Usage: Basic
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
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
addinspects the thrown error's message for the substringsunique,UNIQUE, orduplicateto classify a unique-index violation. SurrealDB's exact message format has drifted between versions; the substring approach survives the most common variants.existsusescount() AS count … GROUP ALLrather thanSELECT … LIMIT 1becausecountis consistently zero-or-one in SurrealDB and avoids deserialising the row.mapMembershippreserves the originalmetadatafield shape —undefinedstaysundefinedrather than being coerced to{}, so callers can distinguish "no metadata" from "empty metadata".