form0-core 0.1.9 → 0.1.10
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/package.json
CHANGED
|
@@ -28,9 +28,18 @@ export function evaluateCalculatedFields(
|
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const blockedFieldNames = blockCyclicCalculatedFields(plan, values);
|
|
32
|
+
const evaluableFieldNames = plan.orderedFieldNames.filter(
|
|
33
|
+
(fieldName) => !blockedFieldNames.has(fieldName)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (evaluableFieldNames.length === 0) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
if (plan.hasDynamicDependencies) {
|
|
32
41
|
evaluateFieldSequenceUntilStable(
|
|
33
|
-
|
|
42
|
+
evaluableFieldNames,
|
|
34
43
|
plan,
|
|
35
44
|
schema,
|
|
36
45
|
values,
|
|
@@ -50,17 +59,6 @@ export function evaluateCalculatedFields(
|
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
if (component.isCyclic) {
|
|
53
|
-
evaluateFieldSequenceUntilStable(
|
|
54
|
-
component.fieldNames,
|
|
55
|
-
plan,
|
|
56
|
-
schema,
|
|
57
|
-
values,
|
|
58
|
-
helpers,
|
|
59
|
-
securityConfig,
|
|
60
|
-
resolver,
|
|
61
|
-
warnings,
|
|
62
|
-
runtimeDiagnostics
|
|
63
|
-
);
|
|
64
62
|
return;
|
|
65
63
|
}
|
|
66
64
|
|
|
@@ -81,6 +79,24 @@ export function evaluateCalculatedFields(
|
|
|
81
79
|
});
|
|
82
80
|
}
|
|
83
81
|
|
|
82
|
+
function blockCyclicCalculatedFields(plan, values) {
|
|
83
|
+
const blockedFieldNames = new Set();
|
|
84
|
+
|
|
85
|
+
plan.cyclicComponentIds.forEach((componentId) => {
|
|
86
|
+
const component = plan.componentsById.get(componentId);
|
|
87
|
+
if (!component?.isCyclic) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
component.fieldNames.forEach((fieldName) => {
|
|
92
|
+
blockedFieldNames.add(fieldName);
|
|
93
|
+
values[fieldName] = null;
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return blockedFieldNames;
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
function buildScopedContext(
|
|
85
101
|
values,
|
|
86
102
|
helpers,
|
|
@@ -198,10 +214,11 @@ function emitDependencyPlanWarnings(plan, warningSystem, runtimeDiagnostics) {
|
|
|
198
214
|
}
|
|
199
215
|
|
|
200
216
|
const fieldNames = component.fieldNames;
|
|
201
|
-
const message = `CalculatedField dependency cycle detected: ${fieldNames.join(' -> ')}. Runtime evaluation
|
|
217
|
+
const message = `CalculatedField dependency cycle detected: ${fieldNames.join(' -> ')}. Runtime evaluation is disabled for the involved fields until the cycle is removed.`;
|
|
202
218
|
warningSystem.emitWarning({
|
|
203
219
|
type: 'calculation_dependency',
|
|
204
220
|
reason: 'cyclic_dependencies',
|
|
221
|
+
severity: 'error',
|
|
205
222
|
message,
|
|
206
223
|
suggestion:
|
|
207
224
|
'Remove the cycle or break it with a source field so calculated values can be evaluated deterministically.',
|
|
@@ -220,7 +237,7 @@ function emitDependencyPlanWarnings(plan, warningSystem, runtimeDiagnostics) {
|
|
|
220
237
|
runtimeDiagnostics,
|
|
221
238
|
fieldName,
|
|
222
239
|
message,
|
|
223
|
-
'
|
|
240
|
+
'error',
|
|
224
241
|
`cycle:${componentId}`
|
|
225
242
|
);
|
|
226
243
|
});
|
|
@@ -296,7 +313,7 @@ function evaluateFieldSequenceUntilStable(
|
|
|
296
313
|
warnings,
|
|
297
314
|
runtimeDiagnostics
|
|
298
315
|
) {
|
|
299
|
-
const maxPasses = Math.max(2,
|
|
316
|
+
const maxPasses = Math.max(2, fieldNames.length);
|
|
300
317
|
let stillChangingFieldNames = [];
|
|
301
318
|
|
|
302
319
|
for (let pass = 0; pass < maxPasses; pass += 1) {
|
|
@@ -105,8 +105,235 @@ export function normalizeCalculationFormSchema(schema) {
|
|
|
105
105
|
throw new Error('Expected a form schema or a root schema containing form.elements');
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function isEscapedCharacter(source, index) {
|
|
109
|
+
let backslashCount = 0;
|
|
110
|
+
let cursor = index - 1;
|
|
111
|
+
|
|
112
|
+
while (cursor >= 0 && source[cursor] === '\\') {
|
|
113
|
+
backslashCount += 1;
|
|
114
|
+
cursor -= 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return backslashCount % 2 === 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function skipWhitespace(source, state) {
|
|
121
|
+
while (state.index < source.length && /\s/.test(source[state.index])) {
|
|
122
|
+
state.index += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseStaticStringLiteral(source, state) {
|
|
127
|
+
const quote = source[state.index];
|
|
128
|
+
if (quote !== "'" && quote !== '"' && quote !== '`') {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
state.index += 1;
|
|
133
|
+
let value = '';
|
|
134
|
+
|
|
135
|
+
while (state.index < source.length) {
|
|
136
|
+
const char = source[state.index];
|
|
137
|
+
|
|
138
|
+
if (char === quote && !isEscapedCharacter(source, state.index)) {
|
|
139
|
+
state.index += 1;
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (quote === '`' && char === '$' && source[state.index + 1] === '{') {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (char === '\\' && state.index + 1 < source.length) {
|
|
148
|
+
const escapedChar = source[state.index + 1];
|
|
149
|
+
const escapeMap = {
|
|
150
|
+
n: '\n',
|
|
151
|
+
r: '\r',
|
|
152
|
+
t: '\t',
|
|
153
|
+
"'": "'",
|
|
154
|
+
'"': '"',
|
|
155
|
+
'`': '`',
|
|
156
|
+
'\\': '\\',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
value += escapeMap[escapedChar] ?? escapedChar;
|
|
160
|
+
state.index += 2;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
value += char;
|
|
165
|
+
state.index += 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseStaticStringTerm(source, state) {
|
|
172
|
+
skipWhitespace(source, state);
|
|
173
|
+
|
|
174
|
+
if (source[state.index] === '(') {
|
|
175
|
+
state.index += 1;
|
|
176
|
+
const nestedValue = parseStaticStringConcatenation(source, state);
|
|
177
|
+
if (nestedValue === null) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
skipWhitespace(source, state);
|
|
182
|
+
if (source[state.index] !== ')') {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
state.index += 1;
|
|
187
|
+
return nestedValue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return parseStaticStringLiteral(source, state);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseStaticStringConcatenation(source, state) {
|
|
194
|
+
const firstValue = parseStaticStringTerm(source, state);
|
|
195
|
+
if (firstValue === null) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let result = firstValue;
|
|
200
|
+
|
|
201
|
+
while (true) {
|
|
202
|
+
skipWhitespace(source, state);
|
|
203
|
+
if (source[state.index] !== '+') {
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
state.index += 1;
|
|
208
|
+
const nextValue = parseStaticStringTerm(source, state);
|
|
209
|
+
if (nextValue === null) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
result += nextValue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resolveStaticStringExpression(source) {
|
|
218
|
+
if (typeof source !== 'string') {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const state = { index: 0 };
|
|
223
|
+
skipWhitespace(source, state);
|
|
224
|
+
|
|
225
|
+
if (state.index >= source.length) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const result = parseStaticStringConcatenation(source, state);
|
|
230
|
+
if (result === null) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
skipWhitespace(source, state);
|
|
235
|
+
return state.index === source.length ? result : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function extractEvalCallArguments(code) {
|
|
239
|
+
const source = typeof code === 'string' ? code : '';
|
|
240
|
+
const argumentsList = [];
|
|
241
|
+
let i = 0;
|
|
242
|
+
|
|
243
|
+
while (i < source.length) {
|
|
244
|
+
const evalMatch = source.substring(i).match(/^EVAL\s*\(/);
|
|
245
|
+
if (!evalMatch) {
|
|
246
|
+
i += 1;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
i += evalMatch[0].length;
|
|
251
|
+
const startIndex = i;
|
|
252
|
+
|
|
253
|
+
let parenCount = 1;
|
|
254
|
+
let inSingleQuote = false;
|
|
255
|
+
let inDoubleQuote = false;
|
|
256
|
+
let inBacktick = false;
|
|
257
|
+
|
|
258
|
+
while (i < source.length && parenCount > 0) {
|
|
259
|
+
const char = source[i];
|
|
260
|
+
|
|
261
|
+
if (char === "'" && !inDoubleQuote && !inBacktick && !isEscapedCharacter(source, i)) {
|
|
262
|
+
inSingleQuote = !inSingleQuote;
|
|
263
|
+
} else if (
|
|
264
|
+
char === '"' &&
|
|
265
|
+
!inSingleQuote &&
|
|
266
|
+
!inBacktick &&
|
|
267
|
+
!isEscapedCharacter(source, i)
|
|
268
|
+
) {
|
|
269
|
+
inDoubleQuote = !inDoubleQuote;
|
|
270
|
+
} else if (
|
|
271
|
+
char === '`' &&
|
|
272
|
+
!inSingleQuote &&
|
|
273
|
+
!inDoubleQuote &&
|
|
274
|
+
!isEscapedCharacter(source, i)
|
|
275
|
+
) {
|
|
276
|
+
inBacktick = !inBacktick;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!inSingleQuote && !inDoubleQuote && !inBacktick) {
|
|
280
|
+
if (char === '(') {
|
|
281
|
+
parenCount += 1;
|
|
282
|
+
} else if (char === ')') {
|
|
283
|
+
parenCount -= 1;
|
|
284
|
+
if (parenCount === 0) {
|
|
285
|
+
argumentsList.push(source.slice(startIndex, i));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
i += 1;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return argumentsList;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function resolveStaticEvalFieldReference(expression) {
|
|
298
|
+
let currentExpression = typeof expression === 'string' ? expression : null;
|
|
299
|
+
|
|
300
|
+
for (let depth = 0; depth < 2 && currentExpression; depth += 1) {
|
|
301
|
+
const resolvedString = resolveStaticStringExpression(currentExpression);
|
|
302
|
+
if (resolvedString === null) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const fieldReferenceMatch = resolvedString.match(/^\$([a-zA-Z_][a-zA-Z0-9_]*)$/);
|
|
307
|
+
if (fieldReferenceMatch) {
|
|
308
|
+
return fieldReferenceMatch[1];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
currentExpression = resolvedString;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function extractStaticEvalFieldReferences(code) {
|
|
318
|
+
const fieldReferences = new Set();
|
|
319
|
+
|
|
320
|
+
extractEvalCallArguments(code).forEach((argumentSource) => {
|
|
321
|
+
const fieldName = resolveStaticEvalFieldReference(argumentSource);
|
|
322
|
+
if (fieldName) {
|
|
323
|
+
fieldReferences.add(fieldName);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return fieldReferences;
|
|
328
|
+
}
|
|
329
|
+
|
|
108
330
|
export function hasDynamicCalculationDependencies(code) {
|
|
109
|
-
|
|
331
|
+
const evalArguments = extractEvalCallArguments(code);
|
|
332
|
+
if (evalArguments.length === 0) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return evalArguments.some((argumentSource) => !resolveStaticEvalFieldReference(argumentSource));
|
|
110
337
|
}
|
|
111
338
|
|
|
112
339
|
export function removeEvalContents(code) {
|
|
@@ -174,7 +401,15 @@ export function extractStaticFieldReferenceMatches(code) {
|
|
|
174
401
|
}
|
|
175
402
|
|
|
176
403
|
export function extractStaticFieldReferences(code) {
|
|
177
|
-
|
|
404
|
+
const fieldReferences = new Set(
|
|
405
|
+
extractStaticFieldReferenceMatches(code).map((match) => match.fieldName)
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
extractStaticEvalFieldReferences(code).forEach((fieldName) => {
|
|
409
|
+
fieldReferences.add(fieldName);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
return fieldReferences;
|
|
178
413
|
}
|
|
179
414
|
|
|
180
415
|
export function buildCalculationExecutionContext(form, fieldDataName, resolver) {
|