@frt-platform/report-core 1.4.0 → 1.5.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/dist/index.mjs CHANGED
@@ -10,7 +10,6 @@ var REPORT_TEMPLATE_FIELD_TYPES = [
10
10
  "singleSelect",
11
11
  "multiSelect",
12
12
  "repeatGroup"
13
- // NEW FIELD TYPE
14
13
  ];
15
14
  var Condition = z.lazy(
16
15
  () => z.union([
@@ -37,15 +36,17 @@ var BaseFieldSchema = z.object({
37
36
  /^[a-z0-9_-]+$/,
38
37
  "Use lowercase letters, numbers, underscores, or dashes for field identifiers."
39
38
  ),
40
- type: z.enum([
41
- "shortText",
42
- "longText",
43
- "number",
44
- "date",
45
- "checkbox",
46
- "singleSelect",
47
- "multiSelect"
48
- ]),
39
+ type: z.enum(
40
+ [
41
+ "shortText",
42
+ "longText",
43
+ "number",
44
+ "date",
45
+ "checkbox",
46
+ "singleSelect",
47
+ "multiSelect"
48
+ ]
49
+ ),
49
50
  label: z.string().min(1, "Field labels cannot be empty.").max(200, "Field labels must be 200 characters or fewer."),
50
51
  required: z.boolean().optional(),
51
52
  description: z.string().max(400, "Descriptions must be 400 characters or fewer.").optional(),
@@ -75,7 +76,9 @@ var BaseFieldSchema = z.object({
75
76
  minSelections: z.number().int().min(0).optional(),
76
77
  maxSelections: z.number().int().min(0).optional(),
77
78
  // Privacy
78
- dataClassification: z.enum(["none", "personal", "special"]).optional()
79
+ dataClassification: z.enum(["none", "personal", "special"]).optional(),
80
+ // Computed expression (backend/platform evaluated)
81
+ computed: z.string().max(200, "Computed expressions must be 200 characters or fewer.").optional()
79
82
  });
80
83
  var RepeatGroupFieldSchema = z.lazy(
81
84
  () => z.object({
@@ -88,24 +91,19 @@ var RepeatGroupFieldSchema = z.lazy(
88
91
  // Conditional logic for the whole group
89
92
  visibleIf: Condition.optional(),
90
93
  requiredIf: Condition.optional(),
91
- // NEW: Dynamic and static min/max
94
+ // Static + dynamic min/max
92
95
  min: z.number().int().min(0).optional(),
93
96
  max: z.number().int().min(1).optional(),
94
97
  minIf: Condition.optional(),
95
98
  maxIf: Condition.optional(),
96
99
  // Contents: nested fields (recursive)
97
- fields: z.array(
98
- z.lazy(() => ReportTemplateFieldSchema)
99
- ).min(1, "Repeat groups must contain at least one field."),
100
+ fields: z.array(z.lazy(() => ReportTemplateFieldSchema)).min(1, "Repeat groups must contain at least one field."),
100
101
  // Pre-populated rows allowed
101
102
  defaultValue: z.array(z.record(z.any())).optional()
102
103
  })
103
104
  );
104
105
  var ReportTemplateFieldSchema = z.lazy(
105
- () => z.union([
106
- BaseFieldSchema,
107
- RepeatGroupFieldSchema
108
- ])
106
+ () => z.union([BaseFieldSchema, RepeatGroupFieldSchema])
109
107
  );
110
108
  var ReportTemplateSectionSchema = z.object({
111
109
  id: z.string().min(1, "Section identifiers cannot be empty.").max(60, "Section identifiers must be 60 characters or fewer.").regex(
@@ -123,62 +121,82 @@ var ReportTemplateSchemaValidator = z.object({
123
121
  sections: z.array(ReportTemplateSectionSchema).max(25, "Templates can include at most 25 sections.").default([])
124
122
  });
125
123
 
124
+ // src/template/ids.ts
125
+ function createUniqueId(prefix, existing) {
126
+ const used = new Set(existing);
127
+ let attempt = used.size + 1;
128
+ let candidate = `${prefix}-${attempt}`;
129
+ while (used.has(candidate)) {
130
+ attempt += 1;
131
+ candidate = `${prefix}-${attempt}`;
132
+ }
133
+ return candidate;
134
+ }
135
+
126
136
  // src/template/migrate.ts
127
- var LEGACY_FIELD_TYPE_MAP = {
128
- shorttext: "shortText",
129
- text: "shortText",
130
- "text-box": "shortText",
131
- longtext: "longText",
132
- textarea: "longText",
133
- paragraph: "longText",
134
- number: "number",
135
- numeric: "number",
136
- date: "date",
137
- checkbox: "checkbox",
138
- boolean: "checkbox",
139
- singleselect: "singleSelect",
140
- select: "singleSelect",
141
- dropdown: "singleSelect",
142
- radio: "singleSelect",
143
- "multi-select": "multiSelect",
144
- multiselect: "multiSelect",
145
- checkboxes: "multiSelect"
146
- };
137
+ function mapLegacyFieldType(type) {
138
+ if (!type) return "shortText";
139
+ const t = type.toLowerCase();
140
+ if (t === "text" || t === "short_text" || t === "input") return "shortText";
141
+ if (t === "textarea" || t === "long_text") return "longText";
142
+ if (t === "number" || t === "numeric") return "number";
143
+ if (t === "date" || t === "datetime" || t === "datetime-local") return "date";
144
+ if (t === "checkbox") return "checkbox";
145
+ if (t === "checkboxes" || t === "multi_select" || t === "multiselect") {
146
+ return "multiSelect";
147
+ }
148
+ if (t === "select" || t === "single_select" || t === "dropdown") {
149
+ return "singleSelect";
150
+ }
151
+ return type;
152
+ }
147
153
  function migrateLegacySchema(raw) {
148
- if (!raw || typeof raw !== "object") return raw;
149
- const obj = structuredClone(raw);
150
- if (obj.fields) {
151
- obj.sections = [
154
+ if (!raw || typeof raw !== "object") {
155
+ return {
156
+ version: REPORT_TEMPLATE_VERSION,
157
+ sections: []
158
+ };
159
+ }
160
+ const legacy = raw;
161
+ const version = typeof legacy.version === "number" ? legacy.version : REPORT_TEMPLATE_VERSION;
162
+ function mapFields(fields) {
163
+ if (!Array.isArray(fields)) return [];
164
+ let counter = 0;
165
+ return fields.map((field) => {
166
+ counter += 1;
167
+ const fallbackId = `field_${counter}`;
168
+ return {
169
+ ...field,
170
+ id: field.id ?? fallbackId,
171
+ type: mapLegacyFieldType(field.type)
172
+ };
173
+ });
174
+ }
175
+ let sections;
176
+ if (Array.isArray(legacy.sections)) {
177
+ sections = legacy.sections.map((sec, index) => ({
178
+ id: sec.id ?? `section_${index + 1}`,
179
+ title: sec.title ?? legacy.title,
180
+ description: sec.description ?? legacy.description,
181
+ fields: mapFields(sec.fields)
182
+ }));
183
+ } else if (Array.isArray(legacy.fields)) {
184
+ sections = [
152
185
  {
153
186
  id: "section_1",
154
- // ⬅ underscore to match normalizer/tests
155
- title: obj.title,
156
- description: obj.description,
157
- fields: obj.fields.map((f, i) => ({
158
- ...f,
159
- id: f.id || `field_${i + 1}`,
160
- // ⬅ underscore here too
161
- type: LEGACY_FIELD_TYPE_MAP[f.type?.toLowerCase()] ?? f.type
162
- }))
187
+ title: legacy.title,
188
+ description: legacy.description,
189
+ fields: mapFields(legacy.fields)
163
190
  }
164
191
  ];
165
- delete obj.fields;
166
- return obj;
167
- }
168
- if (Array.isArray(obj.sections)) {
169
- obj.sections = obj.sections.map((sec, i) => ({
170
- ...sec,
171
- id: sec.id || `section_${i + 1}`,
172
- // ⬅ underscore
173
- fields: (sec.fields || []).map((f, idx) => ({
174
- ...f,
175
- id: f.id || `field_${idx + 1}`,
176
- // ⬅ underscore
177
- type: LEGACY_FIELD_TYPE_MAP[f.type?.toLowerCase()] ?? f.type
178
- }))
179
- }));
192
+ } else {
193
+ sections = legacy.sections ?? [];
180
194
  }
181
- return obj;
195
+ return {
196
+ ...legacy,
197
+ version,
198
+ sections
199
+ };
182
200
  }
183
201
 
184
202
  // src/template/normalize.ts
@@ -290,27 +308,8 @@ function serializeReportTemplateSchema(schema, options = {}) {
290
308
  }
291
309
 
292
310
  // src/template/diff.ts
293
- function indexById(items) {
294
- const map = {};
295
- items.forEach((item, i) => map[item.id] = i);
296
- return map;
297
- }
298
- function diffObjectProperties(before, after, ignoreKeys = []) {
299
- const changes = [];
300
- const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
301
- for (const key of keys) {
302
- if (ignoreKeys.includes(key)) continue;
303
- const b = before[key];
304
- const a = after[key];
305
- const changed = Array.isArray(b) && Array.isArray(a) ? JSON.stringify(b) !== JSON.stringify(a) : b !== a;
306
- if (changed) {
307
- changes.push({ key, before: b, after: a });
308
- }
309
- }
310
- return changes;
311
- }
312
- function diffRepeatGroupFields(beforeFields, afterFields, sectionId, groupId) {
313
- const empty = {
311
+ function createEmptyDiff() {
312
+ return {
314
313
  addedSections: [],
315
314
  removedSections: [],
316
315
  reorderedSections: [],
@@ -321,137 +320,191 @@ function diffRepeatGroupFields(beforeFields, afterFields, sectionId, groupId) {
321
320
  modifiedFields: [],
322
321
  nestedFieldDiffs: []
323
322
  };
324
- const beforeIndex = indexById(beforeFields);
325
- const afterIndex = indexById(afterFields);
326
- for (const f of afterFields) {
327
- if (!(f.id in beforeIndex)) {
328
- empty.addedFields.push({ sectionId, fieldId: f.id, index: afterIndex[f.id] });
323
+ }
324
+ function shallowDiff(before, after, ignoreKeys) {
325
+ const changes = [];
326
+ const keys = /* @__PURE__ */ new Set([
327
+ ...Object.keys(before ?? {}),
328
+ ...Object.keys(after ?? {})
329
+ ]);
330
+ for (const key of keys) {
331
+ if (ignoreKeys.has(key)) continue;
332
+ const beforeVal = before[key];
333
+ const afterVal = after[key];
334
+ if (beforeVal !== afterVal) {
335
+ changes.push({ key, before: beforeVal, after: afterVal });
329
336
  }
330
337
  }
331
- for (const f of beforeFields) {
332
- if (!(f.id in afterIndex)) {
333
- empty.removedFields.push({ sectionId, fieldId: f.id, index: beforeIndex[f.id] });
338
+ return changes;
339
+ }
340
+ function diffTemplates(before, after) {
341
+ const diff = createEmptyDiff();
342
+ const beforeSections = before.sections ?? [];
343
+ const afterSections = after.sections ?? [];
344
+ const beforeSectionIndex = /* @__PURE__ */ new Map();
345
+ const afterSectionIndex = /* @__PURE__ */ new Map();
346
+ beforeSections.forEach((s, idx) => beforeSectionIndex.set(s.id, idx));
347
+ afterSections.forEach((s, idx) => afterSectionIndex.set(s.id, idx));
348
+ for (const [id, idx] of afterSectionIndex) {
349
+ if (!beforeSectionIndex.has(id)) {
350
+ diff.addedSections.push({ sectionId: id, index: idx });
334
351
  }
335
352
  }
336
- for (const f of afterFields) {
337
- if (f.id in beforeIndex) {
338
- const from = beforeIndex[f.id];
339
- const to = afterIndex[f.id];
340
- if (from !== to) {
341
- empty.reorderedFields.push({
342
- sectionId,
343
- fieldId: f.id,
344
- from,
345
- to
346
- });
347
- }
353
+ for (const [id, idx] of beforeSectionIndex) {
354
+ if (!afterSectionIndex.has(id)) {
355
+ diff.removedSections.push({ sectionId: id, index: idx });
348
356
  }
349
357
  }
350
- for (const f of afterFields) {
351
- const idx = beforeIndex[f.id];
352
- if (idx == null) continue;
353
- const before = beforeFields[idx];
354
- const changes = diffObjectProperties(before, f, ["id", "fields"]);
355
- if (changes.length > 0) {
356
- empty.modifiedFields.push({
357
- sectionId,
358
- fieldId: f.id,
359
- before,
360
- after: f,
361
- changes
358
+ for (const [id, beforeIdx] of beforeSectionIndex) {
359
+ const afterIdx = afterSectionIndex.get(id);
360
+ if (afterIdx != null && afterIdx !== beforeIdx) {
361
+ diff.reorderedSections.push({
362
+ sectionId: id,
363
+ from: beforeIdx,
364
+ to: afterIdx
362
365
  });
363
366
  }
364
- if (f.type === "repeatGroup" && Array.isArray(f.fields)) {
365
- const innerBefore = before.fields ?? [];
366
- const innerAfter = f.fields;
367
- const nested = diffRepeatGroupFields(
368
- innerBefore,
369
- innerAfter,
370
- sectionId,
371
- f.id
367
+ }
368
+ function getSectionById(sections, id) {
369
+ return sections.find((s) => s.id === id);
370
+ }
371
+ for (const [id] of beforeSectionIndex) {
372
+ if (!afterSectionIndex.has(id)) continue;
373
+ const beforeSec = getSectionById(beforeSections, id);
374
+ const afterSec = getSectionById(afterSections, id);
375
+ const changes = shallowDiff(
376
+ beforeSec,
377
+ afterSec,
378
+ /* @__PURE__ */ new Set(["id", "fields"])
379
+ );
380
+ if (changes.length > 0) {
381
+ diff.modifiedSections.push({ sectionId: id, changes });
382
+ }
383
+ }
384
+ function diffFieldsForSection(sectionId, beforeFields, afterFields) {
385
+ const beforeIndex = /* @__PURE__ */ new Map();
386
+ const afterIndex = /* @__PURE__ */ new Map();
387
+ beforeFields.forEach((f, idx) => beforeIndex.set(f.id, idx));
388
+ afterFields.forEach((f, idx) => afterIndex.set(f.id, idx));
389
+ for (const [fieldId, idx] of afterIndex) {
390
+ if (!beforeIndex.has(fieldId)) {
391
+ diff.addedFields.push({ sectionId, fieldId, index: idx });
392
+ }
393
+ }
394
+ for (const [fieldId, idx] of beforeIndex) {
395
+ if (!afterIndex.has(fieldId)) {
396
+ diff.removedFields.push({ sectionId, fieldId, index: idx });
397
+ }
398
+ }
399
+ for (const [fieldId, beforeIdx] of beforeIndex) {
400
+ const afterIdx = afterIndex.get(fieldId);
401
+ if (afterIdx != null && afterIdx !== beforeIdx) {
402
+ diff.reorderedFields.push({
403
+ sectionId,
404
+ fieldId,
405
+ from: beforeIdx,
406
+ to: afterIdx
407
+ });
408
+ }
409
+ }
410
+ const ignoreFieldKeys = /* @__PURE__ */ new Set(["id", "fields"]);
411
+ const getField = (fields, id) => fields.find((f) => f.id === id);
412
+ for (const [fieldId] of beforeIndex) {
413
+ if (!afterIndex.has(fieldId)) continue;
414
+ const beforeField = getField(beforeFields, fieldId);
415
+ const afterField = getField(afterFields, fieldId);
416
+ const changes = shallowDiff(
417
+ beforeField,
418
+ afterField,
419
+ ignoreFieldKeys
372
420
  );
373
- if (nested.addedFields.length > 0 || nested.removedFields.length > 0 || nested.reorderedFields.length > 0 || nested.modifiedFields.length > 0) {
374
- empty.nestedFieldDiffs.push({
421
+ if (changes.length > 0) {
422
+ diff.modifiedFields.push({
375
423
  sectionId,
376
- groupId: f.id,
377
- diffs: nested
424
+ fieldId,
425
+ changes
378
426
  });
379
427
  }
428
+ if (beforeField.type === "repeatGroup" && afterField.type === "repeatGroup") {
429
+ const nestedBeforeFields = beforeField.fields ?? [];
430
+ const nestedAfterFields = afterField.fields ?? [];
431
+ const nestedDiff = createEmptyDiff();
432
+ diffFieldsInto(
433
+ sectionId,
434
+ fieldId,
435
+ nestedBeforeFields,
436
+ nestedAfterFields,
437
+ nestedDiff
438
+ );
439
+ if (hasAnyFieldDiff(nestedDiff)) {
440
+ diff.nestedFieldDiffs.push({
441
+ sectionId,
442
+ groupId: fieldId,
443
+ diffs: nestedDiff
444
+ });
445
+ }
446
+ }
380
447
  }
381
448
  }
382
- return empty;
383
- }
384
- function diffTemplates(before, after) {
385
- const diff = {
386
- addedSections: [],
387
- removedSections: [],
388
- reorderedSections: [],
389
- modifiedSections: [],
390
- addedFields: [],
391
- removedFields: [],
392
- reorderedFields: [],
393
- modifiedFields: [],
394
- nestedFieldDiffs: []
395
- };
396
- const beforeSections = before.sections;
397
- const afterSections = after.sections;
398
- const beforeSecIndex = indexById(beforeSections);
399
- const afterSecIndex = indexById(afterSections);
400
- for (const sec of afterSections) {
401
- if (!(sec.id in beforeSecIndex)) {
402
- diff.addedSections.push({
403
- sectionId: sec.id,
404
- index: afterSecIndex[sec.id]
405
- });
449
+ function diffFieldsInto(sectionId, groupId, beforeFields, afterFields, target) {
450
+ const beforeIndex = /* @__PURE__ */ new Map();
451
+ const afterIndex = /* @__PURE__ */ new Map();
452
+ beforeFields.forEach((f, idx) => beforeIndex.set(f.id, idx));
453
+ afterFields.forEach((f, idx) => afterIndex.set(f.id, idx));
454
+ for (const [fieldId, idx] of afterIndex) {
455
+ if (!beforeIndex.has(fieldId)) {
456
+ target.addedFields.push({ sectionId, fieldId, index: idx });
457
+ }
406
458
  }
407
- }
408
- for (const sec of beforeSections) {
409
- if (!(sec.id in afterSecIndex)) {
410
- diff.removedSections.push({
411
- sectionId: sec.id,
412
- index: beforeSecIndex[sec.id]
413
- });
459
+ for (const [fieldId, idx] of beforeIndex) {
460
+ if (!afterIndex.has(fieldId)) {
461
+ target.removedFields.push({ sectionId, fieldId, index: idx });
462
+ }
414
463
  }
415
- }
416
- for (const sec of afterSections) {
417
- if (sec.id in beforeSecIndex) {
418
- const from = beforeSecIndex[sec.id];
419
- const to = afterSecIndex[sec.id];
420
- if (from !== to) {
421
- diff.reorderedSections.push({ sectionId: sec.id, from, to });
464
+ for (const [fieldId, beforeIdx] of beforeIndex) {
465
+ const afterIdx = afterIndex.get(fieldId);
466
+ if (afterIdx != null && afterIdx !== beforeIdx) {
467
+ target.reorderedFields.push({
468
+ sectionId,
469
+ fieldId,
470
+ from: beforeIdx,
471
+ to: afterIdx
472
+ });
422
473
  }
423
474
  }
424
- }
425
- for (const sec of afterSections) {
426
- const idx = beforeSecIndex[sec.id];
427
- if (idx == null) continue;
428
- const beforeSec = beforeSections[idx];
429
- const changes = diffObjectProperties(beforeSec, sec, ["id", "fields"]);
430
- if (changes.length > 0) {
431
- diff.modifiedSections.push({
432
- sectionId: sec.id,
433
- changes
434
- });
475
+ const ignoreFieldKeys = /* @__PURE__ */ new Set(["id", "fields"]);
476
+ const getField = (fields, id) => fields.find((f) => f.id === id);
477
+ for (const [fieldId] of beforeIndex) {
478
+ if (!afterIndex.has(fieldId)) continue;
479
+ const beforeField = getField(beforeFields, fieldId);
480
+ const afterField = getField(afterFields, fieldId);
481
+ const changes = shallowDiff(
482
+ beforeField,
483
+ afterField,
484
+ ignoreFieldKeys
485
+ );
486
+ if (changes.length > 0) {
487
+ target.modifiedFields.push({
488
+ sectionId,
489
+ fieldId,
490
+ changes
491
+ });
492
+ }
435
493
  }
436
494
  }
437
- for (const afterSec of afterSections) {
438
- const secId = afterSec.id;
439
- const beforeIdx = beforeSecIndex[secId];
440
- if (beforeIdx == null) continue;
495
+ function hasAnyFieldDiff(d) {
496
+ return d.addedFields.length > 0 || d.removedFields.length > 0 || d.reorderedFields.length > 0 || d.modifiedFields.length > 0 || d.addedSections.length > 0 || d.removedSections.length > 0 || d.reorderedSections.length > 0 || d.modifiedSections.length > 0 || d.nestedFieldDiffs.length > 0;
497
+ }
498
+ for (const [id, beforeIdx] of beforeSectionIndex) {
499
+ const afterIdx = afterSectionIndex.get(id);
500
+ if (afterIdx == null) continue;
441
501
  const beforeSec = beforeSections[beforeIdx];
442
- const beforeFields = beforeSec.fields ?? [];
443
- const afterFields = afterSec.fields ?? [];
444
- const nestedDiff = diffRepeatGroupFields(
445
- beforeFields,
446
- afterFields,
447
- secId,
448
- ""
502
+ const afterSec = afterSections[afterIdx];
503
+ diffFieldsForSection(
504
+ id,
505
+ beforeSec.fields ?? [],
506
+ afterSec.fields ?? []
449
507
  );
450
- diff.addedFields.push(...nestedDiff.addedFields);
451
- diff.removedFields.push(...nestedDiff.removedFields);
452
- diff.reorderedFields.push(...nestedDiff.reorderedFields);
453
- diff.modifiedFields.push(...nestedDiff.modifiedFields);
454
- diff.nestedFieldDiffs.push(...nestedDiff.nestedFieldDiffs);
455
508
  }
456
509
  return diff;
457
510
  }
@@ -465,7 +518,9 @@ function mapFieldTypeToJSONSchemaCore(field) {
465
518
  "x-frt-placeholder": field.placeholder,
466
519
  "x-frt-dataClassification": field.dataClassification,
467
520
  "x-frt-visibleIf": field.visibleIf,
468
- "x-frt-requiredIf": field.requiredIf
521
+ "x-frt-requiredIf": field.requiredIf,
522
+ // ⭐ surface computed expression as vendor extension
523
+ "x-frt-computed": field.computed
469
524
  };
470
525
  switch (field.type) {
471
526
  case "shortText":
@@ -609,58 +664,86 @@ function exportJSONSchema(template) {
609
664
  return schema;
610
665
  }
611
666
 
667
+ // src/validation/conditions.ts
668
+ function evaluateCondition(condition, response) {
669
+ if (!condition) return true;
670
+ if ("equals" in condition) {
671
+ const entries = Object.entries(condition.equals ?? {});
672
+ if (entries.length === 0) return true;
673
+ return entries.every(([key, expected]) => {
674
+ const value = response[key];
675
+ return value === expected;
676
+ });
677
+ }
678
+ if ("any" in condition) {
679
+ const children = condition.any ?? [];
680
+ if (children.length === 0) return false;
681
+ return children.some(
682
+ (child) => evaluateCondition(child, response)
683
+ );
684
+ }
685
+ if ("all" in condition) {
686
+ const children = condition.all ?? [];
687
+ if (children.length === 0) return true;
688
+ return children.every(
689
+ (child) => evaluateCondition(child, response)
690
+ );
691
+ }
692
+ if ("not" in condition) {
693
+ return !evaluateCondition(condition.not, response);
694
+ }
695
+ return true;
696
+ }
697
+
698
+ // src/validation/baseSchema.ts
699
+ import { z as z2 } from "zod";
700
+
612
701
  // src/fields/defaults.ts
613
- var DEFAULT_FIELD_LABEL = "Untitled question";
702
+ var DEFAULT_FIELD_LABEL = "Untitled field";
614
703
  var CORE_FIELD_DEFAULTS = {
615
704
  shortText: {
705
+ type: "shortText",
616
706
  label: DEFAULT_FIELD_LABEL,
617
707
  placeholder: "Short answer text"
618
708
  },
619
709
  longText: {
710
+ type: "longText",
620
711
  label: DEFAULT_FIELD_LABEL,
621
712
  placeholder: "Long answer text"
622
713
  },
623
714
  number: {
715
+ type: "number",
624
716
  label: DEFAULT_FIELD_LABEL,
625
717
  placeholder: "123"
626
718
  },
627
719
  date: {
720
+ type: "date",
628
721
  label: DEFAULT_FIELD_LABEL
629
722
  },
630
723
  checkbox: {
631
- label: DEFAULT_FIELD_LABEL,
632
- placeholder: "Check to confirm"
724
+ type: "checkbox",
725
+ label: DEFAULT_FIELD_LABEL
633
726
  },
634
727
  singleSelect: {
728
+ type: "singleSelect",
635
729
  label: DEFAULT_FIELD_LABEL,
636
730
  options: ["Option 1", "Option 2", "Option 3"]
637
731
  },
638
732
  multiSelect: {
733
+ type: "multiSelect",
639
734
  label: DEFAULT_FIELD_LABEL,
640
735
  options: ["Option 1", "Option 2", "Option 3"],
641
736
  allowOther: false
642
737
  },
643
738
  repeatGroup: {
644
- // Minimal safe defaults – your builder UI is expected
645
- // to configure nested fields + min/max as needed.
739
+ type: "repeatGroup",
646
740
  label: DEFAULT_FIELD_LABEL,
741
+ // The actual shape is filled in by the builder;
742
+ // for tests we just need this to be an array.
647
743
  fields: []
648
- // nested fields go here in the UI layer
649
744
  }
650
745
  };
651
746
 
652
- // src/fields/ids.ts
653
- function createUniqueId(prefix, existing) {
654
- const used = new Set(existing);
655
- let attempt = used.size + 1;
656
- let candidate = `${prefix}-${attempt}`;
657
- while (used.has(candidate)) {
658
- attempt++;
659
- candidate = `${prefix}-${attempt}`;
660
- }
661
- return candidate;
662
- }
663
-
664
747
  // src/fields/registry.ts
665
748
  var FieldRegistryClass = class {
666
749
  constructor() {
@@ -684,55 +767,191 @@ var FieldRegistryClass = class {
684
767
  }
685
768
  };
686
769
  var FieldRegistry = new FieldRegistryClass();
687
- var CORE_TYPES = [
688
- "shortText",
689
- "longText",
690
- "number",
691
- "date",
692
- "checkbox",
693
- "singleSelect",
694
- "multiSelect",
695
- "repeatGroup"
696
- ];
697
- for (const type of CORE_TYPES) {
770
+ for (const type of REPORT_TEMPLATE_FIELD_TYPES) {
698
771
  FieldRegistry.register(type, {
699
772
  defaults: CORE_FIELD_DEFAULTS[type]
700
773
  // Core types rely on existing validation logic.
701
- // buildResponseSchema is optional — fallback handles it.
774
+ // buildResponseSchema is optional — the validation engine provides
775
+ // built-in handlers for all core types.
702
776
  });
703
777
  }
704
778
 
705
- // src/validation/conditions.ts
706
- function evaluateCondition(condition, response) {
707
- if ("equals" in condition) {
708
- return Object.entries(condition.equals).every(([key, val]) => {
709
- return response[key] === val;
710
- });
779
+ // src/validation/baseSchema.ts
780
+ function buildBaseFieldSchema(field) {
781
+ const isRequired = Boolean(field.required);
782
+ const registry = FieldRegistry.get(field.type);
783
+ if (registry?.buildResponseSchema) {
784
+ return registry.buildResponseSchema(field);
711
785
  }
712
- if ("any" in condition) {
713
- return condition.any.some(
714
- (sub) => evaluateCondition(sub, response)
715
- );
786
+ switch (field.type) {
787
+ case "shortText":
788
+ case "longText": {
789
+ let schema = z2.string();
790
+ if (typeof field.minLength === "number") {
791
+ schema = schema.min(field.minLength);
792
+ }
793
+ if (typeof field.maxLength === "number") {
794
+ schema = schema.max(field.maxLength);
795
+ }
796
+ return isRequired ? schema : schema.optional();
797
+ }
798
+ case "number": {
799
+ let schema = z2.number();
800
+ if (typeof field.minValue === "number") {
801
+ schema = schema.min(field.minValue);
802
+ }
803
+ if (typeof field.maxValue === "number") {
804
+ schema = schema.max(field.maxValue);
805
+ }
806
+ return isRequired ? schema : schema.optional();
807
+ }
808
+ case "date": {
809
+ const schema = z2.string();
810
+ return isRequired ? schema : schema.optional();
811
+ }
812
+ case "checkbox": {
813
+ if (isRequired) return z2.literal(true);
814
+ return z2.boolean().optional();
815
+ }
816
+ case "singleSelect": {
817
+ let schema = z2.string();
818
+ const opts = field.options;
819
+ const hasOptions = Array.isArray(opts) && opts.length > 0;
820
+ if (hasOptions && !field.allowOther) {
821
+ const allowed = new Set(opts);
822
+ schema = schema.refine(
823
+ (value) => allowed.has(value),
824
+ "Value must be one of the defined options."
825
+ );
826
+ }
827
+ return isRequired ? schema : schema.optional();
828
+ }
829
+ case "multiSelect": {
830
+ let schema = z2.array(z2.string());
831
+ const opts = field.options;
832
+ const hasOptions = Array.isArray(opts) && opts.length > 0;
833
+ if (hasOptions && !field.allowOther) {
834
+ const allowed = new Set(opts);
835
+ schema = schema.refine(
836
+ (values) => values.every((v) => allowed.has(v)),
837
+ "All values must be among the defined options."
838
+ );
839
+ }
840
+ if (typeof field.minSelections === "number") {
841
+ schema = schema.min(
842
+ field.minSelections,
843
+ `Select at least ${field.minSelections} option(s).`
844
+ );
845
+ } else if (isRequired) {
846
+ schema = schema.min(
847
+ 1,
848
+ "Select at least one option."
849
+ );
850
+ }
851
+ if (typeof field.maxSelections === "number") {
852
+ schema = schema.max(
853
+ field.maxSelections,
854
+ `Select at most ${field.maxSelections} option(s).`
855
+ );
856
+ }
857
+ return isRequired ? schema : schema.optional();
858
+ }
859
+ case "repeatGroup": {
860
+ return z2.any();
861
+ }
862
+ default:
863
+ return z2.any();
716
864
  }
717
- if ("all" in condition) {
718
- return condition.all.every(
719
- (sub) => evaluateCondition(sub, response)
865
+ }
866
+
867
+ // src/validation/responses.ts
868
+ import { z as z4 } from "zod";
869
+
870
+ // src/validation/errors.ts
871
+ import { z as z3, ZodError } from "zod";
872
+ function mapZodIssueToFieldErrorCode(issue) {
873
+ switch (issue.code) {
874
+ case z3.ZodIssueCode.invalid_type:
875
+ return "field.invalid_type";
876
+ case z3.ZodIssueCode.too_small:
877
+ return "field.too_small";
878
+ case z3.ZodIssueCode.too_big:
879
+ return "field.too_big";
880
+ case z3.ZodIssueCode.invalid_enum_value:
881
+ case z3.ZodIssueCode.invalid_literal:
882
+ return "field.invalid_option";
883
+ case z3.ZodIssueCode.custom:
884
+ return "field.custom";
885
+ default:
886
+ return "field.custom";
887
+ }
888
+ }
889
+ function buildFieldMetadataLookup(template) {
890
+ const map = /* @__PURE__ */ new Map();
891
+ for (const section of template.sections) {
892
+ for (const field of section.fields) {
893
+ map.set(field.id, {
894
+ sectionId: section.id,
895
+ sectionTitle: section.title,
896
+ label: field.label
897
+ });
898
+ }
899
+ }
900
+ return map;
901
+ }
902
+ function mapZodIssuesToResponseErrors(template, issues) {
903
+ const fieldMeta = buildFieldMetadataLookup(template);
904
+ const errors = [];
905
+ for (const issue of issues) {
906
+ const path = issue.path ?? [];
907
+ const topLevelFieldId = path.find(
908
+ (p) => typeof p === "string"
720
909
  );
910
+ const rowIndex = path.find(
911
+ (p) => typeof p === "number"
912
+ );
913
+ const nestedFieldId = path.length >= 3 && typeof path[path.length - 1] === "string" ? path[path.length - 1] : void 0;
914
+ const fieldId = topLevelFieldId ?? "";
915
+ const meta = fieldId ? fieldMeta.get(fieldId) : void 0;
916
+ const code = mapZodIssueToFieldErrorCode(issue);
917
+ const sectionLabel = meta?.sectionTitle ?? meta?.sectionId;
918
+ const fieldLabel = meta?.label ?? fieldId;
919
+ let message;
920
+ if (rowIndex != null && nestedFieldId && meta && sectionLabel) {
921
+ message = `Section "${sectionLabel}" \u2192 "${meta.label}" (row ${rowIndex + 1}, field "${nestedFieldId}"): ${issue.message}`;
922
+ } else if (meta && sectionLabel && fieldLabel) {
923
+ message = `Section "${sectionLabel}" \u2192 Field "${fieldLabel}": ${issue.message}`;
924
+ } else if (fieldLabel) {
925
+ message = `Field "${fieldLabel}": ${issue.message}`;
926
+ } else {
927
+ message = issue.message;
928
+ }
929
+ errors.push({
930
+ fieldId,
931
+ sectionId: meta?.sectionId,
932
+ sectionTitle: meta?.sectionTitle,
933
+ label: meta?.label,
934
+ code,
935
+ message,
936
+ rawIssue: issue
937
+ });
721
938
  }
722
- if ("not" in condition) {
723
- const sub = condition.not;
724
- return !evaluateCondition(sub, response);
939
+ return errors;
940
+ }
941
+ function explainValidationError(template, error) {
942
+ if (error instanceof ZodError) {
943
+ return mapZodIssuesToResponseErrors(template, error.issues);
725
944
  }
726
- return false;
945
+ return null;
727
946
  }
728
947
 
729
948
  // src/validation/responses.ts
730
- import {
731
- z as z2,
732
- ZodError
733
- } from "zod";
734
- function buildBaseFieldSchema(field) {
949
+ function buildBaseFieldSchema2(field) {
735
950
  const isRequired = Boolean(field.required);
951
+ const isComputed = Boolean(field.computed);
952
+ if (isComputed) {
953
+ return z4.any().optional();
954
+ }
736
955
  const registry = FieldRegistry.get(field.type);
737
956
  if (registry?.buildResponseSchema) {
738
957
  return registry.buildResponseSchema(field);
@@ -740,7 +959,7 @@ function buildBaseFieldSchema(field) {
740
959
  switch (field.type) {
741
960
  case "shortText":
742
961
  case "longText": {
743
- let schema = z2.string();
962
+ let schema = z4.string();
744
963
  if (typeof field.minLength === "number") {
745
964
  schema = schema.min(field.minLength);
746
965
  }
@@ -750,7 +969,7 @@ function buildBaseFieldSchema(field) {
750
969
  return isRequired ? schema : schema.optional();
751
970
  }
752
971
  case "number": {
753
- let schema = z2.number();
972
+ let schema = z4.number();
754
973
  if (typeof field.minValue === "number") {
755
974
  schema = schema.min(field.minValue);
756
975
  }
@@ -760,15 +979,15 @@ function buildBaseFieldSchema(field) {
760
979
  return isRequired ? schema : schema.optional();
761
980
  }
762
981
  case "date": {
763
- let schema = z2.string();
982
+ let schema = z4.string();
764
983
  return isRequired ? schema : schema.optional();
765
984
  }
766
985
  case "checkbox": {
767
- if (isRequired) return z2.literal(true);
768
- return z2.boolean().optional();
986
+ if (isRequired) return z4.literal(true);
987
+ return z4.boolean().optional();
769
988
  }
770
989
  case "singleSelect": {
771
- let schema = z2.string();
990
+ let schema = z4.string();
772
991
  if (Array.isArray(field.options) && field.options.length > 0) {
773
992
  const allowed = new Set(field.options);
774
993
  schema = schema.refine(
@@ -779,11 +998,13 @@ function buildBaseFieldSchema(field) {
779
998
  return isRequired ? schema : schema.optional();
780
999
  }
781
1000
  case "multiSelect": {
782
- let schema = z2.array(z2.string());
1001
+ let schema = z4.array(z4.string());
783
1002
  if (Array.isArray(field.options) && field.options.length > 0 && !field.allowOther) {
784
1003
  const allowed = new Set(field.options);
785
1004
  schema = schema.refine(
786
- (values) => values.every((value) => allowed.has(value)),
1005
+ (values) => values.every(
1006
+ (value) => allowed.has(value)
1007
+ ),
787
1008
  "All values must be among the defined options."
788
1009
  );
789
1010
  }
@@ -810,11 +1031,11 @@ function buildBaseFieldSchema(field) {
810
1031
  const nestedFields = Array.isArray(field.fields) ? field.fields : [];
811
1032
  const rowShape = {};
812
1033
  for (const nested of nestedFields) {
813
- rowShape[nested.id] = buildBaseFieldSchema(
1034
+ rowShape[nested.id] = buildBaseFieldSchema2(
814
1035
  nested
815
1036
  );
816
1037
  }
817
- let schema = z2.array(z2.object(rowShape));
1038
+ let schema = z4.array(z4.object(rowShape));
818
1039
  if (typeof field.min === "number") {
819
1040
  schema = schema.min(
820
1041
  field.min,
@@ -830,12 +1051,11 @@ function buildBaseFieldSchema(field) {
830
1051
  return isRequired ? schema : schema.optional();
831
1052
  }
832
1053
  default: {
833
- return z2.any();
1054
+ return z4.any();
834
1055
  }
835
1056
  }
836
1057
  }
837
1058
  function buildRepeatGroupSchemaWithConditions(field, response) {
838
- const isRequired = Boolean(field.required);
839
1059
  const nestedFields = Array.isArray(field.fields) ? field.fields : [];
840
1060
  const rowShape = {};
841
1061
  for (const nested of nestedFields) {
@@ -844,48 +1064,58 @@ function buildRepeatGroupSchemaWithConditions(field, response) {
844
1064
  response
845
1065
  );
846
1066
  }
847
- let schema = z2.array(z2.object(rowShape));
1067
+ let schema = z4.array(z4.object(rowShape));
848
1068
  const hasStaticMin = typeof field.min === "number";
849
- const hasMinIf = Boolean(field.minIf);
850
- const minIfCondition = field.minIf;
851
- const shouldEnforceMin = hasStaticMin && (!hasMinIf || evaluateCondition(minIfCondition, response));
852
- if (shouldEnforceMin) {
853
- const minValue = field.min;
1069
+ const hasStaticMax = typeof field.max === "number";
1070
+ let effectiveMin = hasStaticMin ? field.min : void 0;
1071
+ let effectiveMax = hasStaticMax ? field.max : void 0;
1072
+ if (field.minIf) {
1073
+ const applyDynamicMin = evaluateCondition(field.minIf, response);
1074
+ if (applyDynamicMin && typeof field.min === "number") {
1075
+ effectiveMin = field.min;
1076
+ } else if (!applyDynamicMin) {
1077
+ effectiveMin = void 0;
1078
+ }
1079
+ }
1080
+ if (field.maxIf) {
1081
+ const applyDynamicMax = evaluateCondition(field.maxIf, response);
1082
+ if (applyDynamicMax && typeof field.max === "number") {
1083
+ effectiveMax = field.max;
1084
+ } else if (!applyDynamicMax) {
1085
+ effectiveMax = void 0;
1086
+ }
1087
+ }
1088
+ if (typeof effectiveMin === "number") {
854
1089
  schema = schema.min(
855
- minValue,
856
- `Add at least ${minValue} item(s).`
1090
+ effectiveMin,
1091
+ `Add at least ${effectiveMin} item(s).`
857
1092
  );
858
1093
  }
859
- const hasStaticMax = typeof field.max === "number";
860
- const hasMaxIf = Boolean(field.maxIf);
861
- const maxIfCondition = field.maxIf;
862
- const shouldEnforceMax = hasStaticMax && (!hasMaxIf || evaluateCondition(maxIfCondition, response));
863
- if (shouldEnforceMax) {
864
- const maxValue = field.max;
1094
+ if (typeof effectiveMax === "number") {
865
1095
  schema = schema.max(
866
- maxValue,
867
- `Add at most ${maxValue} item(s).`
1096
+ effectiveMax,
1097
+ `Add at most ${effectiveMax} item(s).`
868
1098
  );
869
1099
  }
870
- return isRequired ? schema : schema.optional();
1100
+ return schema;
871
1101
  }
872
1102
  function buildConditionalFieldSchema(field, response) {
873
1103
  if (field.visibleIf) {
874
1104
  const visible = evaluateCondition(field.visibleIf, response);
875
1105
  if (!visible) {
876
- return z2.any().optional();
1106
+ return z4.any().optional();
877
1107
  }
878
1108
  }
879
1109
  let schema;
880
1110
  if (field.type === "repeatGroup") {
881
1111
  schema = buildRepeatGroupSchemaWithConditions(field, response);
882
1112
  } else {
883
- schema = buildBaseFieldSchema(field);
1113
+ schema = buildBaseFieldSchema2(field);
884
1114
  }
885
1115
  if (field.requiredIf) {
886
1116
  const shouldBeRequired = evaluateCondition(field.requiredIf, response);
887
1117
  if (shouldBeRequired) {
888
- if (schema instanceof z2.ZodOptional) {
1118
+ if (schema instanceof z4.ZodOptional) {
889
1119
  schema = schema.unwrap();
890
1120
  }
891
1121
  }
@@ -899,18 +1129,12 @@ function buildResponseSchemaWithConditions(template, response) {
899
1129
  shape[field.id] = buildConditionalFieldSchema(field, response);
900
1130
  }
901
1131
  }
902
- return z2.object(shape);
1132
+ return z4.object(shape);
903
1133
  }
904
1134
  function buildResponseSchema(template) {
905
1135
  return buildResponseSchemaWithConditions(template, {});
906
1136
  }
907
- function validateReportResponse(template, data) {
908
- if (typeof data !== "object" || data === null) {
909
- throw new Error("Response must be an object");
910
- }
911
- const response = data;
912
- const schema = buildResponseSchemaWithConditions(template, response);
913
- const parsed = schema.parse(response);
1137
+ function stripInvisibleFields(template, response, parsed) {
914
1138
  for (const section of template.sections) {
915
1139
  for (const field of section.fields) {
916
1140
  if (field.visibleIf) {
@@ -921,76 +1145,38 @@ function validateReportResponse(template, data) {
921
1145
  }
922
1146
  }
923
1147
  }
924
- return parsed;
925
- }
926
- function mapZodIssueToFieldErrorCode(issue) {
927
- switch (issue.code) {
928
- case z2.ZodIssueCode.invalid_type:
929
- return "field.invalid_type";
930
- case z2.ZodIssueCode.too_small:
931
- return "field.too_small";
932
- case z2.ZodIssueCode.too_big:
933
- return "field.too_big";
934
- case z2.ZodIssueCode.invalid_enum_value:
935
- case z2.ZodIssueCode.invalid_literal:
936
- return "field.invalid_option";
937
- case z2.ZodIssueCode.custom:
938
- return "field.custom";
939
- default:
940
- return "field.custom";
941
- }
942
1148
  }
943
- function buildFieldMetadataLookup(template) {
944
- const map = /* @__PURE__ */ new Map();
1149
+ function stripComputedFields(template, parsed) {
945
1150
  for (const section of template.sections) {
946
1151
  for (const field of section.fields) {
947
- map.set(field.id, {
948
- sectionId: section.id,
949
- sectionTitle: section.title,
950
- label: field.label
951
- });
1152
+ if (field.computed) {
1153
+ delete parsed[field.id];
1154
+ }
1155
+ if (field.type === "repeatGroup" && Array.isArray(parsed[field.id])) {
1156
+ const rows = parsed[field.id];
1157
+ const nestedFields = Array.isArray(field.fields) ? field.fields : [];
1158
+ for (const row of rows) {
1159
+ if (!row || typeof row !== "object") continue;
1160
+ for (const nested of nestedFields) {
1161
+ if (nested.computed) {
1162
+ delete row[nested.id];
1163
+ }
1164
+ }
1165
+ }
1166
+ }
952
1167
  }
953
1168
  }
954
- return map;
955
1169
  }
956
- function mapZodIssuesToResponseErrors(template, issues) {
957
- const fieldMeta = buildFieldMetadataLookup(template);
958
- const errors = [];
959
- for (const issue of issues) {
960
- const path = issue.path ?? [];
961
- const topLevelFieldId = path.find(
962
- (p) => typeof p === "string"
963
- );
964
- const rowIndex = path.find(
965
- (p) => typeof p === "number"
966
- );
967
- const nestedFieldId = path.length >= 3 && typeof path[path.length - 1] === "string" ? path[path.length - 1] : void 0;
968
- const fieldId = topLevelFieldId ?? "";
969
- const meta = fieldId ? fieldMeta.get(fieldId) : void 0;
970
- const code = mapZodIssueToFieldErrorCode(issue);
971
- const sectionLabel = meta?.sectionTitle ?? meta?.sectionId;
972
- const fieldLabel = meta?.label ?? fieldId;
973
- let message;
974
- if (rowIndex != null && nestedFieldId && meta && sectionLabel) {
975
- message = `Section "${sectionLabel}" \u2192 "${meta.label}" (row ${rowIndex + 1}, field "${nestedFieldId}"): ${issue.message}`;
976
- } else if (meta && sectionLabel && fieldLabel) {
977
- message = `Section "${sectionLabel}" \u2192 Field "${fieldLabel}": ${issue.message}`;
978
- } else if (fieldLabel) {
979
- message = `Field "${fieldLabel}": ${issue.message}`;
980
- } else {
981
- message = issue.message;
982
- }
983
- errors.push({
984
- fieldId,
985
- sectionId: meta?.sectionId,
986
- sectionTitle: meta?.sectionTitle,
987
- label: meta?.label,
988
- code,
989
- message,
990
- rawIssue: issue
991
- });
1170
+ function validateReportResponse(template, data) {
1171
+ if (typeof data !== "object" || data === null) {
1172
+ throw new Error("Response must be an object");
992
1173
  }
993
- return errors;
1174
+ const response = data;
1175
+ const schema = buildResponseSchemaWithConditions(template, response);
1176
+ const parsed = schema.parse(response);
1177
+ stripInvisibleFields(template, response, parsed);
1178
+ stripComputedFields(template, parsed);
1179
+ return parsed;
994
1180
  }
995
1181
  function validateReportResponseDetailed(template, data) {
996
1182
  if (typeof data !== "object" || data === null) {
@@ -1010,27 +1196,13 @@ function validateReportResponseDetailed(template, data) {
1010
1196
  const result = schema.safeParse(response);
1011
1197
  if (result.success) {
1012
1198
  const parsed = { ...result.data };
1013
- for (const section of template.sections) {
1014
- for (const field of section.fields) {
1015
- if (field.visibleIf) {
1016
- const visible = evaluateCondition(field.visibleIf, response);
1017
- if (!visible) {
1018
- delete parsed[field.id];
1019
- }
1020
- }
1021
- }
1022
- }
1199
+ stripInvisibleFields(template, response, parsed);
1200
+ stripComputedFields(template, parsed);
1023
1201
  return { success: true, value: parsed };
1024
1202
  }
1025
1203
  const errors = mapZodIssuesToResponseErrors(template, result.error.issues);
1026
1204
  return { success: false, errors };
1027
1205
  }
1028
- function explainValidationError(template, error) {
1029
- if (error instanceof ZodError) {
1030
- return mapZodIssuesToResponseErrors(template, error.issues);
1031
- }
1032
- return null;
1033
- }
1034
1206
  export {
1035
1207
  CORE_FIELD_DEFAULTS,
1036
1208
  Condition,