# `repositories/campaign` _(no submodules)_ ## 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(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` 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.