first commit
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# `repositories/campaign`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns the SurrealDB-backed implementation of the campaign
|
||||
persistence contract for `@suaveplan/api-plugin-campaigns`. It implements
|
||||
the `CampaignStore` interface exported by `@suaveplan/campaigns` so the
|
||||
in-process `CampaignEngine` and the HTTP route layer (Chunk C) can both
|
||||
read and write campaign records through a single, swappable port.
|
||||
|
||||
The repository follows the suaveplan SurrealDB idiom established by
|
||||
`@suaveplan/delivery-tracking`: every database call is a single
|
||||
`SurrealAdapter.query<T>(sql, params)` invocation using positional
|
||||
`$p0`/`$p1`/... placeholders, the SQL is auditable at a glance, and every
|
||||
adapter exception is wrapped in a `CampaignsApiError` with a stable
|
||||
`CAMPAIGNS_API.*` code so callers can branch on string equality rather
|
||||
than inspecting driver internals.
|
||||
|
||||
## Features
|
||||
|
||||
- Positional SurQL parameter binding (`$p0`, `$p1`, ...) — no named-var
|
||||
drift, no driver-specific quirks bleeding into our error messages.
|
||||
- Snake-case column ↔ camelCase property mapping handled in a single
|
||||
`rowToCampaign` helper so the column vocabulary lives in exactly one
|
||||
place.
|
||||
- Zod guards (`IdSchema`, `CampaignQuerySchema`) at the repository boundary
|
||||
— invalid input throws `INVALID_INPUT` before any SurQL is issued.
|
||||
- Idiomatic `update()` short-circuit: an empty `UpdateCampaignInput` skips
|
||||
the write and re-reads via `getById`, mirroring the upstream behaviour
|
||||
but with proper `CAMPAIGN_NOT_FOUND` semantics.
|
||||
- `InMemoryCampaignStore` re-export so consumers always have a single
|
||||
import surface regardless of backend choice.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { SurrealCampaignRepository } from "@suaveplan/api-plugin-campaigns/repositories/campaign/campaign";
|
||||
import type { SurrealAdapter } from "@suaveplan/surrealdb";
|
||||
|
||||
declare const db: SurrealAdapter;
|
||||
const repo = new SurrealCampaignRepository({ db, clock: () => Date.now() });
|
||||
const campaign = await repo.getById("c-123");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { SurrealCampaignRepository } from "@suaveplan/api-plugin-campaigns/repositories/campaign/campaign";
|
||||
import { CAMPAIGNS_API_CODES, CampaignsApiError } from "@suaveplan/api-plugin-campaigns";
|
||||
|
||||
const repo = new SurrealCampaignRepository({
|
||||
db,
|
||||
clock: () => Date.now(),
|
||||
table: "campaign_v2", // override for blue/green deploys
|
||||
});
|
||||
|
||||
try {
|
||||
await repo.update("c-123", { name: "Renamed" });
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof CampaignsApiError &&
|
||||
err.code === CAMPAIGNS_API_CODES.CAMPAIGN_NOT_FOUND
|
||||
) {
|
||||
// ... return a 404 from the HTTP layer
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface SurrealCampaignRepositoryOptions` — constructor bundle:
|
||||
`db`, `clock`, optional `table`, optional `logger`.
|
||||
- `class SurrealCampaignRepository implements CampaignStore` — exposes
|
||||
`create`, `getById`, `update`, `delete`, `list`, `updateStats`,
|
||||
`updateStatus`. Every method either returns a domain `Campaign` (or
|
||||
`Campaign[]`, `boolean`, `null`) or throws `CampaignsApiError`.
|
||||
- `InMemoryCampaignStore` — re-export from `@suaveplan/campaigns` for
|
||||
tests and prototype workloads.
|
||||
- `interface CampaignRow` — SurrealDB row shape consumed by
|
||||
`rowToCampaign`; exported from `./campaign.types.ts` for tests.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `update` short-circuit (no fields → `getById`) mirrors the upstream
|
||||
contract but raises `CAMPAIGN_NOT_FOUND` rather than a generic `Error`
|
||||
when the record is missing. SurrealDB rejects empty `SET` clauses, so the
|
||||
short-circuit is necessary to keep the public API ergonomic.
|
||||
|
||||
Optional fields land on the wire as explicit `null` rather than
|
||||
`undefined` because SurrealDB treats absent keys as "leave alone" on
|
||||
`MERGE`, which would surprise callers using the `Partial<CampaignStats>`
|
||||
input. Returning rows are normalised back to `undefined` in
|
||||
`rowToCampaign` so the in-memory and SurrealDB stores are
|
||||
indistinguishable from above.
|
||||
|
||||
`Zod` validation is intentionally narrow — only `id` and the listing
|
||||
`CampaignQuery` are validated. Full `Campaign` shape validation belongs in
|
||||
the route layer (Chunk C); the repository trusts callers to honour the
|
||||
upstream type contract while still defending against the two boundary
|
||||
shapes that come from untrusted HTTP input.
|
||||
Reference in New Issue
Block a user