@fogpipe/forma-core 0.12.0-alpha.2 → 0.13.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/README.md +12 -12
- package/dist/{chunk-U2OXXFEH.js → chunk-BDICNCE2.js} +1 -1
- package/dist/chunk-BDICNCE2.js.map +1 -0
- package/dist/{chunk-ZSW7NIMY.js → chunk-NKA3L2LJ.js} +64 -15
- package/dist/chunk-NKA3L2LJ.js.map +1 -0
- package/dist/engine/calculate.d.ts.map +1 -1
- package/dist/engine/enabled.d.ts.map +1 -1
- package/dist/engine/index.cjs +62 -13
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.ts +8 -8
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +2 -2
- package/dist/engine/readonly.d.ts.map +1 -1
- package/dist/engine/required.d.ts.map +1 -1
- package/dist/engine/validate.d.ts.map +1 -1
- package/dist/engine/visibility.d.ts.map +1 -1
- package/dist/feel/index.cjs.map +1 -1
- package/dist/feel/index.js +1 -1
- package/dist/index.cjs +62 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/feel.test.ts +67 -76
- package/src/__tests__/format.test.ts +19 -6
- package/src/__tests__/validate.test.ts +62 -20
- package/src/__tests__/visibility.test.ts +217 -85
- package/src/engine/calculate.ts +13 -10
- package/src/engine/enabled.ts +16 -6
- package/src/engine/index.ts +8 -28
- package/src/engine/readonly.ts +16 -6
- package/src/engine/required.ts +7 -5
- package/src/engine/validate.ts +43 -22
- package/src/engine/visibility.ts +40 -16
- package/src/feel/index.ts +12 -12
- package/src/format/index.ts +1 -1
- package/src/types.ts +46 -7
- package/dist/chunk-U2OXXFEH.js.map +0 -1
- package/dist/chunk-ZSW7NIMY.js.map +0 -1
package/src/engine/enabled.ts
CHANGED
|
@@ -49,7 +49,7 @@ export interface EnabledOptions {
|
|
|
49
49
|
export function getEnabled(
|
|
50
50
|
data: Record<string, unknown>,
|
|
51
51
|
spec: Forma,
|
|
52
|
-
options: EnabledOptions = {}
|
|
52
|
+
options: EnabledOptions = {},
|
|
53
53
|
): EnabledResult {
|
|
54
54
|
const computed = options.computed ?? calculate(data, spec);
|
|
55
55
|
const context: EvaluationContext = {
|
|
@@ -73,7 +73,15 @@ export function getEnabled(
|
|
|
73
73
|
if (fieldDef.type === "array" && fieldDef.itemFields) {
|
|
74
74
|
const arrayData = data[fieldPath];
|
|
75
75
|
if (Array.isArray(arrayData)) {
|
|
76
|
-
evaluateArrayItemEnabled(
|
|
76
|
+
evaluateArrayItemEnabled(
|
|
77
|
+
fieldPath,
|
|
78
|
+
fieldDef,
|
|
79
|
+
arrayData,
|
|
80
|
+
data,
|
|
81
|
+
computed,
|
|
82
|
+
spec,
|
|
83
|
+
result,
|
|
84
|
+
);
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
}
|
|
@@ -90,7 +98,7 @@ export function getEnabled(
|
|
|
90
98
|
*/
|
|
91
99
|
function isFieldEnabled(
|
|
92
100
|
fieldDef: FieldDefinition,
|
|
93
|
-
context: EvaluationContext
|
|
101
|
+
context: EvaluationContext,
|
|
94
102
|
): boolean {
|
|
95
103
|
// If field has enabledWhen, evaluate it
|
|
96
104
|
if (fieldDef.enabledWhen) {
|
|
@@ -111,7 +119,7 @@ function evaluateArrayItemEnabled(
|
|
|
111
119
|
data: Record<string, unknown>,
|
|
112
120
|
computed: Record<string, unknown>,
|
|
113
121
|
spec: Forma,
|
|
114
|
-
result: EnabledResult
|
|
122
|
+
result: EnabledResult,
|
|
115
123
|
): void {
|
|
116
124
|
if (!fieldDef.itemFields) return;
|
|
117
125
|
|
|
@@ -125,7 +133,9 @@ function evaluateArrayItemEnabled(
|
|
|
125
133
|
itemIndex: i,
|
|
126
134
|
};
|
|
127
135
|
|
|
128
|
-
for (const [itemFieldName, itemFieldDef] of Object.entries(
|
|
136
|
+
for (const [itemFieldName, itemFieldDef] of Object.entries(
|
|
137
|
+
fieldDef.itemFields,
|
|
138
|
+
)) {
|
|
129
139
|
const itemFieldPath = `${arrayPath}[${i}].${itemFieldName}`;
|
|
130
140
|
result[itemFieldPath] = isFieldEnabled(itemFieldDef, itemContext);
|
|
131
141
|
}
|
|
@@ -143,7 +153,7 @@ function evaluateArrayItemEnabled(
|
|
|
143
153
|
export function isEnabled(
|
|
144
154
|
fieldPath: string,
|
|
145
155
|
data: Record<string, unknown>,
|
|
146
|
-
spec: Forma
|
|
156
|
+
spec: Forma,
|
|
147
157
|
): boolean {
|
|
148
158
|
const fieldDef = spec.fields[fieldPath];
|
|
149
159
|
if (!fieldDef) {
|
package/src/engine/index.ts
CHANGED
|
@@ -38,41 +38,21 @@ export type {
|
|
|
38
38
|
} from "./visibility.js";
|
|
39
39
|
|
|
40
40
|
// Required
|
|
41
|
-
export {
|
|
42
|
-
getRequired,
|
|
43
|
-
isRequired,
|
|
44
|
-
} from "./required.js";
|
|
41
|
+
export { getRequired, isRequired } from "./required.js";
|
|
45
42
|
|
|
46
|
-
export type {
|
|
47
|
-
RequiredOptions,
|
|
48
|
-
} from "./required.js";
|
|
43
|
+
export type { RequiredOptions } from "./required.js";
|
|
49
44
|
|
|
50
45
|
// Enabled
|
|
51
|
-
export {
|
|
52
|
-
getEnabled,
|
|
53
|
-
isEnabled,
|
|
54
|
-
} from "./enabled.js";
|
|
46
|
+
export { getEnabled, isEnabled } from "./enabled.js";
|
|
55
47
|
|
|
56
|
-
export type {
|
|
57
|
-
EnabledOptions,
|
|
58
|
-
} from "./enabled.js";
|
|
48
|
+
export type { EnabledOptions } from "./enabled.js";
|
|
59
49
|
|
|
60
50
|
// Readonly
|
|
61
|
-
export {
|
|
62
|
-
getReadonly,
|
|
63
|
-
isReadonly,
|
|
64
|
-
} from "./readonly.js";
|
|
51
|
+
export { getReadonly, isReadonly } from "./readonly.js";
|
|
65
52
|
|
|
66
|
-
export type {
|
|
67
|
-
ReadonlyOptions,
|
|
68
|
-
} from "./readonly.js";
|
|
53
|
+
export type { ReadonlyOptions } from "./readonly.js";
|
|
69
54
|
|
|
70
55
|
// Validate
|
|
71
|
-
export {
|
|
72
|
-
validate,
|
|
73
|
-
validateSingleField,
|
|
74
|
-
} from "./validate.js";
|
|
56
|
+
export { validate, validateSingleField } from "./validate.js";
|
|
75
57
|
|
|
76
|
-
export type {
|
|
77
|
-
ValidateOptions,
|
|
78
|
-
} from "./validate.js";
|
|
58
|
+
export type { ValidateOptions } from "./validate.js";
|
package/src/engine/readonly.ts
CHANGED
|
@@ -53,7 +53,7 @@ export interface ReadonlyOptions {
|
|
|
53
53
|
export function getReadonly(
|
|
54
54
|
data: Record<string, unknown>,
|
|
55
55
|
spec: Forma,
|
|
56
|
-
options: ReadonlyOptions = {}
|
|
56
|
+
options: ReadonlyOptions = {},
|
|
57
57
|
): ReadonlyResult {
|
|
58
58
|
const computed = options.computed ?? calculate(data, spec);
|
|
59
59
|
const context: EvaluationContext = {
|
|
@@ -77,7 +77,15 @@ export function getReadonly(
|
|
|
77
77
|
if (fieldDef.type === "array" && fieldDef.itemFields) {
|
|
78
78
|
const arrayData = data[fieldPath];
|
|
79
79
|
if (Array.isArray(arrayData)) {
|
|
80
|
-
evaluateArrayItemReadonly(
|
|
80
|
+
evaluateArrayItemReadonly(
|
|
81
|
+
fieldPath,
|
|
82
|
+
fieldDef,
|
|
83
|
+
arrayData,
|
|
84
|
+
data,
|
|
85
|
+
computed,
|
|
86
|
+
spec,
|
|
87
|
+
result,
|
|
88
|
+
);
|
|
81
89
|
}
|
|
82
90
|
}
|
|
83
91
|
}
|
|
@@ -94,7 +102,7 @@ export function getReadonly(
|
|
|
94
102
|
*/
|
|
95
103
|
function isFieldReadonly(
|
|
96
104
|
fieldDef: FieldDefinition,
|
|
97
|
-
context: EvaluationContext
|
|
105
|
+
context: EvaluationContext,
|
|
98
106
|
): boolean {
|
|
99
107
|
// If field has readonlyWhen, evaluate it
|
|
100
108
|
if (fieldDef.readonlyWhen) {
|
|
@@ -115,7 +123,7 @@ function evaluateArrayItemReadonly(
|
|
|
115
123
|
data: Record<string, unknown>,
|
|
116
124
|
computed: Record<string, unknown>,
|
|
117
125
|
spec: Forma,
|
|
118
|
-
result: ReadonlyResult
|
|
126
|
+
result: ReadonlyResult,
|
|
119
127
|
): void {
|
|
120
128
|
if (!fieldDef.itemFields) return;
|
|
121
129
|
|
|
@@ -129,7 +137,9 @@ function evaluateArrayItemReadonly(
|
|
|
129
137
|
itemIndex: i,
|
|
130
138
|
};
|
|
131
139
|
|
|
132
|
-
for (const [itemFieldName, itemFieldDef] of Object.entries(
|
|
140
|
+
for (const [itemFieldName, itemFieldDef] of Object.entries(
|
|
141
|
+
fieldDef.itemFields,
|
|
142
|
+
)) {
|
|
133
143
|
const itemFieldPath = `${arrayPath}[${i}].${itemFieldName}`;
|
|
134
144
|
result[itemFieldPath] = isFieldReadonly(itemFieldDef, itemContext);
|
|
135
145
|
}
|
|
@@ -147,7 +157,7 @@ function evaluateArrayItemReadonly(
|
|
|
147
157
|
export function isReadonly(
|
|
148
158
|
fieldPath: string,
|
|
149
159
|
data: Record<string, unknown>,
|
|
150
|
-
spec: Forma
|
|
160
|
+
spec: Forma,
|
|
151
161
|
): boolean {
|
|
152
162
|
const fieldDef = spec.fields[fieldPath];
|
|
153
163
|
if (!fieldDef) {
|
package/src/engine/required.ts
CHANGED
|
@@ -48,7 +48,7 @@ export interface RequiredOptions {
|
|
|
48
48
|
export function getRequired(
|
|
49
49
|
data: Record<string, unknown>,
|
|
50
50
|
spec: Forma,
|
|
51
|
-
options: RequiredOptions = {}
|
|
51
|
+
options: RequiredOptions = {},
|
|
52
52
|
): RequiredFieldsResult {
|
|
53
53
|
const computed = options.computed ?? calculate(data, spec);
|
|
54
54
|
const context: EvaluationContext = {
|
|
@@ -82,13 +82,15 @@ export function getRequired(
|
|
|
82
82
|
itemIndex: i,
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
for (const [itemFieldName, itemFieldDef] of Object.entries(
|
|
85
|
+
for (const [itemFieldName, itemFieldDef] of Object.entries(
|
|
86
|
+
fieldDef.itemFields,
|
|
87
|
+
)) {
|
|
86
88
|
const itemFieldPath = `${fieldPath}[${i}].${itemFieldName}`;
|
|
87
89
|
result[itemFieldPath] = isFieldRequired(
|
|
88
90
|
itemFieldPath,
|
|
89
91
|
itemFieldDef,
|
|
90
92
|
spec,
|
|
91
|
-
itemContext
|
|
93
|
+
itemContext,
|
|
92
94
|
);
|
|
93
95
|
}
|
|
94
96
|
}
|
|
@@ -111,7 +113,7 @@ export function isFieldRequired(
|
|
|
111
113
|
fieldPath: string,
|
|
112
114
|
fieldDef: FieldDefinition,
|
|
113
115
|
spec: Forma,
|
|
114
|
-
context: EvaluationContext
|
|
116
|
+
context: EvaluationContext,
|
|
115
117
|
): boolean {
|
|
116
118
|
// If field has requiredWhen, evaluate it
|
|
117
119
|
if (fieldDef.requiredWhen) {
|
|
@@ -133,7 +135,7 @@ export function isFieldRequired(
|
|
|
133
135
|
export function isRequired(
|
|
134
136
|
fieldPath: string,
|
|
135
137
|
data: Record<string, unknown>,
|
|
136
|
-
spec: Forma
|
|
138
|
+
spec: Forma,
|
|
137
139
|
): boolean {
|
|
138
140
|
const fieldDef = spec.fields[fieldPath];
|
|
139
141
|
if (!fieldDef) {
|
package/src/engine/validate.ts
CHANGED
|
@@ -73,7 +73,7 @@ export interface ValidateOptions {
|
|
|
73
73
|
export function validate(
|
|
74
74
|
data: Record<string, unknown>,
|
|
75
75
|
spec: Forma,
|
|
76
|
-
options: ValidateOptions = {}
|
|
76
|
+
options: ValidateOptions = {},
|
|
77
77
|
): ValidationResult {
|
|
78
78
|
const { onlyVisible = true } = options;
|
|
79
79
|
|
|
@@ -81,7 +81,8 @@ export function validate(
|
|
|
81
81
|
const computed = options.computed ?? calculate(data, spec);
|
|
82
82
|
|
|
83
83
|
// Calculate visibility
|
|
84
|
-
const visibility =
|
|
84
|
+
const visibility =
|
|
85
|
+
options.visibility ?? getVisibility(data, spec, { computed });
|
|
85
86
|
|
|
86
87
|
// Collect errors
|
|
87
88
|
const errors: FieldError[] = [];
|
|
@@ -114,7 +115,7 @@ export function validate(
|
|
|
114
115
|
data,
|
|
115
116
|
computed,
|
|
116
117
|
visibility,
|
|
117
|
-
onlyVisible
|
|
118
|
+
onlyVisible,
|
|
118
119
|
);
|
|
119
120
|
|
|
120
121
|
errors.push(...fieldErrors);
|
|
@@ -142,7 +143,7 @@ function validateField(
|
|
|
142
143
|
data: Record<string, unknown>,
|
|
143
144
|
computed: Record<string, unknown>,
|
|
144
145
|
visibility: Record<string, boolean>,
|
|
145
|
-
onlyVisible: boolean
|
|
146
|
+
onlyVisible: boolean,
|
|
146
147
|
): FieldError[] {
|
|
147
148
|
const errors: FieldError[] = [];
|
|
148
149
|
const context: EvaluationContext = {
|
|
@@ -174,7 +175,11 @@ function validateField(
|
|
|
174
175
|
|
|
175
176
|
// 3. Custom FEEL validation rules
|
|
176
177
|
if (fieldDef.validations && !isEmpty(value)) {
|
|
177
|
-
const customErrors = validateCustomRules(
|
|
178
|
+
const customErrors = validateCustomRules(
|
|
179
|
+
path,
|
|
180
|
+
fieldDef.validations,
|
|
181
|
+
context,
|
|
182
|
+
);
|
|
178
183
|
errors.push(...customErrors);
|
|
179
184
|
}
|
|
180
185
|
|
|
@@ -189,7 +194,7 @@ function validateField(
|
|
|
189
194
|
data,
|
|
190
195
|
computed,
|
|
191
196
|
visibility,
|
|
192
|
-
onlyVisible
|
|
197
|
+
onlyVisible,
|
|
193
198
|
);
|
|
194
199
|
errors.push(...arrayErrors);
|
|
195
200
|
}
|
|
@@ -218,7 +223,7 @@ function validateType(
|
|
|
218
223
|
path: string,
|
|
219
224
|
value: unknown,
|
|
220
225
|
schema: JSONSchemaProperty,
|
|
221
|
-
fieldDef: { label?: string }
|
|
226
|
+
fieldDef: { label?: string },
|
|
222
227
|
): FieldError | null {
|
|
223
228
|
const label = fieldDef.label ?? path;
|
|
224
229
|
|
|
@@ -320,7 +325,10 @@ function validateType(
|
|
|
320
325
|
}
|
|
321
326
|
}
|
|
322
327
|
|
|
323
|
-
if (
|
|
328
|
+
if (
|
|
329
|
+
"exclusiveMinimum" in schema &&
|
|
330
|
+
schema.exclusiveMinimum !== undefined
|
|
331
|
+
) {
|
|
324
332
|
if (value <= schema.exclusiveMinimum) {
|
|
325
333
|
return {
|
|
326
334
|
field: path,
|
|
@@ -330,7 +338,10 @@ function validateType(
|
|
|
330
338
|
}
|
|
331
339
|
}
|
|
332
340
|
|
|
333
|
-
if (
|
|
341
|
+
if (
|
|
342
|
+
"exclusiveMaximum" in schema &&
|
|
343
|
+
schema.exclusiveMaximum !== undefined
|
|
344
|
+
) {
|
|
334
345
|
if (value >= schema.exclusiveMaximum) {
|
|
335
346
|
return {
|
|
336
347
|
field: path,
|
|
@@ -344,7 +355,8 @@ function validateType(
|
|
|
344
355
|
const multipleOf = schema.multipleOf;
|
|
345
356
|
// Use epsilon comparison to handle floating point precision issues
|
|
346
357
|
const remainder = Math.abs(value % multipleOf);
|
|
347
|
-
const isValid =
|
|
358
|
+
const isValid =
|
|
359
|
+
remainder < 1e-10 || Math.abs(remainder - multipleOf) < 1e-10;
|
|
348
360
|
if (!isValid) {
|
|
349
361
|
return {
|
|
350
362
|
field: path,
|
|
@@ -402,7 +414,7 @@ function validateFormat(
|
|
|
402
414
|
path: string,
|
|
403
415
|
value: string,
|
|
404
416
|
format: string,
|
|
405
|
-
label: string
|
|
417
|
+
label: string,
|
|
406
418
|
): FieldError | null {
|
|
407
419
|
switch (format) {
|
|
408
420
|
case "email": {
|
|
@@ -430,7 +442,10 @@ function validateFormat(
|
|
|
430
442
|
}
|
|
431
443
|
// Verify the date is actually valid (e.g., not Feb 30)
|
|
432
444
|
const parsed = new Date(value + "T00:00:00Z");
|
|
433
|
-
if (
|
|
445
|
+
if (
|
|
446
|
+
isNaN(parsed.getTime()) ||
|
|
447
|
+
parsed.toISOString().slice(0, 10) !== value
|
|
448
|
+
) {
|
|
434
449
|
return {
|
|
435
450
|
field: path,
|
|
436
451
|
message: `${label} must be a valid date`,
|
|
@@ -465,7 +480,8 @@ function validateFormat(
|
|
|
465
480
|
}
|
|
466
481
|
|
|
467
482
|
case "uuid": {
|
|
468
|
-
const uuidRegex =
|
|
483
|
+
const uuidRegex =
|
|
484
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
469
485
|
if (!uuidRegex.test(value)) {
|
|
470
486
|
return {
|
|
471
487
|
field: path,
|
|
@@ -491,7 +507,7 @@ function validateFormat(
|
|
|
491
507
|
function validateCustomRules(
|
|
492
508
|
path: string,
|
|
493
509
|
rules: ValidationRule[],
|
|
494
|
-
context: EvaluationContext
|
|
510
|
+
context: EvaluationContext,
|
|
495
511
|
): FieldError[] {
|
|
496
512
|
const errors: FieldError[] = [];
|
|
497
513
|
|
|
@@ -526,13 +542,14 @@ function validateArray(
|
|
|
526
542
|
data: Record<string, unknown>,
|
|
527
543
|
computed: Record<string, unknown>,
|
|
528
544
|
visibility: Record<string, boolean>,
|
|
529
|
-
onlyVisible: boolean
|
|
545
|
+
onlyVisible: boolean,
|
|
530
546
|
): FieldError[] {
|
|
531
547
|
const errors: FieldError[] = [];
|
|
532
548
|
const label = fieldDef.label ?? path;
|
|
533
549
|
|
|
534
550
|
// Get array schema for minItems/maxItems fallback
|
|
535
|
-
const arraySchema =
|
|
551
|
+
const arraySchema =
|
|
552
|
+
schemaProperty?.type === "array" ? schemaProperty : undefined;
|
|
536
553
|
|
|
537
554
|
// Check min/max items - fieldDef overrides schema
|
|
538
555
|
const minItems = fieldDef.minItems ?? arraySchema?.minItems;
|
|
@@ -571,7 +588,7 @@ function validateArray(
|
|
|
571
588
|
data,
|
|
572
589
|
computed,
|
|
573
590
|
visibility,
|
|
574
|
-
onlyVisible
|
|
591
|
+
onlyVisible,
|
|
575
592
|
);
|
|
576
593
|
errors.push(...itemErrors);
|
|
577
594
|
}
|
|
@@ -593,7 +610,7 @@ function validateArrayItem(
|
|
|
593
610
|
data: Record<string, unknown>,
|
|
594
611
|
computed: Record<string, unknown>,
|
|
595
612
|
visibility: Record<string, boolean>,
|
|
596
|
-
onlyVisible: boolean
|
|
613
|
+
onlyVisible: boolean,
|
|
597
614
|
): FieldError[] {
|
|
598
615
|
const errors: FieldError[] = [];
|
|
599
616
|
|
|
@@ -649,7 +666,7 @@ function validateArrayItem(
|
|
|
649
666
|
itemFieldPath,
|
|
650
667
|
value,
|
|
651
668
|
fieldSchema,
|
|
652
|
-
fieldDef ?? { label: fieldName }
|
|
669
|
+
fieldDef ?? { label: fieldName },
|
|
653
670
|
);
|
|
654
671
|
if (typeError) {
|
|
655
672
|
errors.push(typeError);
|
|
@@ -658,7 +675,11 @@ function validateArrayItem(
|
|
|
658
675
|
|
|
659
676
|
// Custom validations from fieldDef
|
|
660
677
|
if (fieldDef?.validations && !isEmpty(value)) {
|
|
661
|
-
const customErrors = validateCustomRules(
|
|
678
|
+
const customErrors = validateCustomRules(
|
|
679
|
+
itemFieldPath,
|
|
680
|
+
fieldDef.validations,
|
|
681
|
+
context,
|
|
682
|
+
);
|
|
662
683
|
errors.push(...customErrors);
|
|
663
684
|
}
|
|
664
685
|
}
|
|
@@ -681,7 +702,7 @@ function validateArrayItem(
|
|
|
681
702
|
export function validateSingleField(
|
|
682
703
|
fieldPath: string,
|
|
683
704
|
data: Record<string, unknown>,
|
|
684
|
-
spec: Forma
|
|
705
|
+
spec: Forma,
|
|
685
706
|
): FieldError[] {
|
|
686
707
|
const fieldDef = spec.fields[fieldPath];
|
|
687
708
|
if (!fieldDef) {
|
|
@@ -701,6 +722,6 @@ export function validateSingleField(
|
|
|
701
722
|
data,
|
|
702
723
|
computed,
|
|
703
724
|
visibility,
|
|
704
|
-
true
|
|
725
|
+
true,
|
|
705
726
|
);
|
|
706
727
|
}
|
package/src/engine/visibility.ts
CHANGED
|
@@ -46,7 +46,7 @@ export interface OptionsVisibilityResult {
|
|
|
46
46
|
*/
|
|
47
47
|
function filterOptionsByContext(
|
|
48
48
|
options: readonly SelectOption[],
|
|
49
|
-
context: EvaluationContext
|
|
49
|
+
context: EvaluationContext,
|
|
50
50
|
): SelectOption[] {
|
|
51
51
|
return options.filter((option) => {
|
|
52
52
|
if (!option.visibleWhen) return true;
|
|
@@ -67,7 +67,7 @@ function processArrayItemOptions(
|
|
|
67
67
|
fieldDef: ArrayFieldDefinition,
|
|
68
68
|
arrayData: readonly unknown[],
|
|
69
69
|
baseContext: EvaluationContext,
|
|
70
|
-
result: Record<string, SelectOption[]
|
|
70
|
+
result: Record<string, SelectOption[]>,
|
|
71
71
|
): void {
|
|
72
72
|
if (!fieldDef.itemFields) return;
|
|
73
73
|
|
|
@@ -79,10 +79,19 @@ function processArrayItemOptions(
|
|
|
79
79
|
itemIndex: i,
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
for (const [itemFieldName, itemFieldDef] of Object.entries(
|
|
83
|
-
|
|
82
|
+
for (const [itemFieldName, itemFieldDef] of Object.entries(
|
|
83
|
+
fieldDef.itemFields,
|
|
84
|
+
)) {
|
|
85
|
+
if (
|
|
86
|
+
isSelectionField(itemFieldDef) &&
|
|
87
|
+
itemFieldDef.options &&
|
|
88
|
+
itemFieldDef.options.length > 0
|
|
89
|
+
) {
|
|
84
90
|
const itemFieldPath = `${arrayPath}[${i}].${itemFieldName}`;
|
|
85
|
-
result[itemFieldPath] = filterOptionsByContext(
|
|
91
|
+
result[itemFieldPath] = filterOptionsByContext(
|
|
92
|
+
itemFieldDef.options,
|
|
93
|
+
itemContext,
|
|
94
|
+
);
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
}
|
|
@@ -113,7 +122,7 @@ function processArrayItemOptions(
|
|
|
113
122
|
export function getVisibility(
|
|
114
123
|
data: Record<string, unknown>,
|
|
115
124
|
spec: Forma,
|
|
116
|
-
options: VisibilityOptions = {}
|
|
125
|
+
options: VisibilityOptions = {},
|
|
117
126
|
): VisibilityResult {
|
|
118
127
|
const computed = options.computed ?? calculate(data, spec);
|
|
119
128
|
|
|
@@ -143,7 +152,7 @@ function evaluateFieldVisibility(
|
|
|
143
152
|
fieldDef: FieldDefinition,
|
|
144
153
|
data: Record<string, unknown>,
|
|
145
154
|
context: EvaluationContext,
|
|
146
|
-
result: VisibilityResult
|
|
155
|
+
result: VisibilityResult,
|
|
147
156
|
): void {
|
|
148
157
|
if (fieldDef.visibleWhen) {
|
|
149
158
|
result[path] = evaluateBoolean(fieldDef.visibleWhen, context);
|
|
@@ -171,7 +180,7 @@ function evaluateArrayItemVisibility(
|
|
|
171
180
|
fieldDef: ArrayFieldDefinition,
|
|
172
181
|
arrayData: unknown[],
|
|
173
182
|
baseContext: EvaluationContext,
|
|
174
|
-
result: VisibilityResult
|
|
183
|
+
result: VisibilityResult,
|
|
175
184
|
): void {
|
|
176
185
|
if (!fieldDef.itemFields) return;
|
|
177
186
|
|
|
@@ -183,11 +192,16 @@ function evaluateArrayItemVisibility(
|
|
|
183
192
|
itemIndex: i,
|
|
184
193
|
};
|
|
185
194
|
|
|
186
|
-
for (const [fieldName, itemFieldDef] of Object.entries(
|
|
195
|
+
for (const [fieldName, itemFieldDef] of Object.entries(
|
|
196
|
+
fieldDef.itemFields,
|
|
197
|
+
)) {
|
|
187
198
|
const itemFieldPath = `${arrayPath}[${i}].${fieldName}`;
|
|
188
199
|
|
|
189
200
|
if (itemFieldDef.visibleWhen) {
|
|
190
|
-
result[itemFieldPath] = evaluateBoolean(
|
|
201
|
+
result[itemFieldPath] = evaluateBoolean(
|
|
202
|
+
itemFieldDef.visibleWhen,
|
|
203
|
+
itemContext,
|
|
204
|
+
);
|
|
191
205
|
} else {
|
|
192
206
|
result[itemFieldPath] = true;
|
|
193
207
|
}
|
|
@@ -204,7 +218,7 @@ export function isFieldVisible(
|
|
|
204
218
|
fieldPath: string,
|
|
205
219
|
data: Record<string, unknown>,
|
|
206
220
|
spec: Forma,
|
|
207
|
-
options: VisibilityOptions = {}
|
|
221
|
+
options: VisibilityOptions = {},
|
|
208
222
|
): boolean {
|
|
209
223
|
const fieldDef = spec.fields[fieldPath];
|
|
210
224
|
if (!fieldDef) {
|
|
@@ -235,7 +249,7 @@ export function isFieldVisible(
|
|
|
235
249
|
export function getPageVisibility(
|
|
236
250
|
data: Record<string, unknown>,
|
|
237
251
|
spec: Forma,
|
|
238
|
-
options: VisibilityOptions = {}
|
|
252
|
+
options: VisibilityOptions = {},
|
|
239
253
|
): Record<string, boolean> {
|
|
240
254
|
if (!spec.pages) {
|
|
241
255
|
return {};
|
|
@@ -294,7 +308,7 @@ export function getPageVisibility(
|
|
|
294
308
|
export function getOptionsVisibility(
|
|
295
309
|
data: Record<string, unknown>,
|
|
296
310
|
spec: Forma,
|
|
297
|
-
options: VisibilityOptions = {}
|
|
311
|
+
options: VisibilityOptions = {},
|
|
298
312
|
): OptionsVisibilityResult {
|
|
299
313
|
const computed = options.computed ?? calculate(data, spec);
|
|
300
314
|
const result: Record<string, SelectOption[]> = {};
|
|
@@ -310,7 +324,11 @@ export function getOptionsVisibility(
|
|
|
310
324
|
if (!fieldDef) continue;
|
|
311
325
|
|
|
312
326
|
// Top-level fields with options
|
|
313
|
-
if (
|
|
327
|
+
if (
|
|
328
|
+
isSelectionField(fieldDef) &&
|
|
329
|
+
fieldDef.options &&
|
|
330
|
+
fieldDef.options.length > 0
|
|
331
|
+
) {
|
|
314
332
|
result[fieldPath] = filterOptionsByContext(fieldDef.options, baseContext);
|
|
315
333
|
}
|
|
316
334
|
|
|
@@ -318,7 +336,13 @@ export function getOptionsVisibility(
|
|
|
318
336
|
if (isArrayField(fieldDef) && fieldDef.itemFields) {
|
|
319
337
|
const arrayData = data[fieldPath];
|
|
320
338
|
if (Array.isArray(arrayData)) {
|
|
321
|
-
processArrayItemOptions(
|
|
339
|
+
processArrayItemOptions(
|
|
340
|
+
fieldPath,
|
|
341
|
+
fieldDef,
|
|
342
|
+
arrayData,
|
|
343
|
+
baseContext,
|
|
344
|
+
result,
|
|
345
|
+
);
|
|
322
346
|
}
|
|
323
347
|
}
|
|
324
348
|
}
|
|
@@ -360,7 +384,7 @@ export function getVisibleOptions(
|
|
|
360
384
|
computed?: Record<string, unknown>;
|
|
361
385
|
item?: Record<string, unknown>;
|
|
362
386
|
itemIndex?: number;
|
|
363
|
-
} = {}
|
|
387
|
+
} = {},
|
|
364
388
|
): SelectOption[] {
|
|
365
389
|
if (!options || options.length === 0) return [];
|
|
366
390
|
|
package/src/feel/index.ts
CHANGED
|
@@ -110,7 +110,7 @@ function buildFeelContext(ctx: EvaluationContext): Record<string, unknown> {
|
|
|
110
110
|
*/
|
|
111
111
|
export function evaluate<T = unknown>(
|
|
112
112
|
expression: FEELExpression,
|
|
113
|
-
context: EvaluationContext
|
|
113
|
+
context: EvaluationContext,
|
|
114
114
|
): EvaluationOutcome<T> {
|
|
115
115
|
try {
|
|
116
116
|
const feelContext = buildFeelContext(context);
|
|
@@ -140,13 +140,13 @@ export function evaluate<T = unknown>(
|
|
|
140
140
|
*/
|
|
141
141
|
export function evaluateBoolean(
|
|
142
142
|
expression: FEELExpression,
|
|
143
|
-
context: EvaluationContext
|
|
143
|
+
context: EvaluationContext,
|
|
144
144
|
): boolean {
|
|
145
145
|
const result = evaluate<boolean>(expression, context);
|
|
146
146
|
|
|
147
147
|
if (!result.success) {
|
|
148
148
|
console.warn(
|
|
149
|
-
`FEEL expression error: ${result.error}\nExpression: ${result.expression}
|
|
149
|
+
`FEEL expression error: ${result.error}\nExpression: ${result.expression}`,
|
|
150
150
|
);
|
|
151
151
|
return false;
|
|
152
152
|
}
|
|
@@ -166,14 +166,14 @@ export function evaluateBoolean(
|
|
|
166
166
|
if (result.value === null || result.value === undefined) {
|
|
167
167
|
console.warn(
|
|
168
168
|
`[forma] FEEL expression returned null (treating as false): "${expression}"\n` +
|
|
169
|
-
`This often means a referenced field is undefined. See docs for null-safe patterns
|
|
169
|
+
`This often means a referenced field is undefined. See docs for null-safe patterns.`,
|
|
170
170
|
);
|
|
171
171
|
return false;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
if (typeof result.value !== "boolean") {
|
|
175
175
|
console.warn(
|
|
176
|
-
`FEEL expression did not return boolean: ${expression}\nGot: ${typeof result.value}
|
|
176
|
+
`FEEL expression did not return boolean: ${expression}\nGot: ${typeof result.value}`,
|
|
177
177
|
);
|
|
178
178
|
return false;
|
|
179
179
|
}
|
|
@@ -192,20 +192,20 @@ export function evaluateBoolean(
|
|
|
192
192
|
*/
|
|
193
193
|
export function evaluateNumber(
|
|
194
194
|
expression: FEELExpression,
|
|
195
|
-
context: EvaluationContext
|
|
195
|
+
context: EvaluationContext,
|
|
196
196
|
): number | null {
|
|
197
197
|
const result = evaluate<number>(expression, context);
|
|
198
198
|
|
|
199
199
|
if (!result.success) {
|
|
200
200
|
console.warn(
|
|
201
|
-
`FEEL expression error: ${result.error}\nExpression: ${result.expression}
|
|
201
|
+
`FEEL expression error: ${result.error}\nExpression: ${result.expression}`,
|
|
202
202
|
);
|
|
203
203
|
return null;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
if (typeof result.value !== "number") {
|
|
207
207
|
console.warn(
|
|
208
|
-
`FEEL expression did not return number: ${expression}\nGot: ${typeof result.value}
|
|
208
|
+
`FEEL expression did not return number: ${expression}\nGot: ${typeof result.value}`,
|
|
209
209
|
);
|
|
210
210
|
return null;
|
|
211
211
|
}
|
|
@@ -222,20 +222,20 @@ export function evaluateNumber(
|
|
|
222
222
|
*/
|
|
223
223
|
export function evaluateString(
|
|
224
224
|
expression: FEELExpression,
|
|
225
|
-
context: EvaluationContext
|
|
225
|
+
context: EvaluationContext,
|
|
226
226
|
): string | null {
|
|
227
227
|
const result = evaluate<string>(expression, context);
|
|
228
228
|
|
|
229
229
|
if (!result.success) {
|
|
230
230
|
console.warn(
|
|
231
|
-
`FEEL expression error: ${result.error}\nExpression: ${result.expression}
|
|
231
|
+
`FEEL expression error: ${result.error}\nExpression: ${result.expression}`,
|
|
232
232
|
);
|
|
233
233
|
return null;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
if (typeof result.value !== "string") {
|
|
237
237
|
console.warn(
|
|
238
|
-
`FEEL expression did not return string: ${expression}\nGot: ${typeof result.value}
|
|
238
|
+
`FEEL expression did not return string: ${expression}\nGot: ${typeof result.value}`,
|
|
239
239
|
);
|
|
240
240
|
return null;
|
|
241
241
|
}
|
|
@@ -258,7 +258,7 @@ export function evaluateString(
|
|
|
258
258
|
*/
|
|
259
259
|
export function evaluateBooleanBatch(
|
|
260
260
|
expressions: Record<string, FEELExpression>,
|
|
261
|
-
context: EvaluationContext
|
|
261
|
+
context: EvaluationContext,
|
|
262
262
|
): Record<string, boolean> {
|
|
263
263
|
const results: Record<string, boolean> = {};
|
|
264
264
|
|
package/src/format/index.ts
CHANGED
|
@@ -112,7 +112,7 @@ export function parseDecimalFormat(format: string): number | null {
|
|
|
112
112
|
export function formatValue(
|
|
113
113
|
value: unknown,
|
|
114
114
|
format?: string,
|
|
115
|
-
options?: FormatOptions
|
|
115
|
+
options?: FormatOptions,
|
|
116
116
|
): string {
|
|
117
117
|
const { locale = "en-US", currency = "USD", nullDisplay } = options ?? {};
|
|
118
118
|
|