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