@legalplace/conditions-runner 2.2.20 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/ComparatorResolver.d.ts +22 -0
- package/dist/ComparatorResolver.js +180 -0
- package/dist/ConditionsRunner.d.ts +1 -0
- package/dist/ConditionsRunner.js +23 -1
- package/dist/ConditionsRunner.test.js +281 -0
- package/dist/DataPopulator.js +3 -3
- package/dist/ScopedLeafEvaluator.d.ts +3 -0
- package/dist/ScopedLeafEvaluator.js +106 -0
- package/package.json +6 -6
- package/src/ComparatorResolver.ts +338 -0
- package/src/ConditionsRunner.test.ts +361 -1
- package/src/ConditionsRunner.ts +62 -1
- package/src/DataPopulator.ts +4 -4
- package/src/ScopedLeafEvaluator.ts +236 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import LpLogic from "@legalplace/lplogic";
|
|
2
|
+
import type { ConditionData } from "./types/conditions.d";
|
|
3
|
+
import {
|
|
4
|
+
areVariablesSiblings,
|
|
5
|
+
extractLhsVariableId,
|
|
6
|
+
extractVarTagIds,
|
|
7
|
+
getConditionVariableOptionId,
|
|
8
|
+
resolveComparatorString,
|
|
9
|
+
SCOPED_OPERATOR_REGEX,
|
|
10
|
+
type ComparatorResolverContext,
|
|
11
|
+
type ComparatorResolverDeps,
|
|
12
|
+
} from "./ComparatorResolver";
|
|
13
|
+
|
|
14
|
+
const UI_METADATA_KEYS = new Set(["scope", "groupMeta"]);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether the comparator references a same-multiple sibling of the LHS variable.
|
|
18
|
+
*/
|
|
19
|
+
const hasSiblingComparatorTag = (
|
|
20
|
+
deps: ComparatorResolverDeps,
|
|
21
|
+
comparator: string,
|
|
22
|
+
lhsVariableId: number
|
|
23
|
+
): boolean =>
|
|
24
|
+
extractVarTagIds(comparator).some((variableId) =>
|
|
25
|
+
areVariablesSiblings(deps.references, variableId, lhsVariableId)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether this leaf must be evaluated pairwise (one RHS index per LHS row).
|
|
30
|
+
*/
|
|
31
|
+
const shouldEvaluatePairwiseSiblingLeaf = (
|
|
32
|
+
operatorKey: string,
|
|
33
|
+
args: unknown[],
|
|
34
|
+
context: ComparatorResolverContext,
|
|
35
|
+
deps: ComparatorResolverDeps
|
|
36
|
+
): boolean => {
|
|
37
|
+
const comparator = args[1];
|
|
38
|
+
if (typeof comparator !== "string") return false;
|
|
39
|
+
|
|
40
|
+
const lhsVariableId = extractLhsVariableId(args[0]);
|
|
41
|
+
if (lhsVariableId === undefined) return false;
|
|
42
|
+
if (!hasSiblingComparatorTag(deps, comparator, lhsVariableId)) return false;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
context.conditionVariableId === undefined ||
|
|
46
|
+
SCOPED_OPERATOR_REGEX.test(operatorKey)
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Builds leaf context bound to a specific LHS variable occurrence.
|
|
52
|
+
*/
|
|
53
|
+
const withLhsLeafContext = (
|
|
54
|
+
context: ComparatorResolverContext,
|
|
55
|
+
deps: ComparatorResolverDeps,
|
|
56
|
+
lhsVariableId: number,
|
|
57
|
+
index: number
|
|
58
|
+
): ComparatorResolverContext => ({
|
|
59
|
+
...context,
|
|
60
|
+
index,
|
|
61
|
+
evalVariableId: lhsVariableId,
|
|
62
|
+
optionId: getConditionVariableOptionId(deps.references, lhsVariableId),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Evaluates one occurrence of a comparison leaf.
|
|
67
|
+
*/
|
|
68
|
+
const evaluateRowComparison = (
|
|
69
|
+
baseOperator: string,
|
|
70
|
+
lhs: unknown,
|
|
71
|
+
comparator: string,
|
|
72
|
+
lhsVariableId: number,
|
|
73
|
+
lhsValue: string | number,
|
|
74
|
+
leafContext: ComparatorResolverContext,
|
|
75
|
+
deps: ComparatorResolverDeps
|
|
76
|
+
): boolean => {
|
|
77
|
+
const resolvedComparator = resolveComparatorString(
|
|
78
|
+
comparator,
|
|
79
|
+
leafContext,
|
|
80
|
+
deps
|
|
81
|
+
);
|
|
82
|
+
const leafData: ConditionData = {
|
|
83
|
+
o: {},
|
|
84
|
+
v: { [lhsVariableId]: [lhsValue] },
|
|
85
|
+
c: { o: {}, s: {}, v: {} },
|
|
86
|
+
};
|
|
87
|
+
return Boolean(
|
|
88
|
+
LpLogic({ [baseOperator]: [lhs, resolvedComparator] }, leafData)
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Evaluates a sibling `[var:N]` leaf pairwise across occurrences.
|
|
94
|
+
*/
|
|
95
|
+
const evaluatePairwiseSiblingLeaf = (
|
|
96
|
+
operatorKey: string,
|
|
97
|
+
args: unknown[],
|
|
98
|
+
context: ComparatorResolverContext,
|
|
99
|
+
deps: ComparatorResolverDeps
|
|
100
|
+
): boolean => {
|
|
101
|
+
const scopeMatch = operatorKey.match(SCOPED_OPERATOR_REGEX);
|
|
102
|
+
const scope = scopeMatch === null ? "all" : scopeMatch[1];
|
|
103
|
+
const baseOperator = operatorKey.replace(SCOPED_OPERATOR_REGEX, "");
|
|
104
|
+
const lhsVariableId = extractLhsVariableId(args[0]);
|
|
105
|
+
if (lhsVariableId === undefined) return false;
|
|
106
|
+
|
|
107
|
+
const lhsValues = deps.ovc.variables[lhsVariableId] ?? [];
|
|
108
|
+
if (lhsValues.length === 0) {
|
|
109
|
+
return scope !== "any" && scope !== "first";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const comparator = args[1] as string;
|
|
113
|
+
const lhs = args[0];
|
|
114
|
+
|
|
115
|
+
if (scope === "first") {
|
|
116
|
+
return evaluateRowComparison(
|
|
117
|
+
baseOperator,
|
|
118
|
+
lhs,
|
|
119
|
+
comparator,
|
|
120
|
+
lhsVariableId,
|
|
121
|
+
lhsValues[0] as string | number,
|
|
122
|
+
withLhsLeafContext(context, deps, lhsVariableId, 0),
|
|
123
|
+
deps
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (scope === "any") {
|
|
128
|
+
return lhsValues.some((lhsValue, index) =>
|
|
129
|
+
evaluateRowComparison(
|
|
130
|
+
baseOperator,
|
|
131
|
+
lhs,
|
|
132
|
+
comparator,
|
|
133
|
+
lhsVariableId,
|
|
134
|
+
lhsValue as string | number,
|
|
135
|
+
withLhsLeafContext(context, deps, lhsVariableId, index),
|
|
136
|
+
deps
|
|
137
|
+
)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return lhsValues.every((lhsValue, index) =>
|
|
142
|
+
evaluateRowComparison(
|
|
143
|
+
baseOperator,
|
|
144
|
+
lhs,
|
|
145
|
+
comparator,
|
|
146
|
+
lhsVariableId,
|
|
147
|
+
lhsValue as string | number,
|
|
148
|
+
withLhsLeafContext(context, deps, lhsVariableId, index),
|
|
149
|
+
deps
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Evaluates a scoped `-first` / `-any` leaf (non-pairwise) against unsliced data.
|
|
156
|
+
*/
|
|
157
|
+
const evaluateScopedLeaf = (
|
|
158
|
+
operatorKey: string,
|
|
159
|
+
args: unknown[],
|
|
160
|
+
unslicedData: ConditionData,
|
|
161
|
+
context: ComparatorResolverContext,
|
|
162
|
+
deps: ComparatorResolverDeps
|
|
163
|
+
): boolean => {
|
|
164
|
+
const lhsVariableId = extractLhsVariableId(args[0]);
|
|
165
|
+
const comparator = args[1];
|
|
166
|
+
const leafContext =
|
|
167
|
+
lhsVariableId === undefined
|
|
168
|
+
? { ...context, index: 0 }
|
|
169
|
+
: withLhsLeafContext(context, deps, lhsVariableId, 0);
|
|
170
|
+
|
|
171
|
+
const resolvedArgs = [...args];
|
|
172
|
+
if (typeof comparator === "string") {
|
|
173
|
+
resolvedArgs[1] = resolveComparatorString(comparator, leafContext, deps);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return Boolean(LpLogic({ [operatorKey]: resolvedArgs }, unslicedData));
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Replaces `-first` / `-any` leaves and pairwise sibling `[var:N]` leaves with booleans.
|
|
181
|
+
*
|
|
182
|
+
* DataPopulator slices same-multiple rows for `related`; scoped operators must
|
|
183
|
+
* still aggregate across every occurrence. Sibling comparators on `all` / `any`
|
|
184
|
+
* / `first` must compare matching indexes pairwise.
|
|
185
|
+
*/
|
|
186
|
+
export const aggregateScopedLeaves = (
|
|
187
|
+
tree: unknown,
|
|
188
|
+
unslicedData: ConditionData,
|
|
189
|
+
context: ComparatorResolverContext,
|
|
190
|
+
deps: ComparatorResolverDeps
|
|
191
|
+
): unknown => {
|
|
192
|
+
if (tree === null || tree === undefined) return tree;
|
|
193
|
+
|
|
194
|
+
if (Array.isArray(tree)) {
|
|
195
|
+
return tree.map((child) =>
|
|
196
|
+
aggregateScopedLeaves(child, unslicedData, context, deps)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (typeof tree !== "object") return tree;
|
|
201
|
+
|
|
202
|
+
const node = tree as Record<string, unknown>;
|
|
203
|
+
const structuralKeys = Object.keys(node).filter(
|
|
204
|
+
(key) => !UI_METADATA_KEYS.has(key)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (structuralKeys.length === 0) return node;
|
|
208
|
+
|
|
209
|
+
const operatorKey = structuralKeys[0];
|
|
210
|
+
|
|
211
|
+
if (operatorKey === "and" || operatorKey === "or") {
|
|
212
|
+
const children = node[operatorKey];
|
|
213
|
+
if (!Array.isArray(children)) return node;
|
|
214
|
+
return {
|
|
215
|
+
...node,
|
|
216
|
+
[operatorKey]: children.map((child) =>
|
|
217
|
+
aggregateScopedLeaves(child, unslicedData, context, deps)
|
|
218
|
+
),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const args = node[operatorKey];
|
|
223
|
+
if (
|
|
224
|
+
Array.isArray(args) &&
|
|
225
|
+
shouldEvaluatePairwiseSiblingLeaf(operatorKey, args, context, deps)
|
|
226
|
+
) {
|
|
227
|
+
return evaluatePairwiseSiblingLeaf(operatorKey, args, context, deps);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (SCOPED_OPERATOR_REGEX.test(operatorKey)) {
|
|
231
|
+
if (!Array.isArray(args)) return node;
|
|
232
|
+
return evaluateScopedLeaf(operatorKey, args, unslicedData, context, deps);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return node;
|
|
236
|
+
};
|