first commit
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# segment-evaluator
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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 `logicalOp` on each subsequent rule (default
|
||||
`"and"`; first rule's `logicalOp` is ignored).
|
||||
- Dot-notation field resolution with numeric array index access.
|
||||
- Empty-rules segment matches every contact.
|
||||
- `evaluateRule` exposed for ad-hoc rule testing against a single contact.
|
||||
- Unknown operator raises `AudienceRulesError` with
|
||||
`AUDIENCE_RULES.UNKNOWN_OPERATOR` — the discriminated `SegmentOperator`
|
||||
union makes this branch reachable only via a deliberate cast.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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 `AudienceRulesError` with `AUDIENCE_RULES.UNKNOWN_OPERATOR` when an
|
||||
operator outside the closed `SegmentOperator` union is supplied.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `resolveField` returns `undefined` for any missing intermediate path
|
||||
segment, so `not_exists` correctly 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 helpers `lastActivityBefore`
|
||||
/ `lastActivityAfter` rely on.
|
||||
- `matchesRules` is 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 is `O(rules * contacts)`.
|
||||
- The evaluator is stateless past the injected provider, so the same instance
|
||||
can serve concurrent segment evaluations safely.
|
||||
Reference in New Issue
Block a user