Files
HiveGrid-Wireframes/hivegrid-cloud/packages/email-sequences/src/conditions.ts
T

81 lines
2.4 KiB
TypeScript

/**
* Condition-evaluator for the sequence step DSL.
*
* Pure function — no DB, no IO. The trigger handler builds the
* `context` payload (typically a copy of the outbox event plus
* resolved lead/tenant fields) and asks the engine whether the step
* should run.
*/
import type { SequenceStepCondition } from "./types.js";
/**
* Read a dot-path out of a JSON-like payload. Returns `undefined` when
* any intermediate segment is missing. Array-index access uses
* numeric segments (`items.0.name`).
*/
export function resolvePath(payload: Readonly<Record<string, unknown>>, path: string): unknown {
if (path.length === 0) return undefined;
const segments = path.split(".");
let cur: unknown = payload;
for (const seg of segments) {
if (cur === null || cur === undefined) return undefined;
if (Array.isArray(cur)) {
const idx = Number(seg);
if (!Number.isInteger(idx)) return undefined;
cur = cur[idx];
continue;
}
if (typeof cur === "object") {
cur = (cur as Record<string, unknown>)[seg];
continue;
}
return undefined;
}
return cur;
}
function asArray(v: SequenceStepCondition["value"]): readonly (string | number)[] {
if (Array.isArray(v)) return v;
if (v === undefined || v === null) return [];
return [v as string | number];
}
export function evaluateCondition(
payload: Readonly<Record<string, unknown>>,
cond: SequenceStepCondition
): boolean {
const actual = resolvePath(payload, cond.path);
switch (cond.op) {
case "eq":
return actual === cond.value;
case "neq":
return actual !== cond.value;
case "in":
return asArray(cond.value).includes(actual as string | number);
case "not_in":
return !asArray(cond.value).includes(actual as string | number);
case "exists":
return actual !== undefined && actual !== null;
case "not_exists":
return actual === undefined || actual === null;
default:
return false;
}
}
/**
* AND-evaluate every condition. Empty array → always true (no
* conditions means "always run").
*/
export function evaluateAllConditions(
payload: Readonly<Record<string, unknown>>,
conditions: readonly SequenceStepCondition[] | undefined
): boolean {
if (!conditions || conditions.length === 0) return true;
for (const c of conditions) {
if (!evaluateCondition(payload, c)) return false;
}
return true;
}