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,120 @@
# `repositories/suppression`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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 `rowToEntry` from
`./suppression-internals.ts`.
- Application-layer duplicate detection — `add` issues a `check` before
the INSERT and throws `CAMPAIGNS_API.DUPLICATE_SUPPRESSION` if the
triple already exists.
- Zod-guarded input boundaries (`CreateInputSchema`, `QuerySchema`) so
invalid HTTP payloads fail with `CAMPAIGNS_API.INVALID_INPUT` before
any SurQL is issued.
- Errors wrapped with stable `CAMPAIGNS_API.*` codes (`DB_UNAVAILABLE`,
`INVALID_INPUT`, `TENANT_REQUIRED`, `DUPLICATE_SUPPRESSION`).
## Usage: Basic
```ts
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
```ts
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 CampaignSuppressionStore`
- `new 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.