@frt-platform/report-core 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // src/schema.ts
1
+ // src/template/schema.ts
2
2
  import { z } from "zod";
3
3
  var REPORT_TEMPLATE_VERSION = 1;
4
4
  var REPORT_TEMPLATE_FIELD_TYPES = [
@@ -123,59 +123,7 @@ var ReportTemplateSchemaValidator = z.object({
123
123
  sections: z.array(ReportTemplateSectionSchema).max(25, "Templates can include at most 25 sections.").default([])
124
124
  });
125
125
 
126
- // src/fields.ts
127
- var DEFAULT_FIELD_LABEL = "Untitled question";
128
- var CORE_FIELD_DEFAULTS = {
129
- shortText: {
130
- label: DEFAULT_FIELD_LABEL,
131
- placeholder: "Short answer text"
132
- },
133
- longText: {
134
- label: DEFAULT_FIELD_LABEL,
135
- placeholder: "Long answer text"
136
- },
137
- number: {
138
- label: DEFAULT_FIELD_LABEL,
139
- placeholder: "123"
140
- },
141
- date: {
142
- label: DEFAULT_FIELD_LABEL
143
- },
144
- checkbox: {
145
- label: DEFAULT_FIELD_LABEL,
146
- placeholder: "Check to confirm"
147
- },
148
- singleSelect: {
149
- label: DEFAULT_FIELD_LABEL,
150
- options: ["Option 1", "Option 2", "Option 3"]
151
- },
152
- multiSelect: {
153
- label: DEFAULT_FIELD_LABEL,
154
- options: ["Option 1", "Option 2", "Option 3"],
155
- allowOther: false
156
- },
157
- repeatGroup: {
158
- // Minimal safe defaults – your builder UI is expected
159
- // to configure nested fields + min/max as needed.
160
- label: DEFAULT_FIELD_LABEL,
161
- fields: []
162
- // nested fields go here in the UI layer
163
- }
164
- };
165
-
166
- // src/ids.ts
167
- function createUniqueId(prefix, existing) {
168
- const used = new Set(existing);
169
- let attempt = used.size + 1;
170
- let candidate = `${prefix}-${attempt}`;
171
- while (used.has(candidate)) {
172
- attempt++;
173
- candidate = `${prefix}-${attempt}`;
174
- }
175
- return candidate;
176
- }
177
-
178
- // src/migrate.ts
126
+ // src/template/migrate.ts
179
127
  var LEGACY_FIELD_TYPE_MAP = {
180
128
  shorttext: "shortText",
181
129
  text: "shortText",
@@ -202,12 +150,14 @@ function migrateLegacySchema(raw) {
202
150
  if (obj.fields) {
203
151
  obj.sections = [
204
152
  {
205
- id: "section-1",
153
+ id: "section_1",
154
+ // ⬅ underscore to match normalizer/tests
206
155
  title: obj.title,
207
156
  description: obj.description,
208
157
  fields: obj.fields.map((f, i) => ({
209
158
  ...f,
210
- id: f.id || `field-${i + 1}`,
159
+ id: f.id || `field_${i + 1}`,
160
+ // ⬅ underscore here too
211
161
  type: LEGACY_FIELD_TYPE_MAP[f.type?.toLowerCase()] ?? f.type
212
162
  }))
213
163
  }
@@ -218,10 +168,12 @@ function migrateLegacySchema(raw) {
218
168
  if (Array.isArray(obj.sections)) {
219
169
  obj.sections = obj.sections.map((sec, i) => ({
220
170
  ...sec,
221
- id: sec.id || `section-${i + 1}`,
171
+ id: sec.id || `section_${i + 1}`,
172
+ // ⬅ underscore
222
173
  fields: (sec.fields || []).map((f, idx) => ({
223
174
  ...f,
224
- id: f.id || `field-${idx + 1}`,
175
+ id: f.id || `field_${idx + 1}`,
176
+ // ⬅ underscore
225
177
  type: LEGACY_FIELD_TYPE_MAP[f.type?.toLowerCase()] ?? f.type
226
178
  }))
227
179
  }));
@@ -229,7 +181,7 @@ function migrateLegacySchema(raw) {
229
181
  return obj;
230
182
  }
231
183
 
232
- // src/normalize.ts
184
+ // src/template/normalize.ts
233
185
  function normalizeId(raw, fallback) {
234
186
  if (!raw || typeof raw !== "string") return fallback;
235
187
  const cleaned = raw.trim().toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_-]/g, "");
@@ -302,349 +254,96 @@ function parseReportTemplateSchema(raw) {
302
254
  function parseReportTemplateSchemaFromString(raw) {
303
255
  return parseReportTemplateSchema(JSON.parse(raw));
304
256
  }
305
- function serializeReportTemplateSchema(schema) {
306
- return JSON.stringify(schema, null, 2);
307
- }
308
-
309
- // src/responses.ts
310
- import { z as z2 } from "zod";
311
-
312
- // src/fieldRegistry.ts
313
- var FieldRegistryClass = class {
314
- constructor() {
315
- this.registry = /* @__PURE__ */ new Map();
316
- }
317
- /** Register or override a field type. */
318
- register(type, entry) {
319
- this.registry.set(type, entry);
320
- }
321
- /** Get registry entry for a field type. */
322
- get(type) {
323
- return this.registry.get(type);
324
- }
325
- /** Check if field type is registered. */
326
- has(type) {
327
- return this.registry.has(type);
328
- }
329
- /** Return all field types currently registered. */
330
- list() {
331
- return [...this.registry.keys()];
332
- }
333
- };
334
- var FieldRegistry = new FieldRegistryClass();
335
- var CORE_TYPES = [
336
- "shortText",
337
- "longText",
338
- "number",
339
- "date",
340
- "checkbox",
341
- "singleSelect",
342
- "multiSelect"
343
- ];
344
- for (const type of CORE_TYPES) {
345
- FieldRegistry.register(type, {
346
- defaults: CORE_FIELD_DEFAULTS[type]
347
- // Core types rely on existing validation logic.
348
- // buildResponseSchema is optional — fallback handles it.
349
- });
350
- }
351
-
352
- // src/conditions.ts
353
- function evaluateCondition(condition, response) {
354
- if ("equals" in condition) {
355
- return Object.entries(condition.equals).every(([key, val]) => {
356
- return response[key] === val;
257
+ function serializeReportTemplateSchema(schema, options = {}) {
258
+ const {
259
+ pretty = true,
260
+ sortSectionsById = false,
261
+ sortFieldsById = false
262
+ } = options;
263
+ let toSerialize = schema;
264
+ if (sortSectionsById || sortFieldsById) {
265
+ const sortedSections = [...schema.sections].map((section) => {
266
+ const clonedSection = { ...section };
267
+ if (sortFieldsById) {
268
+ const fields = section.fields ?? [];
269
+ clonedSection.fields = [...fields].sort(
270
+ (a, b) => a.id.localeCompare(b.id)
271
+ );
272
+ }
273
+ return clonedSection;
357
274
  });
275
+ if (sortSectionsById) {
276
+ toSerialize = {
277
+ ...schema,
278
+ sections: sortedSections.sort(
279
+ (a, b) => a.id.localeCompare(b.id)
280
+ )
281
+ };
282
+ } else {
283
+ toSerialize = {
284
+ ...schema,
285
+ sections: sortedSections
286
+ };
287
+ }
358
288
  }
359
- if ("any" in condition) {
360
- return condition.any.some(
361
- (sub) => evaluateCondition(sub, response)
362
- );
363
- }
364
- if ("all" in condition) {
365
- return condition.all.every(
366
- (sub) => evaluateCondition(sub, response)
367
- );
368
- }
369
- if ("not" in condition) {
370
- const sub = condition.not;
371
- return !evaluateCondition(sub, response);
372
- }
373
- return false;
289
+ return JSON.stringify(toSerialize, null, pretty ? 2 : 0);
374
290
  }
375
291
 
376
- // src/responses.ts
377
- function buildBaseFieldSchema(field) {
378
- const isRequired = Boolean(field.required);
379
- const registry = FieldRegistry.get(field.type);
380
- if (registry?.buildResponseSchema) {
381
- return registry.buildResponseSchema(field);
382
- }
383
- switch (field.type) {
384
- case "shortText":
385
- case "longText": {
386
- let schema = z2.string();
387
- if (typeof field.minLength === "number") {
388
- schema = schema.min(field.minLength);
389
- }
390
- if (typeof field.maxLength === "number") {
391
- schema = schema.max(field.maxLength);
392
- }
393
- return isRequired ? schema : schema.optional();
394
- }
395
- case "number": {
396
- let schema = z2.number();
397
- if (typeof field.minValue === "number") {
398
- schema = schema.min(field.minValue);
399
- }
400
- if (typeof field.maxValue === "number") {
401
- schema = schema.max(field.maxValue);
402
- }
403
- return isRequired ? schema : schema.optional();
404
- }
405
- case "date": {
406
- let schema = z2.string();
407
- return isRequired ? schema : schema.optional();
408
- }
409
- case "checkbox": {
410
- if (isRequired) return z2.literal(true);
411
- return z2.boolean().optional();
412
- }
413
- case "singleSelect": {
414
- let schema = z2.string();
415
- if (Array.isArray(field.options) && field.options.length > 0) {
416
- const allowed = new Set(field.options);
417
- schema = schema.refine(
418
- (value) => allowed.has(value),
419
- "Value must be one of the defined options."
420
- );
421
- }
422
- return isRequired ? schema : schema.optional();
423
- }
424
- case "multiSelect": {
425
- let schema = z2.array(z2.string());
426
- if (Array.isArray(field.options) && field.options.length > 0 && !field.allowOther) {
427
- const allowed = new Set(field.options);
428
- schema = schema.refine(
429
- (values) => values.every((value) => allowed.has(value)),
430
- "All values must be among the defined options."
431
- );
432
- }
433
- if (typeof field.minSelections === "number") {
434
- schema = schema.min(
435
- field.minSelections,
436
- `Select at least ${field.minSelections} option(s).`
437
- );
438
- } else if (isRequired) {
439
- schema = schema.min(
440
- 1,
441
- "Select at least one option."
442
- );
443
- }
444
- if (typeof field.maxSelections === "number") {
445
- schema = schema.max(
446
- field.maxSelections,
447
- `Select at most ${field.maxSelections} option(s).`
448
- );
449
- }
450
- return isRequired ? schema : schema.optional();
451
- }
452
- default: {
453
- return z2.any();
292
+ // 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 });
454
308
  }
455
309
  }
310
+ return changes;
456
311
  }
457
- function buildConditionalFieldSchema(field, response) {
458
- if (field.visibleIf) {
459
- const visible = evaluateCondition(field.visibleIf, response);
460
- if (!visible) {
461
- return z2.any().optional();
312
+ function diffRepeatGroupFields(beforeFields, afterFields, sectionId, groupId) {
313
+ const empty = {
314
+ addedSections: [],
315
+ removedSections: [],
316
+ reorderedSections: [],
317
+ modifiedSections: [],
318
+ addedFields: [],
319
+ removedFields: [],
320
+ reorderedFields: [],
321
+ modifiedFields: [],
322
+ nestedFieldDiffs: []
323
+ };
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] });
462
329
  }
463
330
  }
464
- let schema = buildBaseFieldSchema(field);
465
- if (field.requiredIf) {
466
- const shouldBeRequired = evaluateCondition(field.requiredIf, response);
467
- if (shouldBeRequired) {
468
- if (schema instanceof z2.ZodOptional) {
469
- schema = schema.unwrap();
470
- }
471
- }
472
- }
473
- return schema;
474
- }
475
- function buildResponseSchemaWithConditions(template, response) {
476
- const shape = {};
477
- for (const section of template.sections) {
478
- for (const field of section.fields) {
479
- shape[field.id] = buildConditionalFieldSchema(field, response);
480
- }
481
- }
482
- return z2.object(shape);
483
- }
484
- function validateReportResponse(template, data) {
485
- if (typeof data !== "object" || data === null) {
486
- throw new Error("Response must be an object");
487
- }
488
- const response = data;
489
- const schema = buildResponseSchemaWithConditions(template, response);
490
- const parsed = schema.parse(response);
491
- for (const section of template.sections) {
492
- for (const field of section.fields) {
493
- if (field.visibleIf) {
494
- const visible = evaluateCondition(field.visibleIf, response);
495
- if (!visible) {
496
- delete parsed[field.id];
497
- }
498
- }
499
- }
500
- }
501
- return parsed;
502
- }
503
- function mapZodIssueToFieldErrorCode(issue) {
504
- switch (issue.code) {
505
- case z2.ZodIssueCode.invalid_type:
506
- return "field.invalid_type";
507
- case z2.ZodIssueCode.too_small:
508
- return "field.too_small";
509
- case z2.ZodIssueCode.too_big:
510
- return "field.too_big";
511
- case z2.ZodIssueCode.invalid_enum_value:
512
- case z2.ZodIssueCode.invalid_literal:
513
- return "field.invalid_option";
514
- case z2.ZodIssueCode.custom:
515
- return "field.custom";
516
- default:
517
- return "field.custom";
518
- }
519
- }
520
- function buildFieldMetadataLookup(template) {
521
- const map = /* @__PURE__ */ new Map();
522
- for (const section of template.sections) {
523
- for (const field of section.fields) {
524
- map.set(field.id, {
525
- sectionId: section.id,
526
- sectionTitle: section.title,
527
- label: field.label
528
- });
529
- }
530
- }
531
- return map;
532
- }
533
- function validateReportResponseDetailed(template, data) {
534
- if (typeof data !== "object" || data === null) {
535
- return {
536
- success: false,
537
- errors: [
538
- {
539
- fieldId: "",
540
- code: "response.invalid_root",
541
- message: "Response must be an object."
542
- }
543
- ]
544
- };
545
- }
546
- const response = data;
547
- const schema = buildResponseSchemaWithConditions(template, response);
548
- const fieldMeta = buildFieldMetadataLookup(template);
549
- const result = schema.safeParse(response);
550
- if (result.success) {
551
- const parsed = { ...result.data };
552
- for (const section of template.sections) {
553
- for (const field of section.fields) {
554
- if (field.visibleIf) {
555
- const visible = evaluateCondition(field.visibleIf, response);
556
- if (!visible) {
557
- delete parsed[field.id];
558
- }
559
- }
560
- }
561
- }
562
- return { success: true, value: parsed };
563
- }
564
- const errors = [];
565
- for (const issue of result.error.issues) {
566
- const path = issue.path ?? [];
567
- const fieldId = typeof path[0] === "string" ? path[0] : "";
568
- const meta = fieldId ? fieldMeta.get(fieldId) : void 0;
569
- const code = mapZodIssueToFieldErrorCode(issue);
570
- const sectionLabel = meta?.sectionTitle ?? meta?.sectionId;
571
- const fieldLabel = meta?.label ?? fieldId;
572
- let message;
573
- if (meta && sectionLabel && fieldLabel) {
574
- message = `Section "${sectionLabel}" \u2192 Field "${fieldLabel}": ${issue.message}`;
575
- } else if (fieldLabel) {
576
- message = `Field "${fieldLabel}": ${issue.message}`;
577
- } else {
578
- message = issue.message;
579
- }
580
- errors.push({
581
- fieldId,
582
- sectionId: meta?.sectionId,
583
- sectionTitle: meta?.sectionTitle,
584
- label: meta?.label,
585
- code,
586
- message,
587
- rawIssue: issue
588
- });
589
- }
590
- return { success: false, errors };
591
- }
592
-
593
- // src/diff.ts
594
- function indexById(items) {
595
- const map = {};
596
- items.forEach((item, i) => map[item.id] = i);
597
- return map;
598
- }
599
- function diffObjectProperties(before, after, ignoreKeys = []) {
600
- const changes = [];
601
- const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
602
- for (const key of keys) {
603
- if (ignoreKeys.includes(key)) continue;
604
- const b = before[key];
605
- const a = after[key];
606
- const changed = Array.isArray(b) && Array.isArray(a) ? JSON.stringify(b) !== JSON.stringify(a) : b !== a;
607
- if (changed) {
608
- changes.push({ key, before: b, after: a });
609
- }
610
- }
611
- return changes;
612
- }
613
- function diffRepeatGroupFields(beforeFields, afterFields, sectionId, groupId) {
614
- const empty = {
615
- addedSections: [],
616
- removedSections: [],
617
- reorderedSections: [],
618
- modifiedSections: [],
619
- addedFields: [],
620
- removedFields: [],
621
- reorderedFields: [],
622
- modifiedFields: [],
623
- nestedFieldDiffs: []
624
- };
625
- const beforeIndex = indexById(beforeFields);
626
- const afterIndex = indexById(afterFields);
627
- for (const f of afterFields) {
628
- if (!(f.id in beforeIndex)) {
629
- empty.addedFields.push({ sectionId, fieldId: f.id, index: afterIndex[f.id] });
630
- }
631
- }
632
- for (const f of beforeFields) {
633
- if (!(f.id in afterIndex)) {
634
- empty.removedFields.push({ sectionId, fieldId: f.id, index: beforeIndex[f.id] });
635
- }
636
- }
637
- for (const f of afterFields) {
638
- if (f.id in beforeIndex) {
639
- const from = beforeIndex[f.id];
640
- const to = afterIndex[f.id];
641
- if (from !== to) {
642
- empty.reorderedFields.push({
643
- sectionId,
644
- fieldId: f.id,
645
- from,
646
- to
647
- });
331
+ for (const f of beforeFields) {
332
+ if (!(f.id in afterIndex)) {
333
+ empty.removedFields.push({ sectionId, fieldId: f.id, index: beforeIndex[f.id] });
334
+ }
335
+ }
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
+ });
648
347
  }
649
348
  }
650
349
  }
@@ -757,7 +456,7 @@ function diffTemplates(before, after) {
757
456
  return diff;
758
457
  }
759
458
 
760
- // src/jsonSchema.ts
459
+ // src/template/jsonSchema.ts
761
460
  function mapFieldTypeToJSONSchemaCore(field) {
762
461
  const common = {
763
462
  title: field.label,
@@ -809,9 +508,6 @@ function mapFieldTypeToJSONSchemaCore(field) {
809
508
  ...common,
810
509
  type: "boolean"
811
510
  };
812
- if (field.required && !field.requiredIf) {
813
- schema.const = true;
814
- }
815
511
  return schema;
816
512
  }
817
513
  case "singleSelect": {
@@ -819,7 +515,8 @@ function mapFieldTypeToJSONSchemaCore(field) {
819
515
  ...common,
820
516
  type: "string"
821
517
  };
822
- if (Array.isArray(field.options) && field.options.length > 0) {
518
+ const hasOptions = Array.isArray(field.options) && field.options.length > 0;
519
+ if (hasOptions && !field.allowOther) {
823
520
  schema.enum = field.options;
824
521
  }
825
522
  return schema;
@@ -847,6 +544,38 @@ function mapFieldTypeToJSONSchemaCore(field) {
847
544
  }
848
545
  return schema;
849
546
  }
547
+ case "repeatGroup": {
548
+ const nestedFields = Array.isArray(field.fields) ? field.fields : [];
549
+ const rowProperties = {};
550
+ const rowRequired = [];
551
+ for (const nested of nestedFields) {
552
+ rowProperties[nested.id] = mapFieldTypeToJSONSchemaCore(
553
+ nested
554
+ );
555
+ if (nested.required === true && !nested.requiredIf && !nested.visibleIf) {
556
+ rowRequired.push(nested.id);
557
+ }
558
+ }
559
+ const rowSchema = {
560
+ type: "object",
561
+ properties: rowProperties,
562
+ additionalProperties: false
563
+ };
564
+ if (rowRequired.length > 0) {
565
+ rowSchema.required = rowRequired;
566
+ }
567
+ const schema = {
568
+ ...common,
569
+ type: "array",
570
+ items: rowSchema,
571
+ // expose min/max + conditional minIf/maxIf as extensions
572
+ "x-frt-min": field.min,
573
+ "x-frt-max": field.max,
574
+ "x-frt-minIf": field.minIf,
575
+ "x-frt-maxIf": field.maxIf
576
+ };
577
+ return schema;
578
+ }
850
579
  default: {
851
580
  return {
852
581
  ...common
@@ -879,6 +608,386 @@ function exportJSONSchema(template) {
879
608
  }
880
609
  return schema;
881
610
  }
611
+
612
+ // src/fields/defaults.ts
613
+ var DEFAULT_FIELD_LABEL = "Untitled question";
614
+ var CORE_FIELD_DEFAULTS = {
615
+ shortText: {
616
+ label: DEFAULT_FIELD_LABEL,
617
+ placeholder: "Short answer text"
618
+ },
619
+ longText: {
620
+ label: DEFAULT_FIELD_LABEL,
621
+ placeholder: "Long answer text"
622
+ },
623
+ number: {
624
+ label: DEFAULT_FIELD_LABEL,
625
+ placeholder: "123"
626
+ },
627
+ date: {
628
+ label: DEFAULT_FIELD_LABEL
629
+ },
630
+ checkbox: {
631
+ label: DEFAULT_FIELD_LABEL,
632
+ placeholder: "Check to confirm"
633
+ },
634
+ singleSelect: {
635
+ label: DEFAULT_FIELD_LABEL,
636
+ options: ["Option 1", "Option 2", "Option 3"]
637
+ },
638
+ multiSelect: {
639
+ label: DEFAULT_FIELD_LABEL,
640
+ options: ["Option 1", "Option 2", "Option 3"],
641
+ allowOther: false
642
+ },
643
+ repeatGroup: {
644
+ // Minimal safe defaults – your builder UI is expected
645
+ // to configure nested fields + min/max as needed.
646
+ label: DEFAULT_FIELD_LABEL,
647
+ fields: []
648
+ // nested fields go here in the UI layer
649
+ }
650
+ };
651
+
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
+ // src/fields/registry.ts
665
+ var FieldRegistryClass = class {
666
+ constructor() {
667
+ this.registry = /* @__PURE__ */ new Map();
668
+ }
669
+ /** Register or override a field type. */
670
+ register(type, entry) {
671
+ this.registry.set(type, entry);
672
+ }
673
+ /** Get registry entry for a field type. */
674
+ get(type) {
675
+ return this.registry.get(type);
676
+ }
677
+ /** Check if field type is registered. */
678
+ has(type) {
679
+ return this.registry.has(type);
680
+ }
681
+ /** Return all field types currently registered. */
682
+ list() {
683
+ return [...this.registry.keys()];
684
+ }
685
+ };
686
+ 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) {
698
+ FieldRegistry.register(type, {
699
+ defaults: CORE_FIELD_DEFAULTS[type]
700
+ // Core types rely on existing validation logic.
701
+ // buildResponseSchema is optional — fallback handles it.
702
+ });
703
+ }
704
+
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
+ });
711
+ }
712
+ if ("any" in condition) {
713
+ return condition.any.some(
714
+ (sub) => evaluateCondition(sub, response)
715
+ );
716
+ }
717
+ if ("all" in condition) {
718
+ return condition.all.every(
719
+ (sub) => evaluateCondition(sub, response)
720
+ );
721
+ }
722
+ if ("not" in condition) {
723
+ const sub = condition.not;
724
+ return !evaluateCondition(sub, response);
725
+ }
726
+ return false;
727
+ }
728
+
729
+ // src/validation/responses.ts
730
+ import {
731
+ z as z2,
732
+ ZodError
733
+ } from "zod";
734
+ function buildBaseFieldSchema(field) {
735
+ const isRequired = Boolean(field.required);
736
+ const registry = FieldRegistry.get(field.type);
737
+ if (registry?.buildResponseSchema) {
738
+ return registry.buildResponseSchema(field);
739
+ }
740
+ switch (field.type) {
741
+ case "shortText":
742
+ case "longText": {
743
+ let schema = z2.string();
744
+ if (typeof field.minLength === "number") {
745
+ schema = schema.min(field.minLength);
746
+ }
747
+ if (typeof field.maxLength === "number") {
748
+ schema = schema.max(field.maxLength);
749
+ }
750
+ return isRequired ? schema : schema.optional();
751
+ }
752
+ case "number": {
753
+ let schema = z2.number();
754
+ if (typeof field.minValue === "number") {
755
+ schema = schema.min(field.minValue);
756
+ }
757
+ if (typeof field.maxValue === "number") {
758
+ schema = schema.max(field.maxValue);
759
+ }
760
+ return isRequired ? schema : schema.optional();
761
+ }
762
+ case "date": {
763
+ let schema = z2.string();
764
+ return isRequired ? schema : schema.optional();
765
+ }
766
+ case "checkbox": {
767
+ if (isRequired) return z2.literal(true);
768
+ return z2.boolean().optional();
769
+ }
770
+ case "singleSelect": {
771
+ let schema = z2.string();
772
+ if (Array.isArray(field.options) && field.options.length > 0) {
773
+ const allowed = new Set(field.options);
774
+ schema = schema.refine(
775
+ (value) => allowed.has(value),
776
+ "Value must be one of the defined options."
777
+ );
778
+ }
779
+ return isRequired ? schema : schema.optional();
780
+ }
781
+ case "multiSelect": {
782
+ let schema = z2.array(z2.string());
783
+ if (Array.isArray(field.options) && field.options.length > 0 && !field.allowOther) {
784
+ const allowed = new Set(field.options);
785
+ schema = schema.refine(
786
+ (values) => values.every((value) => allowed.has(value)),
787
+ "All values must be among the defined options."
788
+ );
789
+ }
790
+ if (typeof field.minSelections === "number") {
791
+ schema = schema.min(
792
+ field.minSelections,
793
+ `Select at least ${field.minSelections} option(s).`
794
+ );
795
+ } else if (isRequired) {
796
+ schema = schema.min(
797
+ 1,
798
+ "Select at least one option."
799
+ );
800
+ }
801
+ if (typeof field.maxSelections === "number") {
802
+ schema = schema.max(
803
+ field.maxSelections,
804
+ `Select at most ${field.maxSelections} option(s).`
805
+ );
806
+ }
807
+ return isRequired ? schema : schema.optional();
808
+ }
809
+ case "repeatGroup": {
810
+ const nestedFields = Array.isArray(field.fields) ? field.fields : [];
811
+ const rowShape = {};
812
+ for (const nested of nestedFields) {
813
+ rowShape[nested.id] = buildBaseFieldSchema(
814
+ nested
815
+ );
816
+ }
817
+ let schema = z2.array(z2.object(rowShape));
818
+ if (typeof field.min === "number") {
819
+ schema = schema.min(
820
+ field.min,
821
+ `Add at least ${field.min} item(s).`
822
+ );
823
+ }
824
+ if (typeof field.max === "number") {
825
+ schema = schema.max(
826
+ field.max,
827
+ `Add at most ${field.max} item(s).`
828
+ );
829
+ }
830
+ return isRequired ? schema : schema.optional();
831
+ }
832
+ default: {
833
+ return z2.any();
834
+ }
835
+ }
836
+ }
837
+ function buildConditionalFieldSchema(field, response) {
838
+ if (field.visibleIf) {
839
+ const visible = evaluateCondition(field.visibleIf, response);
840
+ if (!visible) {
841
+ return z2.any().optional();
842
+ }
843
+ }
844
+ let schema = buildBaseFieldSchema(field);
845
+ if (field.requiredIf) {
846
+ const shouldBeRequired = evaluateCondition(field.requiredIf, response);
847
+ if (shouldBeRequired) {
848
+ if (schema instanceof z2.ZodOptional) {
849
+ schema = schema.unwrap();
850
+ }
851
+ }
852
+ }
853
+ return schema;
854
+ }
855
+ function buildResponseSchemaWithConditions(template, response) {
856
+ const shape = {};
857
+ for (const section of template.sections) {
858
+ for (const field of section.fields) {
859
+ shape[field.id] = buildConditionalFieldSchema(field, response);
860
+ }
861
+ }
862
+ return z2.object(shape);
863
+ }
864
+ function validateReportResponse(template, data) {
865
+ if (typeof data !== "object" || data === null) {
866
+ throw new Error("Response must be an object");
867
+ }
868
+ const response = data;
869
+ const schema = buildResponseSchemaWithConditions(template, response);
870
+ const parsed = schema.parse(response);
871
+ for (const section of template.sections) {
872
+ for (const field of section.fields) {
873
+ if (field.visibleIf) {
874
+ const visible = evaluateCondition(field.visibleIf, response);
875
+ if (!visible) {
876
+ delete parsed[field.id];
877
+ }
878
+ }
879
+ }
880
+ }
881
+ return parsed;
882
+ }
883
+ function mapZodIssueToFieldErrorCode(issue) {
884
+ switch (issue.code) {
885
+ case z2.ZodIssueCode.invalid_type:
886
+ return "field.invalid_type";
887
+ case z2.ZodIssueCode.too_small:
888
+ return "field.too_small";
889
+ case z2.ZodIssueCode.too_big:
890
+ return "field.too_big";
891
+ case z2.ZodIssueCode.invalid_enum_value:
892
+ case z2.ZodIssueCode.invalid_literal:
893
+ return "field.invalid_option";
894
+ case z2.ZodIssueCode.custom:
895
+ return "field.custom";
896
+ default:
897
+ return "field.custom";
898
+ }
899
+ }
900
+ function buildFieldMetadataLookup(template) {
901
+ const map = /* @__PURE__ */ new Map();
902
+ for (const section of template.sections) {
903
+ for (const field of section.fields) {
904
+ map.set(field.id, {
905
+ sectionId: section.id,
906
+ sectionTitle: section.title,
907
+ label: field.label
908
+ });
909
+ }
910
+ }
911
+ return map;
912
+ }
913
+ function mapZodIssuesToResponseErrors(template, issues) {
914
+ const fieldMeta = buildFieldMetadataLookup(template);
915
+ const errors = [];
916
+ for (const issue of issues) {
917
+ const path = issue.path ?? [];
918
+ const topLevelFieldId = path.find(
919
+ (p) => typeof p === "string"
920
+ );
921
+ const rowIndex = path.find(
922
+ (p) => typeof p === "number"
923
+ );
924
+ const nestedFieldId = path.length >= 3 && typeof path[path.length - 1] === "string" ? path[path.length - 1] : void 0;
925
+ const fieldId = topLevelFieldId ?? "";
926
+ const meta = fieldId ? fieldMeta.get(fieldId) : void 0;
927
+ const code = mapZodIssueToFieldErrorCode(issue);
928
+ const sectionLabel = meta?.sectionTitle ?? meta?.sectionId;
929
+ const fieldLabel = meta?.label ?? fieldId;
930
+ let message;
931
+ if (rowIndex != null && nestedFieldId && meta && sectionLabel) {
932
+ message = `Section "${sectionLabel}" \u2192 "${meta.label}" (row ${rowIndex + 1}, field "${nestedFieldId}"): ${issue.message}`;
933
+ } else if (meta && sectionLabel && fieldLabel) {
934
+ message = `Section "${sectionLabel}" \u2192 Field "${fieldLabel}": ${issue.message}`;
935
+ } else if (fieldLabel) {
936
+ message = `Field "${fieldLabel}": ${issue.message}`;
937
+ } else {
938
+ message = issue.message;
939
+ }
940
+ errors.push({
941
+ fieldId,
942
+ sectionId: meta?.sectionId,
943
+ sectionTitle: meta?.sectionTitle,
944
+ label: meta?.label,
945
+ code,
946
+ message,
947
+ rawIssue: issue
948
+ });
949
+ }
950
+ return errors;
951
+ }
952
+ function validateReportResponseDetailed(template, data) {
953
+ if (typeof data !== "object" || data === null) {
954
+ return {
955
+ success: false,
956
+ errors: [
957
+ {
958
+ fieldId: "",
959
+ code: "response.invalid_root",
960
+ message: "Response must be an object."
961
+ }
962
+ ]
963
+ };
964
+ }
965
+ const response = data;
966
+ const schema = buildResponseSchemaWithConditions(template, response);
967
+ const result = schema.safeParse(response);
968
+ if (result.success) {
969
+ const parsed = { ...result.data };
970
+ for (const section of template.sections) {
971
+ for (const field of section.fields) {
972
+ if (field.visibleIf) {
973
+ const visible = evaluateCondition(field.visibleIf, response);
974
+ if (!visible) {
975
+ delete parsed[field.id];
976
+ }
977
+ }
978
+ }
979
+ }
980
+ return { success: true, value: parsed };
981
+ }
982
+ const errors = mapZodIssuesToResponseErrors(template, result.error.issues);
983
+ return { success: false, errors };
984
+ }
985
+ function explainValidationError(template, error) {
986
+ if (error instanceof ZodError) {
987
+ return mapZodIssuesToResponseErrors(template, error.issues);
988
+ }
989
+ return null;
990
+ }
882
991
  export {
883
992
  CORE_FIELD_DEFAULTS,
884
993
  Condition,
@@ -895,6 +1004,7 @@ export {
895
1004
  createUniqueId,
896
1005
  diffTemplates,
897
1006
  evaluateCondition,
1007
+ explainValidationError,
898
1008
  exportJSONSchema,
899
1009
  migrateLegacySchema,
900
1010
  normalizeReportTemplateSchema,