4.2 KiB
4.2 KiB
segment-evaluator
(no submodules)
Purpose
segment-evaluator.ts provides the pure-function rule engine that decides,
for a given Segment and a tenant's ContactData[], which contacts match.
It supports fourteen comparison operators, AND/OR logical chaining, and
dot-notation field resolution including numeric array indices (e.g.
channels.0.type). It is intentionally storage-agnostic — contact data is
supplied by an injected ContactDataProvider, so the evaluator runs equally
well against an in-memory test array, a paginated database cursor, or a
remote API.
Features
- 14 operators:
eq,neq,gt,gte,lt,lte,contains,not_contains,in,not_in,exists,not_exists,starts_with,ends_with. - AND/OR logical chaining via
logicalOpon each subsequent rule (default"and"; first rule'slogicalOpis ignored). - Dot-notation field resolution with numeric array index access.
- Empty-rules segment matches every contact.
evaluateRuleexposed for ad-hoc rule testing against a single contact.- Unknown operator raises
AudienceRulesErrorwithAUDIENCE_RULES.UNKNOWN_OPERATOR— the discriminatedSegmentOperatorunion makes this branch reachable only via a deliberate cast.
Usage: Basic Example
import { SegmentEvaluator } from "@suaveplan/audience-rules";
const provider = async () => [
{ id: "c1", tags: ["vip"], status: "active" },
{ id: "c2", tags: ["basic"], status: "active" },
];
const evaluator = new SegmentEvaluator(provider);
const result = await evaluator.evaluate({
id: "seg1",
tenantId: "t1",
name: "VIPs",
rules: [{ field: "tags", operator: "contains", value: "vip" }],
refreshMode: "manual",
createdAt: new Date(),
updatedAt: new Date(),
});
// result.contactIds === ["c1"]
Usage: Advanced Example
import { SegmentEvaluator } from "@suaveplan/audience-rules";
const provider = async () => [
{
id: "c1",
status: "active",
metadata: { country: "US", score: 90 },
channels: [{ type: "email", value: "a@b.com" }],
},
];
const evaluator = new SegmentEvaluator(provider);
// "active AND US, OR score >= 95"
const result = await evaluator.evaluate({
id: "seg",
tenantId: "t1",
name: "complex",
refreshMode: "manual",
rules: [
{ field: "status", operator: "eq", value: "active" },
{
field: "metadata.country",
operator: "eq",
value: "US",
logicalOp: "and",
},
{
field: "metadata.score",
operator: "gte",
value: 95,
logicalOp: "or",
},
],
createdAt: new Date(),
updatedAt: new Date(),
});
API Reference
type ContactDataProvider = (tenantId: string) => Promise<readonly ContactData[]>class SegmentEvaluator— constructor(provider: ContactDataProvider).evaluator.evaluate(segment: Segment): Promise<SegmentEvaluationResult>— fetches contacts via the provider, evaluates every rule against every contact, returns matching IDs +total+evaluatedAt+matchRate.evaluator.evaluateRule(contact: ContactData, rule: SegmentRule): boolean— pure helper; resolves the dot-notation field and applies the operator.- Throws
AudienceRulesErrorwithAUDIENCE_RULES.UNKNOWN_OPERATORwhen an operator outside the closedSegmentOperatorunion is supplied.
Implementation Notes
resolveFieldreturnsundefinedfor any missing intermediate path segment, sonot_existscorrectly matches deeply-nested missing fields.- Comparison operators apply unchecked casts (
fieldValue as number) — this matches the upstream behaviour and lets callers compare ISO date strings lexicographically, which is what the predicate helperslastActivityBefore/lastActivityAfterrely on. matchesRulesis left-to-right and does not short-circuit aggressively; it always evaluates every rule. This keeps the algorithm simple and deterministic; for very large rule lists the cost isO(rules * contacts).- The evaluator is stateless past the injected provider, so the same instance can serve concurrent segment evaluations safely.