81 lines
2.4 KiB
TypeScript
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;
|
|
}
|