Files
skills/docs/commerce/contacts/api-plugin-contacts/src/repositories/membership-repository.md
2026-05-29 08:20:56 +01:00

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 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

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

  • 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".