Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
# repositories/contact-repository — SurrealDB-backed ContactStore
## Purpose
Concrete implementation of the universal `ContactStore` interface
from `@suaveplan/contacts`, backed by a connected
`SurrealAdapter`. Wraps every adapter call in `try/catch` blocks that
re-throw failures as `ContactsApiError` with the appropriate
`CREATE_FAILED` / `UPDATE_FAILED` / `DELETE_FAILED` code so callers can
discriminate without sniffing the underlying SurrealDB driver's
exception text.
The repository is **stateless aside from the adapter handle** — the
adapter's lifecycle (connect, disconnect, namespace selection) is the
host's responsibility. Multiple repositories may share one adapter.
## Features
- Full `ContactStore` surface: `create`, `getById`, `update`, `delete`,
`list` (search / status / channelType / tags / paging), `findByChannelValue`,
`findByIds`, `countByTenant`.
- Channel-typed list filter — `channels[WHERE type = $type]` uses
Surreal's array projection so filtering is index-pushable.
- Date hydration — string `createdAt` / `updatedAt` values returned by
SurrealDB are parsed into native `Date` instances.
- Bare ids (`abc`) are auto-prefixed with `contact:` so callers can pass
either form.
- Failures are wrapped in the typed `ContactsApiError` so HTTP handlers
branch on `error.code`.
## Usage: Basic
```ts
import { SurrealAdapter } from "@suaveplan/surrealdb";
import { SurrealContactRepository } from "@suaveplan/api-plugin-contacts";
const adapter = new SurrealAdapter({ url, namespace, database });
await adapter.connect();
const repo = new SurrealContactRepository(adapter);
const c = await repo.create({
tenantId: "tenant-1",
displayName: "Ada Lovelace",
channels: [
{ type: "email", value: "ada@example.com", primary: true, verified: false },
],
});
```
## Usage: Advanced
```ts
import { SurrealContactRepository } from "@suaveplan/api-plugin-contacts";
const repo = new SurrealContactRepository(adapter);
// Paginated list with channelType filter:
const page = await repo.list({
tenantId: "tenant-1",
channelType: "email",
search: "@example.com",
limit: 50,
offset: 0,
});
// Bulk fetch by ids:
const byIds = await repo.findByIds(page.contacts.map((c) => c.id));
```
## API Reference
- `class SurrealContactRepository implements ContactStore` — constructor
`(db: SurrealAdapter)`. Public methods:
- `create(input: CreateContactInput): Promise<Contact>`
- `getById(id: string): Promise<Contact | null>`
- `update(id: string, input: UpdateContactInput): Promise<Contact>`
- `delete(id: string): Promise<void>`
- `list(query: ContactQuery): Promise<ContactListResult>`
- `findByChannelValue(tenantId, channelType, channelValue): Promise<Contact | null>`
- `findByIds(ids: readonly string[]): Promise<Contact[]>`
- `countByTenant(tenantId: string): Promise<number>`
## Implementation Notes
- `list` builds the WHERE clause with positional `$name` parameters and
passes them to `SurrealAdapter.query` for prepared-statement style
binding — no string interpolation of user input.
- `findByIds([])` short-circuits with an empty result and never touches
the adapter so callers don't pay for a SurrealDB round-trip on an
empty input.
- `mapContact` strips `null` first/last names before they hit the
returned `Contact` (the type marks them optional), matching the
universal `InMemoryContactStore` shape exactly.