@legalplace/conditions-runner 2.2.20 → 2.3.0
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 +17 -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/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/ScopedLeafEvaluator.ts +236 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@legalplace/conditions-runner",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Package containing method to execute conditions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/ConditionsRunner.js",
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
},
|
|
22
22
|
"prettier": "@legalplace/prettier-config",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@legalplace/lplogic": "2.3.
|
|
25
|
-
"@legalplace/models-v3-types": "^5.
|
|
26
|
-
"@legalplace/referencesparser": "^3.
|
|
24
|
+
"@legalplace/lplogic": "2.3.3",
|
|
25
|
+
"@legalplace/models-v3-types": "^5.18.0",
|
|
26
|
+
"@legalplace/referencesparser": "^3.3.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@legalplace/prettier-config": "^2.1.3",
|
|
30
|
-
"@legalplace/types": "^2.22.
|
|
30
|
+
"@legalplace/types": "^2.22.13",
|
|
31
31
|
"ts-node": "^10.8.1"
|
|
32
32
|
},
|
|
33
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "5b5eed67e9a7815f5fa77492782158fa6af0ca13"
|
|
34
34
|
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { getRecognizedOperatorKeys } from "@legalplace/referencesparser";
|
|
2
|
+
import type { Types } from "@legalplace/referencesparser";
|
|
3
|
+
import type { InputsType } from "@legalplace/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Placeholder for empty variables in condition comparators.
|
|
7
|
+
* Must not match real user input (`""` would make `contains` always true).
|
|
8
|
+
*/
|
|
9
|
+
export const CONDITION_COMPARATOR_EMPTY_SENTINEL = "\uE000__LP_EMPTY__\uE000";
|
|
10
|
+
|
|
11
|
+
const VAR_TAG_IN_TEXT_REGEX = /\[var:[0-9]+\]/i;
|
|
12
|
+
const UI_METADATA_KEYS = new Set(["scope", "groupMeta"]);
|
|
13
|
+
const RECOGNIZED_OPERATOR_KEYS = new Set(getRecognizedOperatorKeys());
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detects scoped operator suffixes (`-first`, `-any`) for occurrence-aware aggregation.
|
|
17
|
+
*/
|
|
18
|
+
export const SCOPED_OPERATOR_REGEX = /-(first|any)$/;
|
|
19
|
+
|
|
20
|
+
export type ComparatorResolverContext = {
|
|
21
|
+
optionId: number;
|
|
22
|
+
index: number;
|
|
23
|
+
/** Variable whose condition runs (sibling `[var:N]` use this row `index`). */
|
|
24
|
+
conditionVariableId?: number;
|
|
25
|
+
/** Option whose condition runs (vars on that option use this row `index`). */
|
|
26
|
+
conditionOptionId?: number;
|
|
27
|
+
/** LHS `v.N` for sibling binding. */
|
|
28
|
+
evalVariableId?: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ComparatorResolverDeps = {
|
|
32
|
+
references: Types.ReferencesType;
|
|
33
|
+
ovc: InputsType;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Immediate parent option for a variable (`parents[0]`).
|
|
38
|
+
*/
|
|
39
|
+
export const getConditionVariableOptionId = (
|
|
40
|
+
references: Types.ReferencesType,
|
|
41
|
+
variableId: number
|
|
42
|
+
): number => {
|
|
43
|
+
const parents = references.relations.variables[variableId]?.parents;
|
|
44
|
+
return parents !== undefined && parents.length > 0 ? parents[0] : variableId;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extracts variable ids referenced as `[var:N]` tags.
|
|
49
|
+
*/
|
|
50
|
+
export const extractVarTagIds = (text: string): number[] => {
|
|
51
|
+
const tagsMatch = text.match(/\[var:([0-9]+)\]/gi);
|
|
52
|
+
if (tagsMatch === null) return [];
|
|
53
|
+
return [
|
|
54
|
+
...new Set(
|
|
55
|
+
tagsMatch.map((tag) => {
|
|
56
|
+
const match = tag.match(/([0-9]+)/);
|
|
57
|
+
return match === null ? 0 : parseInt(match[0], 10);
|
|
58
|
+
})
|
|
59
|
+
),
|
|
60
|
+
];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extracts the LHS variable id from `{ var: 'v.N' }` LHS shape.
|
|
65
|
+
*/
|
|
66
|
+
export const extractLhsVariableId = (lhs: unknown): number | undefined => {
|
|
67
|
+
if (
|
|
68
|
+
typeof lhs !== "object" ||
|
|
69
|
+
lhs === null ||
|
|
70
|
+
!("var" in lhs) ||
|
|
71
|
+
typeof (lhs as { var: unknown }).var !== "string"
|
|
72
|
+
) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const match = (lhs as { var: string }).var.match(/^v\.([0-9]+)$/);
|
|
77
|
+
return match === null ? undefined : parseInt(match[1], 10);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether two variables share the same multiple root parent.
|
|
82
|
+
*/
|
|
83
|
+
export const areVariablesSiblings = (
|
|
84
|
+
references: Types.ReferencesType,
|
|
85
|
+
variableA: number,
|
|
86
|
+
variableB: number
|
|
87
|
+
): boolean => {
|
|
88
|
+
const rootA =
|
|
89
|
+
references.relations.variables[variableA]?.parents?.slice(-1)[0];
|
|
90
|
+
const rootB =
|
|
91
|
+
references.relations.variables[variableB]?.parents?.slice(-1)[0];
|
|
92
|
+
return rootA !== undefined && rootA === rootB;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether a variable value should be read at the current repeat row `index`.
|
|
97
|
+
*/
|
|
98
|
+
const shouldUseConditionRowIndex = (
|
|
99
|
+
references: Types.ReferencesType,
|
|
100
|
+
variableId: number,
|
|
101
|
+
optionId: number,
|
|
102
|
+
context?: {
|
|
103
|
+
contextVariableId?: number;
|
|
104
|
+
conditionOptionId?: number;
|
|
105
|
+
}
|
|
106
|
+
): boolean => {
|
|
107
|
+
const contextVariableId = context?.contextVariableId;
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
contextVariableId !== undefined &&
|
|
111
|
+
areVariablesSiblings(references, variableId, contextVariableId)
|
|
112
|
+
) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (context?.conditionOptionId !== undefined) {
|
|
117
|
+
const parents = references.relations.variables[variableId]?.parents;
|
|
118
|
+
if (
|
|
119
|
+
parents !== undefined &&
|
|
120
|
+
parents.length > 0 &&
|
|
121
|
+
parents[0] === context.conditionOptionId
|
|
122
|
+
) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const variableParents = references.relations.variables[variableId]?.parents;
|
|
128
|
+
if (variableParents === undefined || variableParents.length === 0) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const rootVariableParentId = variableParents[variableParents.length - 1];
|
|
133
|
+
const rootVariableParentRelations =
|
|
134
|
+
references.relations.options[rootVariableParentId];
|
|
135
|
+
if (rootVariableParentRelations === undefined) return false;
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
rootVariableParentRelations.children.options.includes(optionId) ||
|
|
139
|
+
rootVariableParentRelations.dependants.includes(optionId) ||
|
|
140
|
+
optionId === rootVariableParentId
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Normalizes a raw variable cell for condition comparison.
|
|
146
|
+
*/
|
|
147
|
+
const normalizeConditionValue = (
|
|
148
|
+
currentValue: string | number | null | undefined
|
|
149
|
+
): string => {
|
|
150
|
+
if (currentValue === null || currentValue === undefined) return "";
|
|
151
|
+
if (typeof currentValue === "number") return currentValue.toString();
|
|
152
|
+
if (typeof currentValue === "string") return currentValue;
|
|
153
|
+
return "";
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Substitutes `[var:N]` in a condition comparator using OVC values.
|
|
158
|
+
*/
|
|
159
|
+
export const parseConditionComparatorWithVariables = (
|
|
160
|
+
text: string,
|
|
161
|
+
optionId: number,
|
|
162
|
+
index: number,
|
|
163
|
+
deps: ComparatorResolverDeps,
|
|
164
|
+
context?: Pick<
|
|
165
|
+
ComparatorResolverContext,
|
|
166
|
+
"conditionVariableId" | "conditionOptionId" | "evalVariableId"
|
|
167
|
+
>
|
|
168
|
+
): string => {
|
|
169
|
+
const variableIds = extractVarTagIds(text);
|
|
170
|
+
if (variableIds.length === 0) return text;
|
|
171
|
+
|
|
172
|
+
const rowContext = {
|
|
173
|
+
contextVariableId: context?.evalVariableId ?? context?.conditionVariableId,
|
|
174
|
+
conditionOptionId: context?.conditionOptionId,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
let parsedText = text;
|
|
178
|
+
variableIds.forEach((variableId) => {
|
|
179
|
+
const values = deps.ovc.variables[variableId] ?? [];
|
|
180
|
+
const useRow = shouldUseConditionRowIndex(
|
|
181
|
+
deps.references,
|
|
182
|
+
variableId,
|
|
183
|
+
optionId,
|
|
184
|
+
rowContext
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const cleanValues = useRow
|
|
188
|
+
? [normalizeConditionValue(values[index])]
|
|
189
|
+
: values.map((value) => normalizeConditionValue(value));
|
|
190
|
+
|
|
191
|
+
let resolved =
|
|
192
|
+
cleanValues.length === 1 ? cleanValues[0] : cleanValues.join(", ");
|
|
193
|
+
|
|
194
|
+
if (resolved.trim().length === 0) {
|
|
195
|
+
resolved = CONDITION_COMPARATOR_EMPTY_SENTINEL;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
parsedText = parsedText.replace(
|
|
199
|
+
new RegExp(`\\[var:${variableId}\\]`, "gi"),
|
|
200
|
+
resolved
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return parsedText;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Uses the LHS variable's immediate parent option for related-variable resolution.
|
|
209
|
+
*/
|
|
210
|
+
const resolveLeafContext = (
|
|
211
|
+
lhs: unknown,
|
|
212
|
+
context: ComparatorResolverContext,
|
|
213
|
+
deps: ComparatorResolverDeps
|
|
214
|
+
): ComparatorResolverContext => {
|
|
215
|
+
const variableId = extractLhsVariableId(lhs);
|
|
216
|
+
if (variableId === undefined) return context;
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
...context,
|
|
220
|
+
optionId: getConditionVariableOptionId(deps.references, variableId),
|
|
221
|
+
evalVariableId: variableId,
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Resolves a comparator string containing `[var:N]` tags.
|
|
227
|
+
*
|
|
228
|
+
* Same-multiple tags follow the leaf LHS evaluation index
|
|
229
|
+
* (`evalVariableId` + `index`): related → current row, first → 0.
|
|
230
|
+
*/
|
|
231
|
+
export const resolveComparatorString = (
|
|
232
|
+
value: string,
|
|
233
|
+
context: ComparatorResolverContext,
|
|
234
|
+
deps: ComparatorResolverDeps
|
|
235
|
+
): string => {
|
|
236
|
+
if (!VAR_TAG_IN_TEXT_REGEX.test(value)) return value;
|
|
237
|
+
|
|
238
|
+
return parseConditionComparatorWithVariables(
|
|
239
|
+
value,
|
|
240
|
+
context.optionId,
|
|
241
|
+
context.index,
|
|
242
|
+
deps,
|
|
243
|
+
{
|
|
244
|
+
evalVariableId: context.evalVariableId,
|
|
245
|
+
conditionVariableId: context.conditionVariableId,
|
|
246
|
+
conditionOptionId: context.conditionOptionId,
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Walks a condition tree and resolves comparator literals before LpLogic runs.
|
|
253
|
+
*/
|
|
254
|
+
const walkConditionNode = (
|
|
255
|
+
node: unknown,
|
|
256
|
+
context: ComparatorResolverContext,
|
|
257
|
+
deps: ComparatorResolverDeps
|
|
258
|
+
): unknown => {
|
|
259
|
+
if (node === null || node === undefined) return node;
|
|
260
|
+
|
|
261
|
+
if (Array.isArray(node)) {
|
|
262
|
+
return node.map((child) => walkConditionNode(child, context, deps));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (typeof node !== "object") return node;
|
|
266
|
+
|
|
267
|
+
const conditionNode = node as Record<string, unknown>;
|
|
268
|
+
const structuralKeys = Object.keys(conditionNode).filter(
|
|
269
|
+
(key) => !UI_METADATA_KEYS.has(key)
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
if (structuralKeys.length === 0) return conditionNode;
|
|
273
|
+
|
|
274
|
+
const operatorKey = structuralKeys.find((key) =>
|
|
275
|
+
RECOGNIZED_OPERATOR_KEYS.has(key)
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (structuralKeys.length > 1 && operatorKey === undefined) {
|
|
279
|
+
return conditionNode;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const currentKey = operatorKey ?? structuralKeys[0];
|
|
283
|
+
|
|
284
|
+
if (currentKey === "and" || currentKey === "or") {
|
|
285
|
+
const children = conditionNode[currentKey];
|
|
286
|
+
if (!Array.isArray(children)) return conditionNode;
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
...conditionNode,
|
|
290
|
+
[currentKey]: children.map((child) =>
|
|
291
|
+
walkConditionNode(child, context, deps)
|
|
292
|
+
),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (RECOGNIZED_OPERATOR_KEYS.has(currentKey)) {
|
|
297
|
+
const args = conditionNode[currentKey];
|
|
298
|
+
if (!Array.isArray(args) || args.length < 2) return conditionNode;
|
|
299
|
+
|
|
300
|
+
const resolvedArgs = [...args];
|
|
301
|
+
const comparator = resolvedArgs[1];
|
|
302
|
+
if (typeof comparator === "string") {
|
|
303
|
+
const baseContext = SCOPED_OPERATOR_REGEX.test(currentKey)
|
|
304
|
+
? { ...context, index: 0 }
|
|
305
|
+
: context;
|
|
306
|
+
const leafContext = resolveLeafContext(
|
|
307
|
+
resolvedArgs[0],
|
|
308
|
+
baseContext,
|
|
309
|
+
deps
|
|
310
|
+
);
|
|
311
|
+
resolvedArgs[1] = resolveComparatorString(comparator, leafContext, deps);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
...conditionNode,
|
|
316
|
+
[currentKey]: resolvedArgs,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return conditionNode;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Resolves comparators in a mapped conditions tree.
|
|
325
|
+
*/
|
|
326
|
+
export const resolveConditionComparators = (
|
|
327
|
+
conditions: unknown,
|
|
328
|
+
context: ComparatorResolverContext,
|
|
329
|
+
deps: ComparatorResolverDeps
|
|
330
|
+
): unknown => {
|
|
331
|
+
if (Array.isArray(conditions)) {
|
|
332
|
+
return conditions.map((condition) =>
|
|
333
|
+
walkConditionNode(condition, context, deps)
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return walkConditionNode(conditions, context, deps);
|
|
338
|
+
};
|