first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

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