@fogpipe/forma-core 0.6.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.
@@ -0,0 +1,885 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/engine/index.ts
21
+ var engine_exports = {};
22
+ __export(engine_exports, {
23
+ calculate: () => calculate,
24
+ calculateField: () => calculateField,
25
+ calculateWithErrors: () => calculateWithErrors,
26
+ getEnabled: () => getEnabled,
27
+ getFormattedValue: () => getFormattedValue,
28
+ getPageVisibility: () => getPageVisibility,
29
+ getRequired: () => getRequired,
30
+ getVisibility: () => getVisibility,
31
+ isEnabled: () => isEnabled,
32
+ isFieldVisible: () => isFieldVisible,
33
+ isRequired: () => isRequired,
34
+ validate: () => validate,
35
+ validateSingleField: () => validateSingleField
36
+ });
37
+ module.exports = __toCommonJS(engine_exports);
38
+
39
+ // src/feel/index.ts
40
+ var import_feelin = require("feelin");
41
+ function buildFeelContext(ctx) {
42
+ const feelContext = {
43
+ // Spread form data directly so fields are accessible by name
44
+ ...ctx.data
45
+ };
46
+ if (ctx.computed) {
47
+ feelContext["computed"] = ctx.computed;
48
+ }
49
+ if (ctx.referenceData) {
50
+ feelContext["ref"] = ctx.referenceData;
51
+ }
52
+ if (ctx.item !== void 0) {
53
+ feelContext["item"] = ctx.item;
54
+ }
55
+ if (ctx.itemIndex !== void 0) {
56
+ feelContext["itemIndex"] = ctx.itemIndex;
57
+ }
58
+ if (ctx.value !== void 0) {
59
+ feelContext["value"] = ctx.value;
60
+ }
61
+ return feelContext;
62
+ }
63
+ function evaluate(expression, context) {
64
+ try {
65
+ const feelContext = buildFeelContext(context);
66
+ const result = (0, import_feelin.evaluate)(expression, feelContext);
67
+ return {
68
+ success: true,
69
+ value: result
70
+ };
71
+ } catch (error) {
72
+ return {
73
+ success: false,
74
+ error: error instanceof Error ? error.message : String(error),
75
+ expression
76
+ };
77
+ }
78
+ }
79
+ function evaluateBoolean(expression, context) {
80
+ const result = evaluate(expression, context);
81
+ if (!result.success) {
82
+ console.warn(
83
+ `FEEL expression error: ${result.error}
84
+ Expression: ${result.expression}`
85
+ );
86
+ return false;
87
+ }
88
+ if (result.value === null || result.value === void 0) {
89
+ return false;
90
+ }
91
+ if (typeof result.value !== "boolean") {
92
+ console.warn(
93
+ `FEEL expression did not return boolean: ${expression}
94
+ Got: ${typeof result.value}`
95
+ );
96
+ return false;
97
+ }
98
+ return result.value;
99
+ }
100
+
101
+ // src/engine/calculate.ts
102
+ function calculate(data, spec) {
103
+ const result = calculateWithErrors(data, spec);
104
+ return result.values;
105
+ }
106
+ function calculateWithErrors(data, spec) {
107
+ if (!spec.computed) {
108
+ return { values: {}, errors: [] };
109
+ }
110
+ const values = {};
111
+ const errors = [];
112
+ const orderedFields = getComputationOrder(spec.computed);
113
+ for (const fieldName of orderedFields) {
114
+ const fieldDef = spec.computed[fieldName];
115
+ if (!fieldDef) continue;
116
+ const result = evaluateComputedField(
117
+ fieldName,
118
+ fieldDef,
119
+ data,
120
+ values,
121
+ // Pass already-computed values for dependencies
122
+ spec.referenceData
123
+ // Pass reference data for lookups
124
+ );
125
+ if (result.success) {
126
+ values[fieldName] = result.value;
127
+ } else {
128
+ errors.push({
129
+ field: fieldName,
130
+ message: result.error,
131
+ expression: fieldDef.expression
132
+ });
133
+ values[fieldName] = null;
134
+ }
135
+ }
136
+ return { values, errors };
137
+ }
138
+ function evaluateComputedField(_name, fieldDef, data, computedSoFar, referenceData) {
139
+ const referencedComputed = findComputedReferences(fieldDef.expression);
140
+ for (const ref of referencedComputed) {
141
+ if (computedSoFar[ref] === null) {
142
+ return {
143
+ success: true,
144
+ value: null
145
+ };
146
+ }
147
+ }
148
+ const context = {
149
+ data,
150
+ computed: computedSoFar,
151
+ referenceData
152
+ };
153
+ const result = evaluate(fieldDef.expression, context);
154
+ if (!result.success) {
155
+ return {
156
+ success: false,
157
+ error: result.error
158
+ };
159
+ }
160
+ if (typeof result.value === "number" && !Number.isFinite(result.value)) {
161
+ return {
162
+ success: true,
163
+ value: null
164
+ };
165
+ }
166
+ return {
167
+ success: true,
168
+ value: result.value
169
+ };
170
+ }
171
+ function findComputedReferences(expression) {
172
+ const refs = [];
173
+ const regex = /computed\.(\w+)/g;
174
+ let match;
175
+ while ((match = regex.exec(expression)) !== null) {
176
+ refs.push(match[1]);
177
+ }
178
+ return refs;
179
+ }
180
+ function getComputationOrder(computed) {
181
+ const fieldNames = Object.keys(computed);
182
+ const deps = /* @__PURE__ */ new Map();
183
+ for (const name of fieldNames) {
184
+ deps.set(name, findComputedDependencies(computed[name].expression, fieldNames));
185
+ }
186
+ const sorted = [];
187
+ const visited = /* @__PURE__ */ new Set();
188
+ const visiting = /* @__PURE__ */ new Set();
189
+ function visit(name) {
190
+ if (visited.has(name)) return;
191
+ if (visiting.has(name)) {
192
+ console.warn(`Circular dependency detected in computed field: ${name}`);
193
+ sorted.push(name);
194
+ visited.add(name);
195
+ return;
196
+ }
197
+ visiting.add(name);
198
+ const fieldDeps = deps.get(name) ?? /* @__PURE__ */ new Set();
199
+ for (const dep of fieldDeps) {
200
+ visit(dep);
201
+ }
202
+ visiting.delete(name);
203
+ visited.add(name);
204
+ sorted.push(name);
205
+ }
206
+ for (const name of fieldNames) {
207
+ visit(name);
208
+ }
209
+ return sorted;
210
+ }
211
+ function findComputedDependencies(expression, availableFields) {
212
+ const deps = /* @__PURE__ */ new Set();
213
+ const regex = /computed\.(\w+)/g;
214
+ let match;
215
+ while ((match = regex.exec(expression)) !== null) {
216
+ const fieldName = match[1];
217
+ if (availableFields.includes(fieldName)) {
218
+ deps.add(fieldName);
219
+ }
220
+ }
221
+ return deps;
222
+ }
223
+ function getFormattedValue(fieldName, data, spec) {
224
+ var _a;
225
+ if (!((_a = spec.computed) == null ? void 0 : _a[fieldName])) {
226
+ return null;
227
+ }
228
+ const fieldDef = spec.computed[fieldName];
229
+ const computed = calculate(data, spec);
230
+ const value = computed[fieldName];
231
+ if (value === null || value === void 0) {
232
+ return null;
233
+ }
234
+ return formatValue(value, fieldDef.format);
235
+ }
236
+ function formatValue(value, format) {
237
+ if (!format) {
238
+ return String(value);
239
+ }
240
+ const decimalMatch = format.match(/^decimal\((\d+)\)$/);
241
+ if (decimalMatch) {
242
+ const decimals = parseInt(decimalMatch[1], 10);
243
+ return typeof value === "number" ? value.toFixed(decimals) : String(value);
244
+ }
245
+ if (format === "currency") {
246
+ return typeof value === "number" ? new Intl.NumberFormat("en-US", {
247
+ style: "currency",
248
+ currency: "USD"
249
+ }).format(value) : String(value);
250
+ }
251
+ if (format === "percent") {
252
+ return typeof value === "number" ? new Intl.NumberFormat("en-US", {
253
+ style: "percent",
254
+ minimumFractionDigits: 1
255
+ }).format(value) : String(value);
256
+ }
257
+ return String(value);
258
+ }
259
+ function calculateField(fieldName, data, spec) {
260
+ const computed = calculate(data, spec);
261
+ return computed[fieldName] ?? null;
262
+ }
263
+
264
+ // src/engine/visibility.ts
265
+ function getVisibility(data, spec, options = {}) {
266
+ const computed = options.computed ?? calculate(data, spec);
267
+ const baseContext = {
268
+ data,
269
+ computed,
270
+ referenceData: spec.referenceData
271
+ };
272
+ const result = {};
273
+ for (const fieldPath of spec.fieldOrder) {
274
+ const fieldDef = spec.fields[fieldPath];
275
+ if (fieldDef) {
276
+ evaluateFieldVisibility(fieldPath, fieldDef, data, baseContext, result);
277
+ }
278
+ }
279
+ return result;
280
+ }
281
+ function evaluateFieldVisibility(path, fieldDef, data, context, result) {
282
+ if (fieldDef.visibleWhen) {
283
+ result[path] = evaluateBoolean(fieldDef.visibleWhen, context);
284
+ } else {
285
+ result[path] = true;
286
+ }
287
+ if (!result[path]) {
288
+ return;
289
+ }
290
+ if (fieldDef.itemFields) {
291
+ const arrayData = data[path];
292
+ if (Array.isArray(arrayData)) {
293
+ evaluateArrayItemVisibility(path, fieldDef, arrayData, context, result);
294
+ }
295
+ }
296
+ }
297
+ function evaluateArrayItemVisibility(arrayPath, fieldDef, arrayData, baseContext, result) {
298
+ if (!fieldDef.itemFields) return;
299
+ for (let i = 0; i < arrayData.length; i++) {
300
+ const item = arrayData[i];
301
+ const itemContext = {
302
+ ...baseContext,
303
+ item,
304
+ itemIndex: i
305
+ };
306
+ for (const [fieldName, itemFieldDef] of Object.entries(fieldDef.itemFields)) {
307
+ const itemFieldPath = `${arrayPath}[${i}].${fieldName}`;
308
+ if (itemFieldDef.visibleWhen) {
309
+ result[itemFieldPath] = evaluateBoolean(itemFieldDef.visibleWhen, itemContext);
310
+ } else {
311
+ result[itemFieldPath] = true;
312
+ }
313
+ }
314
+ }
315
+ }
316
+ function isFieldVisible(fieldPath, data, spec, options = {}) {
317
+ const fieldDef = spec.fields[fieldPath];
318
+ if (!fieldDef) {
319
+ return true;
320
+ }
321
+ if (!fieldDef.visibleWhen) {
322
+ return true;
323
+ }
324
+ const computed = options.computed ?? calculate(data, spec);
325
+ const context = {
326
+ data,
327
+ computed,
328
+ referenceData: spec.referenceData
329
+ };
330
+ return evaluateBoolean(fieldDef.visibleWhen, context);
331
+ }
332
+ function getPageVisibility(data, spec, options = {}) {
333
+ if (!spec.pages) {
334
+ return {};
335
+ }
336
+ const computed = options.computed ?? calculate(data, spec);
337
+ const context = {
338
+ data,
339
+ computed,
340
+ referenceData: spec.referenceData
341
+ };
342
+ const result = {};
343
+ for (const page of spec.pages) {
344
+ if (page.visibleWhen) {
345
+ result[page.id] = evaluateBoolean(page.visibleWhen, context);
346
+ } else {
347
+ result[page.id] = true;
348
+ }
349
+ }
350
+ return result;
351
+ }
352
+
353
+ // src/engine/required.ts
354
+ function getRequired(data, spec, options = {}) {
355
+ const computed = options.computed ?? calculate(data, spec);
356
+ const context = {
357
+ data,
358
+ computed,
359
+ referenceData: spec.referenceData
360
+ };
361
+ const result = {};
362
+ for (const fieldPath of spec.fieldOrder) {
363
+ const fieldDef = spec.fields[fieldPath];
364
+ if (fieldDef) {
365
+ result[fieldPath] = isFieldRequired(fieldPath, fieldDef, spec, context);
366
+ }
367
+ }
368
+ for (const [fieldPath, fieldDef] of Object.entries(spec.fields)) {
369
+ if (fieldDef.itemFields) {
370
+ const arrayData = data[fieldPath];
371
+ if (Array.isArray(arrayData)) {
372
+ for (let i = 0; i < arrayData.length; i++) {
373
+ const item = arrayData[i];
374
+ const itemContext = {
375
+ data,
376
+ computed,
377
+ referenceData: spec.referenceData,
378
+ item,
379
+ itemIndex: i
380
+ };
381
+ for (const [itemFieldName, itemFieldDef] of Object.entries(fieldDef.itemFields)) {
382
+ const itemFieldPath = `${fieldPath}[${i}].${itemFieldName}`;
383
+ result[itemFieldPath] = isFieldRequired(
384
+ itemFieldPath,
385
+ itemFieldDef,
386
+ spec,
387
+ itemContext
388
+ );
389
+ }
390
+ }
391
+ }
392
+ }
393
+ }
394
+ return result;
395
+ }
396
+ function isFieldRequired(fieldPath, fieldDef, spec, context) {
397
+ var _a;
398
+ if (fieldDef.requiredWhen) {
399
+ return evaluateBoolean(fieldDef.requiredWhen, context);
400
+ }
401
+ return ((_a = spec.schema.required) == null ? void 0 : _a.includes(fieldPath)) ?? false;
402
+ }
403
+ function isRequired(fieldPath, data, spec) {
404
+ var _a;
405
+ const fieldDef = spec.fields[fieldPath];
406
+ if (!fieldDef) {
407
+ return ((_a = spec.schema.required) == null ? void 0 : _a.includes(fieldPath)) ?? false;
408
+ }
409
+ const computed = calculate(data, spec);
410
+ const context = {
411
+ data,
412
+ computed,
413
+ referenceData: spec.referenceData
414
+ };
415
+ return isFieldRequired(fieldPath, fieldDef, spec, context);
416
+ }
417
+
418
+ // src/engine/enabled.ts
419
+ function getEnabled(data, spec, options = {}) {
420
+ const computed = options.computed ?? calculate(data, spec);
421
+ const context = {
422
+ data,
423
+ computed,
424
+ referenceData: spec.referenceData
425
+ };
426
+ const result = {};
427
+ for (const fieldPath of spec.fieldOrder) {
428
+ const fieldDef = spec.fields[fieldPath];
429
+ if (fieldDef) {
430
+ result[fieldPath] = isFieldEnabled(fieldDef, context);
431
+ }
432
+ }
433
+ for (const [fieldPath, fieldDef] of Object.entries(spec.fields)) {
434
+ if (fieldDef.itemFields) {
435
+ const arrayData = data[fieldPath];
436
+ if (Array.isArray(arrayData)) {
437
+ for (let i = 0; i < arrayData.length; i++) {
438
+ const item = arrayData[i];
439
+ const itemContext = {
440
+ data,
441
+ computed,
442
+ referenceData: spec.referenceData,
443
+ item,
444
+ itemIndex: i
445
+ };
446
+ for (const [itemFieldName, itemFieldDef] of Object.entries(fieldDef.itemFields)) {
447
+ const itemFieldPath = `${fieldPath}[${i}].${itemFieldName}`;
448
+ result[itemFieldPath] = isFieldEnabled(itemFieldDef, itemContext);
449
+ }
450
+ }
451
+ }
452
+ }
453
+ }
454
+ return result;
455
+ }
456
+ function isFieldEnabled(fieldDef, context) {
457
+ if (fieldDef.enabledWhen) {
458
+ return evaluateBoolean(fieldDef.enabledWhen, context);
459
+ }
460
+ return true;
461
+ }
462
+ function isEnabled(fieldPath, data, spec) {
463
+ const fieldDef = spec.fields[fieldPath];
464
+ if (!fieldDef) {
465
+ return true;
466
+ }
467
+ if (!fieldDef.enabledWhen) {
468
+ return true;
469
+ }
470
+ const computed = calculate(data, spec);
471
+ const context = {
472
+ data,
473
+ computed,
474
+ referenceData: spec.referenceData
475
+ };
476
+ return evaluateBoolean(fieldDef.enabledWhen, context);
477
+ }
478
+
479
+ // src/engine/validate.ts
480
+ function validate(data, spec, options = {}) {
481
+ const { onlyVisible = true } = options;
482
+ const computed = options.computed ?? calculate(data, spec);
483
+ const visibility = options.visibility ?? getVisibility(data, spec, { computed });
484
+ const errors = [];
485
+ for (const fieldPath of spec.fieldOrder) {
486
+ const fieldDef = spec.fields[fieldPath];
487
+ if (!fieldDef) continue;
488
+ if (onlyVisible && visibility[fieldPath] === false) {
489
+ continue;
490
+ }
491
+ const schemaProperty = spec.schema.properties[fieldPath];
492
+ const fieldErrors = validateField(
493
+ fieldPath,
494
+ data[fieldPath],
495
+ fieldDef,
496
+ schemaProperty,
497
+ spec,
498
+ data,
499
+ computed,
500
+ visibility,
501
+ onlyVisible
502
+ );
503
+ errors.push(...fieldErrors);
504
+ }
505
+ return {
506
+ valid: errors.filter((e) => e.severity === "error").length === 0,
507
+ errors
508
+ };
509
+ }
510
+ function validateField(path, value, fieldDef, schemaProperty, spec, data, computed, visibility, onlyVisible) {
511
+ const errors = [];
512
+ const context = {
513
+ data,
514
+ computed,
515
+ referenceData: spec.referenceData,
516
+ value
517
+ };
518
+ const required = isFieldRequired(path, fieldDef, spec, context);
519
+ if (required && isEmpty(value)) {
520
+ errors.push({
521
+ field: path,
522
+ message: fieldDef.label ? `${fieldDef.label} is required` : "This field is required",
523
+ severity: "error"
524
+ });
525
+ }
526
+ if (!isEmpty(value) && schemaProperty) {
527
+ const typeError = validateType(path, value, schemaProperty, fieldDef);
528
+ if (typeError) {
529
+ errors.push(typeError);
530
+ }
531
+ }
532
+ if (fieldDef.validations && !isEmpty(value)) {
533
+ const customErrors = validateCustomRules(path, fieldDef.validations, context);
534
+ errors.push(...customErrors);
535
+ }
536
+ if (Array.isArray(value) && fieldDef.itemFields) {
537
+ const arrayErrors = validateArray(
538
+ path,
539
+ value,
540
+ fieldDef,
541
+ spec,
542
+ data,
543
+ computed,
544
+ visibility,
545
+ onlyVisible
546
+ );
547
+ errors.push(...arrayErrors);
548
+ }
549
+ return errors;
550
+ }
551
+ function isEmpty(value) {
552
+ if (value === null || value === void 0) return true;
553
+ if (typeof value === "string" && value.trim() === "") return true;
554
+ if (Array.isArray(value) && value.length === 0) return true;
555
+ return false;
556
+ }
557
+ function validateType(path, value, schema, fieldDef) {
558
+ const label = fieldDef.label ?? path;
559
+ switch (schema.type) {
560
+ case "string": {
561
+ if (typeof value !== "string") {
562
+ return {
563
+ field: path,
564
+ message: `${label} must be a string`,
565
+ severity: "error"
566
+ };
567
+ }
568
+ if ("minLength" in schema && schema.minLength !== void 0) {
569
+ if (value.length < schema.minLength) {
570
+ return {
571
+ field: path,
572
+ message: `${label} must be at least ${schema.minLength} characters`,
573
+ severity: "error"
574
+ };
575
+ }
576
+ }
577
+ if ("maxLength" in schema && schema.maxLength !== void 0) {
578
+ if (value.length > schema.maxLength) {
579
+ return {
580
+ field: path,
581
+ message: `${label} must be no more than ${schema.maxLength} characters`,
582
+ severity: "error"
583
+ };
584
+ }
585
+ }
586
+ if ("pattern" in schema && schema.pattern) {
587
+ const regex = new RegExp(schema.pattern);
588
+ if (!regex.test(value)) {
589
+ return {
590
+ field: path,
591
+ message: `${label} format is invalid`,
592
+ severity: "error"
593
+ };
594
+ }
595
+ }
596
+ if ("enum" in schema && schema.enum) {
597
+ if (!schema.enum.includes(value)) {
598
+ return {
599
+ field: path,
600
+ message: `${label} must be one of: ${schema.enum.join(", ")}`,
601
+ severity: "error"
602
+ };
603
+ }
604
+ }
605
+ if ("format" in schema && schema.format) {
606
+ const formatError = validateFormat(path, value, schema.format, label);
607
+ if (formatError) return formatError;
608
+ }
609
+ return null;
610
+ }
611
+ case "number":
612
+ case "integer": {
613
+ if (typeof value !== "number") {
614
+ return {
615
+ field: path,
616
+ message: `${label} must be a number`,
617
+ severity: "error"
618
+ };
619
+ }
620
+ if (schema.type === "integer" && !Number.isInteger(value)) {
621
+ return {
622
+ field: path,
623
+ message: `${label} must be a whole number`,
624
+ severity: "error"
625
+ };
626
+ }
627
+ if ("minimum" in schema && schema.minimum !== void 0) {
628
+ if (value < schema.minimum) {
629
+ return {
630
+ field: path,
631
+ message: `${label} must be at least ${schema.minimum}`,
632
+ severity: "error"
633
+ };
634
+ }
635
+ }
636
+ if ("maximum" in schema && schema.maximum !== void 0) {
637
+ if (value > schema.maximum) {
638
+ return {
639
+ field: path,
640
+ message: `${label} must be no more than ${schema.maximum}`,
641
+ severity: "error"
642
+ };
643
+ }
644
+ }
645
+ if ("exclusiveMinimum" in schema && schema.exclusiveMinimum !== void 0) {
646
+ if (value <= schema.exclusiveMinimum) {
647
+ return {
648
+ field: path,
649
+ message: `${label} must be greater than ${schema.exclusiveMinimum}`,
650
+ severity: "error"
651
+ };
652
+ }
653
+ }
654
+ if ("exclusiveMaximum" in schema && schema.exclusiveMaximum !== void 0) {
655
+ if (value >= schema.exclusiveMaximum) {
656
+ return {
657
+ field: path,
658
+ message: `${label} must be less than ${schema.exclusiveMaximum}`,
659
+ severity: "error"
660
+ };
661
+ }
662
+ }
663
+ return null;
664
+ }
665
+ case "boolean": {
666
+ if (typeof value !== "boolean") {
667
+ return {
668
+ field: path,
669
+ message: `${label} must be true or false`,
670
+ severity: "error"
671
+ };
672
+ }
673
+ return null;
674
+ }
675
+ case "array": {
676
+ if (!Array.isArray(value)) {
677
+ return {
678
+ field: path,
679
+ message: `${label} must be a list`,
680
+ severity: "error"
681
+ };
682
+ }
683
+ return null;
684
+ }
685
+ case "object": {
686
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
687
+ return {
688
+ field: path,
689
+ message: `${label} must be an object`,
690
+ severity: "error"
691
+ };
692
+ }
693
+ return null;
694
+ }
695
+ default:
696
+ return null;
697
+ }
698
+ }
699
+ function validateFormat(path, value, format, label) {
700
+ switch (format) {
701
+ case "email": {
702
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
703
+ if (!emailRegex.test(value)) {
704
+ return {
705
+ field: path,
706
+ message: `${label} must be a valid email address`,
707
+ severity: "error"
708
+ };
709
+ }
710
+ return null;
711
+ }
712
+ case "date": {
713
+ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
714
+ if (!dateRegex.test(value)) {
715
+ return {
716
+ field: path,
717
+ message: `${label} must be a valid date`,
718
+ severity: "error"
719
+ };
720
+ }
721
+ const parsed = /* @__PURE__ */ new Date(value + "T00:00:00Z");
722
+ if (isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value) {
723
+ return {
724
+ field: path,
725
+ message: `${label} must be a valid date`,
726
+ severity: "error"
727
+ };
728
+ }
729
+ return null;
730
+ }
731
+ case "date-time": {
732
+ if (isNaN(Date.parse(value))) {
733
+ return {
734
+ field: path,
735
+ message: `${label} must be a valid date and time`,
736
+ severity: "error"
737
+ };
738
+ }
739
+ return null;
740
+ }
741
+ case "uri": {
742
+ try {
743
+ new URL(value);
744
+ return null;
745
+ } catch {
746
+ return {
747
+ field: path,
748
+ message: `${label} must be a valid URL`,
749
+ severity: "error"
750
+ };
751
+ }
752
+ }
753
+ case "uuid": {
754
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
755
+ if (!uuidRegex.test(value)) {
756
+ return {
757
+ field: path,
758
+ message: `${label} must be a valid UUID`,
759
+ severity: "error"
760
+ };
761
+ }
762
+ return null;
763
+ }
764
+ default:
765
+ return null;
766
+ }
767
+ }
768
+ function validateCustomRules(path, rules, context) {
769
+ const errors = [];
770
+ for (const rule of rules) {
771
+ const isValid = evaluateBoolean(rule.rule, context);
772
+ if (!isValid) {
773
+ errors.push({
774
+ field: path,
775
+ message: rule.message,
776
+ severity: rule.severity ?? "error"
777
+ });
778
+ }
779
+ }
780
+ return errors;
781
+ }
782
+ function validateArray(path, value, fieldDef, spec, data, computed, visibility, onlyVisible) {
783
+ const errors = [];
784
+ const label = fieldDef.label ?? path;
785
+ if (fieldDef.minItems !== void 0 && value.length < fieldDef.minItems) {
786
+ errors.push({
787
+ field: path,
788
+ message: `${label} must have at least ${fieldDef.minItems} items`,
789
+ severity: "error"
790
+ });
791
+ }
792
+ if (fieldDef.maxItems !== void 0 && value.length > fieldDef.maxItems) {
793
+ errors.push({
794
+ field: path,
795
+ message: `${label} must have no more than ${fieldDef.maxItems} items`,
796
+ severity: "error"
797
+ });
798
+ }
799
+ if (fieldDef.itemFields) {
800
+ for (let i = 0; i < value.length; i++) {
801
+ const item = value[i];
802
+ const itemErrors = validateArrayItem(
803
+ path,
804
+ i,
805
+ item,
806
+ fieldDef.itemFields,
807
+ spec,
808
+ data,
809
+ computed,
810
+ visibility,
811
+ onlyVisible
812
+ );
813
+ errors.push(...itemErrors);
814
+ }
815
+ }
816
+ return errors;
817
+ }
818
+ function validateArrayItem(arrayPath, index, item, itemFields, spec, data, computed, visibility, onlyVisible) {
819
+ const errors = [];
820
+ for (const [fieldName, fieldDef] of Object.entries(itemFields)) {
821
+ const itemFieldPath = `${arrayPath}[${index}].${fieldName}`;
822
+ if (onlyVisible && visibility[itemFieldPath] === false) {
823
+ continue;
824
+ }
825
+ const value = item[fieldName];
826
+ const context = {
827
+ data,
828
+ computed,
829
+ referenceData: spec.referenceData,
830
+ item,
831
+ itemIndex: index,
832
+ value
833
+ };
834
+ const isRequired2 = fieldDef.requiredWhen ? evaluateBoolean(fieldDef.requiredWhen, context) : false;
835
+ if (isRequired2 && isEmpty(value)) {
836
+ errors.push({
837
+ field: itemFieldPath,
838
+ message: fieldDef.label ? `${fieldDef.label} is required` : "This field is required",
839
+ severity: "error"
840
+ });
841
+ }
842
+ if (fieldDef.validations && !isEmpty(value)) {
843
+ const customErrors = validateCustomRules(itemFieldPath, fieldDef.validations, context);
844
+ errors.push(...customErrors);
845
+ }
846
+ }
847
+ return errors;
848
+ }
849
+ function validateSingleField(fieldPath, data, spec) {
850
+ const fieldDef = spec.fields[fieldPath];
851
+ if (!fieldDef) {
852
+ return [];
853
+ }
854
+ const computed = calculate(data, spec);
855
+ const visibility = getVisibility(data, spec, { computed });
856
+ const schemaProperty = spec.schema.properties[fieldPath];
857
+ return validateField(
858
+ fieldPath,
859
+ data[fieldPath],
860
+ fieldDef,
861
+ schemaProperty,
862
+ spec,
863
+ data,
864
+ computed,
865
+ visibility,
866
+ true
867
+ );
868
+ }
869
+ // Annotate the CommonJS export names for ESM import in node:
870
+ 0 && (module.exports = {
871
+ calculate,
872
+ calculateField,
873
+ calculateWithErrors,
874
+ getEnabled,
875
+ getFormattedValue,
876
+ getPageVisibility,
877
+ getRequired,
878
+ getVisibility,
879
+ isEnabled,
880
+ isFieldVisible,
881
+ isRequired,
882
+ validate,
883
+ validateSingleField
884
+ });
885
+ //# sourceMappingURL=index.cjs.map