Files
skills/docs/commerce/contacts/audience-rules/src/segment-evaluator/segment-evaluator.md
2026-05-29 08:20:56 +01:00

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 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

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 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.