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
rowToCampaignhelper so the column vocabulary lives in exactly one place. - Zod guards (
IdSchema,CampaignQuerySchema) at the repository boundary — invalid input throwsINVALID_INPUTbefore any SurQL is issued. - Idiomatic
update()short-circuit: an emptyUpdateCampaignInputskips the write and re-reads viagetById, mirroring the upstream behaviour but with properCAMPAIGN_NOT_FOUNDsemantics. InMemoryCampaignStorere-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, optionaltable, optionallogger.class SurrealCampaignRepository implements CampaignStore— exposescreate,getById,update,delete,list,updateStats,updateStatus. Every method either returns a domainCampaign(orCampaign[],boolean,null) or throwsCampaignsApiError.InMemoryCampaignStore— re-export from@suaveplan/campaignsfor tests and prototype workloads.interface CampaignRow— SurrealDB row shape consumed byrowToCampaign; exported from./campaign.types.tsfor 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.