@frt-platform/report-core 1.0.1 → 1.2.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
@@ -21,18 +21,28 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  CORE_FIELD_DEFAULTS: () => CORE_FIELD_DEFAULTS,
24
+ Condition: () => Condition,
24
25
  DEFAULT_FIELD_LABEL: () => DEFAULT_FIELD_LABEL,
26
+ FieldRegistry: () => FieldRegistry,
25
27
  REPORT_TEMPLATE_FIELD_TYPES: () => REPORT_TEMPLATE_FIELD_TYPES,
26
28
  REPORT_TEMPLATE_VERSION: () => REPORT_TEMPLATE_VERSION,
29
+ RepeatGroupFieldSchema: () => RepeatGroupFieldSchema,
27
30
  ReportTemplateFieldSchema: () => ReportTemplateFieldSchema,
28
31
  ReportTemplateSchemaValidator: () => ReportTemplateSchemaValidator,
29
32
  ReportTemplateSectionSchema: () => ReportTemplateSectionSchema,
33
+ buildBaseFieldSchema: () => buildBaseFieldSchema,
34
+ buildResponseSchemaWithConditions: () => buildResponseSchemaWithConditions,
30
35
  createUniqueId: () => createUniqueId,
36
+ diffTemplates: () => diffTemplates,
37
+ evaluateCondition: () => evaluateCondition,
38
+ exportJSONSchema: () => exportJSONSchema,
31
39
  migrateLegacySchema: () => migrateLegacySchema,
32
40
  normalizeReportTemplateSchema: () => normalizeReportTemplateSchema,
33
41
  parseReportTemplateSchema: () => parseReportTemplateSchema,
34
42
  parseReportTemplateSchemaFromString: () => parseReportTemplateSchemaFromString,
35
- serializeReportTemplateSchema: () => serializeReportTemplateSchema
43
+ serializeReportTemplateSchema: () => serializeReportTemplateSchema,
44
+ validateReportResponse: () => validateReportResponse,
45
+ validateReportResponseDetailed: () => validateReportResponseDetailed
36
46
  });
37
47
  module.exports = __toCommonJS(index_exports);
38
48
 
@@ -46,16 +56,51 @@ var REPORT_TEMPLATE_FIELD_TYPES = [
46
56
  "date",
47
57
  "checkbox",
48
58
  "singleSelect",
49
- "multiSelect"
59
+ "multiSelect",
60
+ "repeatGroup"
61
+ // NEW FIELD TYPE
50
62
  ];
51
- var ReportTemplateFieldSchema = import_zod.z.object({
52
- id: import_zod.z.string().min(1).max(60).regex(/^[a-z0-9_-]+$/),
53
- type: import_zod.z.string(),
54
- label: import_zod.z.string().min(1).max(200),
63
+ var Condition = import_zod.z.lazy(
64
+ () => import_zod.z.union([
65
+ // equals: { equals: { fieldId: value } }
66
+ import_zod.z.object({
67
+ equals: import_zod.z.record(import_zod.z.any())
68
+ }),
69
+ // any: { any: [cond, cond] }
70
+ import_zod.z.object({
71
+ any: import_zod.z.array(Condition)
72
+ }),
73
+ // all: { all: [cond, cond] }
74
+ import_zod.z.object({
75
+ all: import_zod.z.array(Condition)
76
+ }),
77
+ // not: { not: cond }
78
+ import_zod.z.object({
79
+ not: Condition
80
+ })
81
+ ])
82
+ );
83
+ var BaseFieldSchema = import_zod.z.object({
84
+ id: import_zod.z.string().min(1, "Field identifiers cannot be empty.").max(60, "Field identifiers must be 60 characters or fewer.").regex(
85
+ /^[a-z0-9_-]+$/,
86
+ "Use lowercase letters, numbers, underscores, or dashes for field identifiers."
87
+ ),
88
+ type: import_zod.z.enum([
89
+ "shortText",
90
+ "longText",
91
+ "number",
92
+ "date",
93
+ "checkbox",
94
+ "singleSelect",
95
+ "multiSelect"
96
+ ]),
97
+ label: import_zod.z.string().min(1, "Field labels cannot be empty.").max(200, "Field labels must be 200 characters or fewer."),
55
98
  required: import_zod.z.boolean().optional(),
56
- description: import_zod.z.string().max(400).optional(),
57
- placeholder: import_zod.z.string().max(200).optional(),
58
- options: import_zod.z.array(import_zod.z.string().min(1).max(120)).optional(),
99
+ description: import_zod.z.string().max(400, "Descriptions must be 400 characters or fewer.").optional(),
100
+ placeholder: import_zod.z.string().max(200, "Placeholders must be 200 characters or fewer.").optional(),
101
+ options: import_zod.z.array(
102
+ import_zod.z.string().min(1, "Options cannot be empty.").max(120, "Options must be 120 characters or fewer.")
103
+ ).optional(),
59
104
  allowOther: import_zod.z.boolean().optional(),
60
105
  defaultValue: import_zod.z.union([
61
106
  import_zod.z.string(),
@@ -64,36 +109,90 @@ var ReportTemplateFieldSchema = import_zod.z.object({
64
109
  import_zod.z.array(import_zod.z.string()),
65
110
  import_zod.z.null()
66
111
  ]).optional(),
112
+ // Conditional logic
113
+ visibleIf: Condition.optional(),
114
+ requiredIf: Condition.optional(),
115
+ // Text constraints
67
116
  minLength: import_zod.z.number().int().min(0).optional(),
68
117
  maxLength: import_zod.z.number().int().min(0).optional(),
118
+ // Number constraints
69
119
  minValue: import_zod.z.number().optional(),
70
120
  maxValue: import_zod.z.number().optional(),
71
121
  step: import_zod.z.number().nonnegative().optional(),
122
+ // Multi-select constraints
72
123
  minSelections: import_zod.z.number().int().min(0).optional(),
73
124
  maxSelections: import_zod.z.number().int().min(0).optional(),
125
+ // Privacy
74
126
  dataClassification: import_zod.z.enum(["none", "personal", "special"]).optional()
75
127
  });
128
+ var RepeatGroupFieldSchema = import_zod.z.lazy(
129
+ () => import_zod.z.object({
130
+ id: import_zod.z.string().min(1).max(60).regex(
131
+ /^[a-z0-9_-]+$/,
132
+ "Use lowercase letters, numbers, underscores, or dashes for field identifiers."
133
+ ),
134
+ type: import_zod.z.literal("repeatGroup"),
135
+ label: import_zod.z.string().min(1).max(200),
136
+ // Conditional logic for the whole group
137
+ visibleIf: Condition.optional(),
138
+ requiredIf: Condition.optional(),
139
+ // NEW: Dynamic and static min/max
140
+ min: import_zod.z.number().int().min(0).optional(),
141
+ max: import_zod.z.number().int().min(1).optional(),
142
+ minIf: Condition.optional(),
143
+ maxIf: Condition.optional(),
144
+ // Contents: nested fields (recursive)
145
+ fields: import_zod.z.array(
146
+ import_zod.z.lazy(() => ReportTemplateFieldSchema)
147
+ ).min(1, "Repeat groups must contain at least one field."),
148
+ // Pre-populated rows allowed
149
+ defaultValue: import_zod.z.array(import_zod.z.record(import_zod.z.any())).optional()
150
+ })
151
+ );
152
+ var ReportTemplateFieldSchema = import_zod.z.lazy(
153
+ () => import_zod.z.union([
154
+ BaseFieldSchema,
155
+ RepeatGroupFieldSchema
156
+ ])
157
+ );
76
158
  var ReportTemplateSectionSchema = import_zod.z.object({
77
- id: import_zod.z.string().min(1).max(60).regex(/^[a-z0-9_-]+$/),
78
- title: import_zod.z.string().max(200).optional(),
79
- description: import_zod.z.string().max(500).optional(),
80
- fields: import_zod.z.array(ReportTemplateFieldSchema).default([])
159
+ id: import_zod.z.string().min(1, "Section identifiers cannot be empty.").max(60, "Section identifiers must be 60 characters or fewer.").regex(
160
+ /^[a-z0-9_-]+$/,
161
+ "Use lowercase letters, numbers, underscores, or dashes for section identifiers."
162
+ ),
163
+ title: import_zod.z.string().max(200, "Section titles must be 200 characters or fewer.").optional(),
164
+ description: import_zod.z.string().max(500, "Section descriptions must be 500 characters or fewer.").optional(),
165
+ fields: import_zod.z.array(ReportTemplateFieldSchema).max(50, "Sections can include at most 50 fields.").default([])
81
166
  });
82
167
  var ReportTemplateSchemaValidator = import_zod.z.object({
83
- version: import_zod.z.number().int().min(1).default(REPORT_TEMPLATE_VERSION),
84
- title: import_zod.z.string().max(200).optional(),
85
- description: import_zod.z.string().max(500).optional(),
86
- sections: import_zod.z.array(ReportTemplateSectionSchema).max(25).default([])
168
+ version: import_zod.z.number().int().min(1).max(REPORT_TEMPLATE_VERSION).default(REPORT_TEMPLATE_VERSION),
169
+ title: import_zod.z.string().max(200, "Template titles must be 200 characters or fewer.").optional(),
170
+ description: import_zod.z.string().max(500, "Template descriptions must be 500 characters or fewer.").optional(),
171
+ sections: import_zod.z.array(ReportTemplateSectionSchema).max(25, "Templates can include at most 25 sections.").default([])
87
172
  });
88
173
 
89
174
  // src/fields.ts
90
175
  var DEFAULT_FIELD_LABEL = "Untitled question";
91
176
  var CORE_FIELD_DEFAULTS = {
92
- shortText: { label: DEFAULT_FIELD_LABEL, placeholder: "Short answer text" },
93
- longText: { label: DEFAULT_FIELD_LABEL, placeholder: "Long answer text" },
94
- number: { label: DEFAULT_FIELD_LABEL, placeholder: "123" },
95
- date: { label: DEFAULT_FIELD_LABEL },
96
- checkbox: { label: DEFAULT_FIELD_LABEL, placeholder: "Check to confirm" },
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
+ },
97
196
  singleSelect: {
98
197
  label: DEFAULT_FIELD_LABEL,
99
198
  options: ["Option 1", "Option 2", "Option 3"]
@@ -102,6 +201,13 @@ var CORE_FIELD_DEFAULTS = {
102
201
  label: DEFAULT_FIELD_LABEL,
103
202
  options: ["Option 1", "Option 2", "Option 3"],
104
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
105
211
  }
106
212
  };
107
213
 
@@ -121,16 +227,20 @@ function createUniqueId(prefix, existing) {
121
227
  var LEGACY_FIELD_TYPE_MAP = {
122
228
  shorttext: "shortText",
123
229
  text: "shortText",
230
+ "text-box": "shortText",
124
231
  longtext: "longText",
125
232
  textarea: "longText",
233
+ paragraph: "longText",
126
234
  number: "number",
127
235
  numeric: "number",
128
236
  date: "date",
129
237
  checkbox: "checkbox",
130
238
  boolean: "checkbox",
239
+ singleselect: "singleSelect",
131
240
  select: "singleSelect",
132
241
  dropdown: "singleSelect",
133
242
  radio: "singleSelect",
243
+ "multi-select": "multiSelect",
134
244
  multiselect: "multiSelect",
135
245
  checkboxes: "multiSelect"
136
246
  };
@@ -168,63 +278,678 @@ function migrateLegacySchema(raw) {
168
278
  }
169
279
 
170
280
  // src/normalize.ts
171
- function parseReportTemplateSchema(raw) {
172
- const migrated = migrateLegacySchema(raw);
173
- const parsed = ReportTemplateSchemaValidator.parse(migrated);
174
- return normalizeReportTemplateSchema(parsed);
281
+ function normalizeId(raw, fallback) {
282
+ if (!raw || typeof raw !== "string") return fallback;
283
+ const cleaned = raw.trim().toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_-]/g, "");
284
+ return cleaned.length > 0 ? cleaned : fallback;
285
+ }
286
+ function ensureUniqueId(id, used) {
287
+ let final = id;
288
+ let counter = 1;
289
+ while (used.has(final)) {
290
+ final = `${id}-${counter++}`;
291
+ }
292
+ used.add(final);
293
+ return final;
294
+ }
295
+ function normalizeField(field, usedFieldIds, index) {
296
+ const fallbackId = `field_${index + 1}`;
297
+ const normalizedId = normalizeId(field.id, fallbackId);
298
+ const uniqueId = ensureUniqueId(normalizedId, usedFieldIds);
299
+ const base = {
300
+ ...field,
301
+ id: uniqueId
302
+ };
303
+ if (field.type === "repeatGroup") {
304
+ const nestedUsed = /* @__PURE__ */ new Set();
305
+ const nestedFields = Array.isArray(field.fields) ? field.fields : [];
306
+ base.fields = nestedFields.map(
307
+ (inner, idx) => normalizeField(inner, nestedUsed, idx)
308
+ );
309
+ }
310
+ return base;
311
+ }
312
+ function normalizeSection(section, sectionIndex) {
313
+ const fallbackId = `section_${sectionIndex + 1}`;
314
+ const normalizedId = normalizeId(section.id, fallbackId);
315
+ const base = {
316
+ ...section,
317
+ id: normalizedId
318
+ };
319
+ const usedFieldIds = /* @__PURE__ */ new Set();
320
+ base.fields = (section.fields ?? []).map(
321
+ (field, idx) => normalizeField(field, usedFieldIds, idx)
322
+ );
323
+ return base;
175
324
  }
176
325
  function normalizeReportTemplateSchema(schema) {
177
326
  const sections = schema.sections.length > 0 ? schema.sections : [
178
327
  {
179
- id: "section-1",
328
+ id: "section_1",
180
329
  title: "",
181
330
  description: "",
182
331
  fields: []
183
332
  }
184
333
  ];
185
- const normalizedSections = sections.map((sec, i) => {
186
- const seen = /* @__PURE__ */ new Set();
187
- const safeSectionId = sec.id.trim() || `section-${i + 1}`;
188
- const fields = sec.fields.map((field, idx) => {
189
- const trimmed = field.id.trim() || `field-${idx + 1}`;
190
- const safeId = seen.has(trimmed) ? `field-${idx + 1}` : trimmed;
191
- seen.add(safeId);
192
- return {
193
- ...field,
194
- id: safeId
195
- };
196
- });
197
- return {
198
- ...sec,
199
- id: safeSectionId,
200
- fields
201
- };
334
+ const usedSectionIds = /* @__PURE__ */ new Set();
335
+ const normalizedSections = sections.map((section, idx) => {
336
+ const normalized = normalizeSection(section, idx);
337
+ normalized.id = ensureUniqueId(normalized.id, usedSectionIds);
338
+ return normalized;
202
339
  });
203
340
  return {
204
341
  ...schema,
205
342
  sections: normalizedSections
206
343
  };
207
344
  }
345
+ function parseReportTemplateSchema(raw) {
346
+ const migrated = migrateLegacySchema(raw);
347
+ const parsed = ReportTemplateSchemaValidator.parse(migrated);
348
+ return normalizeReportTemplateSchema(parsed);
349
+ }
208
350
  function parseReportTemplateSchemaFromString(raw) {
209
- const obj = JSON.parse(raw);
210
- return parseReportTemplateSchema(obj);
351
+ return parseReportTemplateSchema(JSON.parse(raw));
211
352
  }
212
353
  function serializeReportTemplateSchema(schema) {
213
354
  return JSON.stringify(schema, null, 2);
214
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;
405
+ });
406
+ }
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;
422
+ }
423
+
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();
502
+ }
503
+ }
504
+ }
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();
510
+ }
511
+ }
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
+ });
696
+ }
697
+ }
698
+ }
699
+ for (const f of afterFields) {
700
+ const idx = beforeIndex[f.id];
701
+ if (idx == null) continue;
702
+ const before = beforeFields[idx];
703
+ const changes = diffObjectProperties(before, f, ["id", "fields"]);
704
+ if (changes.length > 0) {
705
+ empty.modifiedFields.push({
706
+ sectionId,
707
+ fieldId: f.id,
708
+ before,
709
+ after: f,
710
+ changes
711
+ });
712
+ }
713
+ if (f.type === "repeatGroup" && Array.isArray(f.fields)) {
714
+ const innerBefore = before.fields ?? [];
715
+ const innerAfter = f.fields;
716
+ const nested = diffRepeatGroupFields(
717
+ innerBefore,
718
+ innerAfter,
719
+ sectionId,
720
+ f.id
721
+ );
722
+ if (nested.addedFields.length > 0 || nested.removedFields.length > 0 || nested.reorderedFields.length > 0 || nested.modifiedFields.length > 0) {
723
+ empty.nestedFieldDiffs.push({
724
+ sectionId,
725
+ groupId: f.id,
726
+ diffs: nested
727
+ });
728
+ }
729
+ }
730
+ }
731
+ return empty;
732
+ }
733
+ function diffTemplates(before, after) {
734
+ const diff = {
735
+ addedSections: [],
736
+ removedSections: [],
737
+ reorderedSections: [],
738
+ modifiedSections: [],
739
+ addedFields: [],
740
+ removedFields: [],
741
+ reorderedFields: [],
742
+ modifiedFields: [],
743
+ nestedFieldDiffs: []
744
+ };
745
+ const beforeSections = before.sections;
746
+ const afterSections = after.sections;
747
+ const beforeSecIndex = indexById(beforeSections);
748
+ const afterSecIndex = indexById(afterSections);
749
+ for (const sec of afterSections) {
750
+ if (!(sec.id in beforeSecIndex)) {
751
+ diff.addedSections.push({
752
+ sectionId: sec.id,
753
+ index: afterSecIndex[sec.id]
754
+ });
755
+ }
756
+ }
757
+ for (const sec of beforeSections) {
758
+ if (!(sec.id in afterSecIndex)) {
759
+ diff.removedSections.push({
760
+ sectionId: sec.id,
761
+ index: beforeSecIndex[sec.id]
762
+ });
763
+ }
764
+ }
765
+ for (const sec of afterSections) {
766
+ if (sec.id in beforeSecIndex) {
767
+ const from = beforeSecIndex[sec.id];
768
+ const to = afterSecIndex[sec.id];
769
+ if (from !== to) {
770
+ diff.reorderedSections.push({ sectionId: sec.id, from, to });
771
+ }
772
+ }
773
+ }
774
+ for (const sec of afterSections) {
775
+ const idx = beforeSecIndex[sec.id];
776
+ if (idx == null) continue;
777
+ const beforeSec = beforeSections[idx];
778
+ const changes = diffObjectProperties(beforeSec, sec, ["id", "fields"]);
779
+ if (changes.length > 0) {
780
+ diff.modifiedSections.push({
781
+ sectionId: sec.id,
782
+ changes
783
+ });
784
+ }
785
+ }
786
+ for (const afterSec of afterSections) {
787
+ const secId = afterSec.id;
788
+ const beforeIdx = beforeSecIndex[secId];
789
+ if (beforeIdx == null) continue;
790
+ const beforeSec = beforeSections[beforeIdx];
791
+ const beforeFields = beforeSec.fields ?? [];
792
+ const afterFields = afterSec.fields ?? [];
793
+ const nestedDiff = diffRepeatGroupFields(
794
+ beforeFields,
795
+ afterFields,
796
+ secId,
797
+ ""
798
+ );
799
+ diff.addedFields.push(...nestedDiff.addedFields);
800
+ diff.removedFields.push(...nestedDiff.removedFields);
801
+ diff.reorderedFields.push(...nestedDiff.reorderedFields);
802
+ diff.modifiedFields.push(...nestedDiff.modifiedFields);
803
+ diff.nestedFieldDiffs.push(...nestedDiff.nestedFieldDiffs);
804
+ }
805
+ return diff;
806
+ }
807
+
808
+ // src/jsonSchema.ts
809
+ function mapFieldTypeToJSONSchemaCore(field) {
810
+ const common = {
811
+ title: field.label,
812
+ description: field.description,
813
+ default: field.defaultValue ?? void 0,
814
+ "x-frt-placeholder": field.placeholder,
815
+ "x-frt-dataClassification": field.dataClassification,
816
+ "x-frt-visibleIf": field.visibleIf,
817
+ "x-frt-requiredIf": field.requiredIf
818
+ };
819
+ switch (field.type) {
820
+ case "shortText":
821
+ case "longText": {
822
+ const schema = {
823
+ ...common,
824
+ type: "string"
825
+ };
826
+ if (typeof field.minLength === "number") {
827
+ schema.minLength = field.minLength;
828
+ }
829
+ if (typeof field.maxLength === "number") {
830
+ schema.maxLength = field.maxLength;
831
+ }
832
+ return schema;
833
+ }
834
+ case "number": {
835
+ const schema = {
836
+ ...common,
837
+ type: "number"
838
+ };
839
+ if (typeof field.minValue === "number") {
840
+ schema.minimum = field.minValue;
841
+ }
842
+ if (typeof field.maxValue === "number") {
843
+ schema.maximum = field.maxValue;
844
+ }
845
+ return schema;
846
+ }
847
+ case "date": {
848
+ const schema = {
849
+ ...common,
850
+ type: "string",
851
+ format: "date-time"
852
+ };
853
+ return schema;
854
+ }
855
+ case "checkbox": {
856
+ const schema = {
857
+ ...common,
858
+ type: "boolean"
859
+ };
860
+ if (field.required && !field.requiredIf) {
861
+ schema.const = true;
862
+ }
863
+ return schema;
864
+ }
865
+ case "singleSelect": {
866
+ const schema = {
867
+ ...common,
868
+ type: "string"
869
+ };
870
+ if (Array.isArray(field.options) && field.options.length > 0) {
871
+ schema.enum = field.options;
872
+ }
873
+ return schema;
874
+ }
875
+ case "multiSelect": {
876
+ const schema = {
877
+ ...common,
878
+ type: "array",
879
+ items: {
880
+ type: "string"
881
+ }
882
+ };
883
+ const hasOptions = Array.isArray(field.options) && field.options.length > 0;
884
+ if (hasOptions && !field.allowOther) {
885
+ schema.items = {
886
+ type: "string",
887
+ enum: field.options
888
+ };
889
+ }
890
+ if (typeof field.minSelections === "number") {
891
+ schema.minItems = field.minSelections;
892
+ }
893
+ if (typeof field.maxSelections === "number") {
894
+ schema.maxItems = field.maxSelections;
895
+ }
896
+ return schema;
897
+ }
898
+ default: {
899
+ return {
900
+ ...common
901
+ };
902
+ }
903
+ }
904
+ }
905
+ function exportJSONSchema(template) {
906
+ const properties = {};
907
+ const required = [];
908
+ for (const section of template.sections) {
909
+ for (const field of section.fields) {
910
+ properties[field.id] = mapFieldTypeToJSONSchemaCore(field);
911
+ if (field.required === true && !field.requiredIf && !field.visibleIf) {
912
+ required.push(field.id);
913
+ }
914
+ }
915
+ }
916
+ const schema = {
917
+ $schema: "https://json-schema.org/draft/2020-12/schema",
918
+ type: "object",
919
+ properties,
920
+ additionalProperties: false,
921
+ "x-frt-templateTitle": template.title,
922
+ "x-frt-templateDescription": template.description,
923
+ "x-frt-version": template.version
924
+ };
925
+ if (required.length > 0) {
926
+ schema.required = required;
927
+ }
928
+ return schema;
929
+ }
215
930
  // Annotate the CommonJS export names for ESM import in node:
216
931
  0 && (module.exports = {
217
932
  CORE_FIELD_DEFAULTS,
933
+ Condition,
218
934
  DEFAULT_FIELD_LABEL,
935
+ FieldRegistry,
219
936
  REPORT_TEMPLATE_FIELD_TYPES,
220
937
  REPORT_TEMPLATE_VERSION,
938
+ RepeatGroupFieldSchema,
221
939
  ReportTemplateFieldSchema,
222
940
  ReportTemplateSchemaValidator,
223
941
  ReportTemplateSectionSchema,
942
+ buildBaseFieldSchema,
943
+ buildResponseSchemaWithConditions,
224
944
  createUniqueId,
945
+ diffTemplates,
946
+ evaluateCondition,
947
+ exportJSONSchema,
225
948
  migrateLegacySchema,
226
949
  normalizeReportTemplateSchema,
227
950
  parseReportTemplateSchema,
228
951
  parseReportTemplateSchemaFromString,
229
- serializeReportTemplateSchema
952
+ serializeReportTemplateSchema,
953
+ validateReportResponse,
954
+ validateReportResponseDetailed
230
955
  });