Files
skills/docs/api-stack/api/api-plugin-campaigns/src/repositories/campaign/campaign.md
2026-05-29 08:20:56 +01:00

4.2 KiB

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

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

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.