4.4 KiB
repositories/suppression
(no submodules)
Purpose
This module owns the SurrealDB-backed implementation of the tenant +
channel-scoped CampaignSuppressionStore interface for
@suaveplan/api-plugin-campaigns. It tracks contacts who should be
excluded from a tenant's campaign sends — for example because they
unsubscribed, hard-bounced, complained, or were manually opted out.
The local interface is intentional and not a wrapper over
@suaveplan/suppression: the suaveplan suppression package is email-only
and keyed on (tenantId, channelValue), while a campaigns plugin must
support multi-channel delivery (email, SMS, push, ...). The
(tenantId, channel, channelValue) triple is therefore the natural
primary key, with channel = "email" representing the legacy case.
Features
- Positional SurQL parameter binding (
$p0,$p1, ...) per the suaveplan Surreal idiom. - Snake-case ↔ camelCase column mapping isolated in
rowToEntryfrom./suppression-internals.ts. - Application-layer duplicate detection —
addissues acheckbefore the INSERT and throwsCAMPAIGNS_API.DUPLICATE_SUPPRESSIONif the triple already exists. - Zod-guarded input boundaries (
CreateInputSchema,QuerySchema) so invalid HTTP payloads fail withCAMPAIGNS_API.INVALID_INPUTbefore any SurQL is issued. - Errors wrapped with stable
CAMPAIGNS_API.*codes (DB_UNAVAILABLE,INVALID_INPUT,TENANT_REQUIRED,DUPLICATE_SUPPRESSION).
Usage: Basic
import { SurrealCampaignSuppressionRepository } from "@suaveplan/api-plugin-campaigns/repositories/suppression/suppression";
import type { SurrealAdapter } from "@suaveplan/surrealdb";
declare const db: SurrealAdapter;
const store = new SurrealCampaignSuppressionRepository({
db,
clock: () => Date.now(),
idGenerator: () => crypto.randomUUID(),
});
await store.add({
tenantId: "t1",
channel: "email",
channelValue: "user@example.com",
reason: "unsubscribe",
});
const blocked = await store.check("t1", "email", "user@example.com");
Usage: Advanced
import { SurrealCampaignSuppressionRepository } from "@suaveplan/api-plugin-campaigns/repositories/suppression/suppression";
import { CAMPAIGNS_API_CODES, CampaignsApiError } from "@suaveplan/api-plugin-campaigns";
const store = new SurrealCampaignSuppressionRepository({
db,
clock: () => Date.now(),
idGenerator: () => crypto.randomUUID(),
table: "campaign_suppression_v2",
});
try {
await store.add({
tenantId: "t1",
channel: "sms",
channelValue: "+15551234567",
reason: "complaint",
});
} catch (err) {
if (
err instanceof CampaignsApiError &&
err.code === CAMPAIGNS_API_CODES.DUPLICATE_SUPPRESSION
) {
// ... already suppressed, treat as success
}
}
API Reference
interface CampaignSuppressionStore— local persistence contract:add,remove,check,list,count,removeByChannelValue.class SurrealCampaignSuppressionRepository implements CampaignSuppressionStorenew SurrealCampaignSuppressionRepository({ db, clock, idGenerator, table?, logger? })
interface CampaignSuppressionEntry— domain row shape.type CampaignSuppressionReason— one of"unsubscribe","bounce","complaint","manual","compliance".const CreateInputSchema,const QuerySchema— re-exported Zod schemas for callers who want to validate at a different boundary.
Implementation Notes
The local interface is intentional and is NOT a wrapper over
@suaveplan/suppression — that package is email-only and would force a
lossy mapping. The repository uses channel as a first-class column so a
single tenant can suppress different addresses on different channels
without collision.
The Surreal add issues an application-layer check before the
INSERT. A Surreal-side UNIQUE INDEX on (tenant_id, channel, channel_value) is recommended as defence in depth, but the application
check is the primary gate — it gives us a clean
CAMPAIGNS_API.DUPLICATE_SUPPRESSION error rather than a driver-specific
constraint violation that would have to be string-matched.
Errors are wrapped via the run<T> helper, which uniformly turns
adapter exceptions into CampaignsApiError(DB_UNAVAILABLE) with the
operation name attached as context.op. This keeps the failure modes
auditable from logs without leaking driver internals.