@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.mjs CHANGED
@@ -8,16 +8,51 @@ var REPORT_TEMPLATE_FIELD_TYPES = [
8
8
  "date",
9
9
  "checkbox",
10
10
  "singleSelect",
11
- "multiSelect"
11
+ "multiSelect",
12
+ "repeatGroup"
13
+ // NEW FIELD TYPE
12
14
  ];
13
- var ReportTemplateFieldSchema = z.object({
14
- id: z.string().min(1).max(60).regex(/^[a-z0-9_-]+$/),
15
- type: z.string(),
16
- label: z.string().min(1).max(200),
15
+ var Condition = z.lazy(
16
+ () => z.union([
17
+ // equals: { equals: { fieldId: value } }
18
+ z.object({
19
+ equals: z.record(z.any())
20
+ }),
21
+ // any: { any: [cond, cond] }
22
+ z.object({
23
+ any: z.array(Condition)
24
+ }),
25
+ // all: { all: [cond, cond] }
26
+ z.object({
27
+ all: z.array(Condition)
28
+ }),
29
+ // not: { not: cond }
30
+ z.object({
31
+ not: Condition
32
+ })
33
+ ])
34
+ );
35
+ var BaseFieldSchema = z.object({
36
+ id: z.string().min(1, "Field identifiers cannot be empty.").max(60, "Field identifiers must be 60 characters or fewer.").regex(
37
+ /^[a-z0-9_-]+$/,
38
+ "Use lowercase letters, numbers, underscores, or dashes for field identifiers."
39
+ ),
40
+ type: z.enum([
41
+ "shortText",
42
+ "longText",
43
+ "number",
44
+ "date",
45
+ "checkbox",
46
+ "singleSelect",
47
+ "multiSelect"
48
+ ]),
49
+ label: z.string().min(1, "Field labels cannot be empty.").max(200, "Field labels must be 200 characters or fewer."),
17
50
  required: z.boolean().optional(),
18
- description: z.string().max(400).optional(),
19
- placeholder: z.string().max(200).optional(),
20
- options: z.array(z.string().min(1).max(120)).optional(),
51
+ description: z.string().max(400, "Descriptions must be 400 characters or fewer.").optional(),
52
+ placeholder: z.string().max(200, "Placeholders must be 200 characters or fewer.").optional(),
53
+ options: z.array(
54
+ z.string().min(1, "Options cannot be empty.").max(120, "Options must be 120 characters or fewer.")
55
+ ).optional(),
21
56
  allowOther: z.boolean().optional(),
22
57
  defaultValue: z.union([
23
58
  z.string(),
@@ -26,36 +61,90 @@ var ReportTemplateFieldSchema = z.object({
26
61
  z.array(z.string()),
27
62
  z.null()
28
63
  ]).optional(),
64
+ // Conditional logic
65
+ visibleIf: Condition.optional(),
66
+ requiredIf: Condition.optional(),
67
+ // Text constraints
29
68
  minLength: z.number().int().min(0).optional(),
30
69
  maxLength: z.number().int().min(0).optional(),
70
+ // Number constraints
31
71
  minValue: z.number().optional(),
32
72
  maxValue: z.number().optional(),
33
73
  step: z.number().nonnegative().optional(),
74
+ // Multi-select constraints
34
75
  minSelections: z.number().int().min(0).optional(),
35
76
  maxSelections: z.number().int().min(0).optional(),
77
+ // Privacy
36
78
  dataClassification: z.enum(["none", "personal", "special"]).optional()
37
79
  });
80
+ var RepeatGroupFieldSchema = z.lazy(
81
+ () => z.object({
82
+ id: z.string().min(1).max(60).regex(
83
+ /^[a-z0-9_-]+$/,
84
+ "Use lowercase letters, numbers, underscores, or dashes for field identifiers."
85
+ ),
86
+ type: z.literal("repeatGroup"),
87
+ label: z.string().min(1).max(200),
88
+ // Conditional logic for the whole group
89
+ visibleIf: Condition.optional(),
90
+ requiredIf: Condition.optional(),
91
+ // NEW: Dynamic and static min/max
92
+ min: z.number().int().min(0).optional(),
93
+ max: z.number().int().min(1).optional(),
94
+ minIf: Condition.optional(),
95
+ maxIf: Condition.optional(),
96
+ // Contents: nested fields (recursive)
97
+ fields: z.array(
98
+ z.lazy(() => ReportTemplateFieldSchema)
99
+ ).min(1, "Repeat groups must contain at least one field."),
100
+ // Pre-populated rows allowed
101
+ defaultValue: z.array(z.record(z.any())).optional()
102
+ })
103
+ );
104
+ var ReportTemplateFieldSchema = z.lazy(
105
+ () => z.union([
106
+ BaseFieldSchema,
107
+ RepeatGroupFieldSchema
108
+ ])
109
+ );
38
110
  var ReportTemplateSectionSchema = z.object({
39
- id: z.string().min(1).max(60).regex(/^[a-z0-9_-]+$/),
40
- title: z.string().max(200).optional(),
41
- description: z.string().max(500).optional(),
42
- fields: z.array(ReportTemplateFieldSchema).default([])
111
+ id: z.string().min(1, "Section identifiers cannot be empty.").max(60, "Section identifiers must be 60 characters or fewer.").regex(
112
+ /^[a-z0-9_-]+$/,
113
+ "Use lowercase letters, numbers, underscores, or dashes for section identifiers."
114
+ ),
115
+ title: z.string().max(200, "Section titles must be 200 characters or fewer.").optional(),
116
+ description: z.string().max(500, "Section descriptions must be 500 characters or fewer.").optional(),
117
+ fields: z.array(ReportTemplateFieldSchema).max(50, "Sections can include at most 50 fields.").default([])
43
118
  });
44
119
  var ReportTemplateSchemaValidator = z.object({
45
- version: z.number().int().min(1).default(REPORT_TEMPLATE_VERSION),
46
- title: z.string().max(200).optional(),
47
- description: z.string().max(500).optional(),
48
- sections: z.array(ReportTemplateSectionSchema).max(25).default([])
120
+ version: z.number().int().min(1).max(REPORT_TEMPLATE_VERSION).default(REPORT_TEMPLATE_VERSION),
121
+ title: z.string().max(200, "Template titles must be 200 characters or fewer.").optional(),
122
+ description: z.string().max(500, "Template descriptions must be 500 characters or fewer.").optional(),
123
+ sections: z.array(ReportTemplateSectionSchema).max(25, "Templates can include at most 25 sections.").default([])
49
124
  });
50
125
 
51
126
  // src/fields.ts
52
127
  var DEFAULT_FIELD_LABEL = "Untitled question";
53
128
  var CORE_FIELD_DEFAULTS = {
54
- shortText: { label: DEFAULT_FIELD_LABEL, placeholder: "Short answer text" },
55
- longText: { label: DEFAULT_FIELD_LABEL, placeholder: "Long answer text" },
56
- number: { label: DEFAULT_FIELD_LABEL, placeholder: "123" },
57
- date: { label: DEFAULT_FIELD_LABEL },
58
- checkbox: { label: DEFAULT_FIELD_LABEL, placeholder: "Check to confirm" },
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
+ },
59
148
  singleSelect: {
60
149
  label: DEFAULT_FIELD_LABEL,
61
150
  options: ["Option 1", "Option 2", "Option 3"]
@@ -64,6 +153,13 @@ var CORE_FIELD_DEFAULTS = {
64
153
  label: DEFAULT_FIELD_LABEL,
65
154
  options: ["Option 1", "Option 2", "Option 3"],
66
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
67
163
  }
68
164
  };
69
165
 
@@ -83,16 +179,20 @@ function createUniqueId(prefix, existing) {
83
179
  var LEGACY_FIELD_TYPE_MAP = {
84
180
  shorttext: "shortText",
85
181
  text: "shortText",
182
+ "text-box": "shortText",
86
183
  longtext: "longText",
87
184
  textarea: "longText",
185
+ paragraph: "longText",
88
186
  number: "number",
89
187
  numeric: "number",
90
188
  date: "date",
91
189
  checkbox: "checkbox",
92
190
  boolean: "checkbox",
191
+ singleselect: "singleSelect",
93
192
  select: "singleSelect",
94
193
  dropdown: "singleSelect",
95
194
  radio: "singleSelect",
195
+ "multi-select": "multiSelect",
96
196
  multiselect: "multiSelect",
97
197
  checkboxes: "multiSelect"
98
198
  };
@@ -130,62 +230,677 @@ function migrateLegacySchema(raw) {
130
230
  }
131
231
 
132
232
  // src/normalize.ts
133
- function parseReportTemplateSchema(raw) {
134
- const migrated = migrateLegacySchema(raw);
135
- const parsed = ReportTemplateSchemaValidator.parse(migrated);
136
- return normalizeReportTemplateSchema(parsed);
233
+ function normalizeId(raw, fallback) {
234
+ if (!raw || typeof raw !== "string") return fallback;
235
+ const cleaned = raw.trim().toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_-]/g, "");
236
+ return cleaned.length > 0 ? cleaned : fallback;
237
+ }
238
+ function ensureUniqueId(id, used) {
239
+ let final = id;
240
+ let counter = 1;
241
+ while (used.has(final)) {
242
+ final = `${id}-${counter++}`;
243
+ }
244
+ used.add(final);
245
+ return final;
246
+ }
247
+ function normalizeField(field, usedFieldIds, index) {
248
+ const fallbackId = `field_${index + 1}`;
249
+ const normalizedId = normalizeId(field.id, fallbackId);
250
+ const uniqueId = ensureUniqueId(normalizedId, usedFieldIds);
251
+ const base = {
252
+ ...field,
253
+ id: uniqueId
254
+ };
255
+ if (field.type === "repeatGroup") {
256
+ const nestedUsed = /* @__PURE__ */ new Set();
257
+ const nestedFields = Array.isArray(field.fields) ? field.fields : [];
258
+ base.fields = nestedFields.map(
259
+ (inner, idx) => normalizeField(inner, nestedUsed, idx)
260
+ );
261
+ }
262
+ return base;
263
+ }
264
+ function normalizeSection(section, sectionIndex) {
265
+ const fallbackId = `section_${sectionIndex + 1}`;
266
+ const normalizedId = normalizeId(section.id, fallbackId);
267
+ const base = {
268
+ ...section,
269
+ id: normalizedId
270
+ };
271
+ const usedFieldIds = /* @__PURE__ */ new Set();
272
+ base.fields = (section.fields ?? []).map(
273
+ (field, idx) => normalizeField(field, usedFieldIds, idx)
274
+ );
275
+ return base;
137
276
  }
138
277
  function normalizeReportTemplateSchema(schema) {
139
278
  const sections = schema.sections.length > 0 ? schema.sections : [
140
279
  {
141
- id: "section-1",
280
+ id: "section_1",
142
281
  title: "",
143
282
  description: "",
144
283
  fields: []
145
284
  }
146
285
  ];
147
- const normalizedSections = sections.map((sec, i) => {
148
- const seen = /* @__PURE__ */ new Set();
149
- const safeSectionId = sec.id.trim() || `section-${i + 1}`;
150
- const fields = sec.fields.map((field, idx) => {
151
- const trimmed = field.id.trim() || `field-${idx + 1}`;
152
- const safeId = seen.has(trimmed) ? `field-${idx + 1}` : trimmed;
153
- seen.add(safeId);
154
- return {
155
- ...field,
156
- id: safeId
157
- };
158
- });
159
- return {
160
- ...sec,
161
- id: safeSectionId,
162
- fields
163
- };
286
+ const usedSectionIds = /* @__PURE__ */ new Set();
287
+ const normalizedSections = sections.map((section, idx) => {
288
+ const normalized = normalizeSection(section, idx);
289
+ normalized.id = ensureUniqueId(normalized.id, usedSectionIds);
290
+ return normalized;
164
291
  });
165
292
  return {
166
293
  ...schema,
167
294
  sections: normalizedSections
168
295
  };
169
296
  }
297
+ function parseReportTemplateSchema(raw) {
298
+ const migrated = migrateLegacySchema(raw);
299
+ const parsed = ReportTemplateSchemaValidator.parse(migrated);
300
+ return normalizeReportTemplateSchema(parsed);
301
+ }
170
302
  function parseReportTemplateSchemaFromString(raw) {
171
- const obj = JSON.parse(raw);
172
- return parseReportTemplateSchema(obj);
303
+ return parseReportTemplateSchema(JSON.parse(raw));
173
304
  }
174
305
  function serializeReportTemplateSchema(schema) {
175
306
  return JSON.stringify(schema, null, 2);
176
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;
357
+ });
358
+ }
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;
374
+ }
375
+
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();
454
+ }
455
+ }
456
+ }
457
+ function buildConditionalFieldSchema(field, response) {
458
+ if (field.visibleIf) {
459
+ const visible = evaluateCondition(field.visibleIf, response);
460
+ if (!visible) {
461
+ return z2.any().optional();
462
+ }
463
+ }
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
+ });
648
+ }
649
+ }
650
+ }
651
+ for (const f of afterFields) {
652
+ const idx = beforeIndex[f.id];
653
+ if (idx == null) continue;
654
+ const before = beforeFields[idx];
655
+ const changes = diffObjectProperties(before, f, ["id", "fields"]);
656
+ if (changes.length > 0) {
657
+ empty.modifiedFields.push({
658
+ sectionId,
659
+ fieldId: f.id,
660
+ before,
661
+ after: f,
662
+ changes
663
+ });
664
+ }
665
+ if (f.type === "repeatGroup" && Array.isArray(f.fields)) {
666
+ const innerBefore = before.fields ?? [];
667
+ const innerAfter = f.fields;
668
+ const nested = diffRepeatGroupFields(
669
+ innerBefore,
670
+ innerAfter,
671
+ sectionId,
672
+ f.id
673
+ );
674
+ if (nested.addedFields.length > 0 || nested.removedFields.length > 0 || nested.reorderedFields.length > 0 || nested.modifiedFields.length > 0) {
675
+ empty.nestedFieldDiffs.push({
676
+ sectionId,
677
+ groupId: f.id,
678
+ diffs: nested
679
+ });
680
+ }
681
+ }
682
+ }
683
+ return empty;
684
+ }
685
+ function diffTemplates(before, after) {
686
+ const diff = {
687
+ addedSections: [],
688
+ removedSections: [],
689
+ reorderedSections: [],
690
+ modifiedSections: [],
691
+ addedFields: [],
692
+ removedFields: [],
693
+ reorderedFields: [],
694
+ modifiedFields: [],
695
+ nestedFieldDiffs: []
696
+ };
697
+ const beforeSections = before.sections;
698
+ const afterSections = after.sections;
699
+ const beforeSecIndex = indexById(beforeSections);
700
+ const afterSecIndex = indexById(afterSections);
701
+ for (const sec of afterSections) {
702
+ if (!(sec.id in beforeSecIndex)) {
703
+ diff.addedSections.push({
704
+ sectionId: sec.id,
705
+ index: afterSecIndex[sec.id]
706
+ });
707
+ }
708
+ }
709
+ for (const sec of beforeSections) {
710
+ if (!(sec.id in afterSecIndex)) {
711
+ diff.removedSections.push({
712
+ sectionId: sec.id,
713
+ index: beforeSecIndex[sec.id]
714
+ });
715
+ }
716
+ }
717
+ for (const sec of afterSections) {
718
+ if (sec.id in beforeSecIndex) {
719
+ const from = beforeSecIndex[sec.id];
720
+ const to = afterSecIndex[sec.id];
721
+ if (from !== to) {
722
+ diff.reorderedSections.push({ sectionId: sec.id, from, to });
723
+ }
724
+ }
725
+ }
726
+ for (const sec of afterSections) {
727
+ const idx = beforeSecIndex[sec.id];
728
+ if (idx == null) continue;
729
+ const beforeSec = beforeSections[idx];
730
+ const changes = diffObjectProperties(beforeSec, sec, ["id", "fields"]);
731
+ if (changes.length > 0) {
732
+ diff.modifiedSections.push({
733
+ sectionId: sec.id,
734
+ changes
735
+ });
736
+ }
737
+ }
738
+ for (const afterSec of afterSections) {
739
+ const secId = afterSec.id;
740
+ const beforeIdx = beforeSecIndex[secId];
741
+ if (beforeIdx == null) continue;
742
+ const beforeSec = beforeSections[beforeIdx];
743
+ const beforeFields = beforeSec.fields ?? [];
744
+ const afterFields = afterSec.fields ?? [];
745
+ const nestedDiff = diffRepeatGroupFields(
746
+ beforeFields,
747
+ afterFields,
748
+ secId,
749
+ ""
750
+ );
751
+ diff.addedFields.push(...nestedDiff.addedFields);
752
+ diff.removedFields.push(...nestedDiff.removedFields);
753
+ diff.reorderedFields.push(...nestedDiff.reorderedFields);
754
+ diff.modifiedFields.push(...nestedDiff.modifiedFields);
755
+ diff.nestedFieldDiffs.push(...nestedDiff.nestedFieldDiffs);
756
+ }
757
+ return diff;
758
+ }
759
+
760
+ // src/jsonSchema.ts
761
+ function mapFieldTypeToJSONSchemaCore(field) {
762
+ const common = {
763
+ title: field.label,
764
+ description: field.description,
765
+ default: field.defaultValue ?? void 0,
766
+ "x-frt-placeholder": field.placeholder,
767
+ "x-frt-dataClassification": field.dataClassification,
768
+ "x-frt-visibleIf": field.visibleIf,
769
+ "x-frt-requiredIf": field.requiredIf
770
+ };
771
+ switch (field.type) {
772
+ case "shortText":
773
+ case "longText": {
774
+ const schema = {
775
+ ...common,
776
+ type: "string"
777
+ };
778
+ if (typeof field.minLength === "number") {
779
+ schema.minLength = field.minLength;
780
+ }
781
+ if (typeof field.maxLength === "number") {
782
+ schema.maxLength = field.maxLength;
783
+ }
784
+ return schema;
785
+ }
786
+ case "number": {
787
+ const schema = {
788
+ ...common,
789
+ type: "number"
790
+ };
791
+ if (typeof field.minValue === "number") {
792
+ schema.minimum = field.minValue;
793
+ }
794
+ if (typeof field.maxValue === "number") {
795
+ schema.maximum = field.maxValue;
796
+ }
797
+ return schema;
798
+ }
799
+ case "date": {
800
+ const schema = {
801
+ ...common,
802
+ type: "string",
803
+ format: "date-time"
804
+ };
805
+ return schema;
806
+ }
807
+ case "checkbox": {
808
+ const schema = {
809
+ ...common,
810
+ type: "boolean"
811
+ };
812
+ if (field.required && !field.requiredIf) {
813
+ schema.const = true;
814
+ }
815
+ return schema;
816
+ }
817
+ case "singleSelect": {
818
+ const schema = {
819
+ ...common,
820
+ type: "string"
821
+ };
822
+ if (Array.isArray(field.options) && field.options.length > 0) {
823
+ schema.enum = field.options;
824
+ }
825
+ return schema;
826
+ }
827
+ case "multiSelect": {
828
+ const schema = {
829
+ ...common,
830
+ type: "array",
831
+ items: {
832
+ type: "string"
833
+ }
834
+ };
835
+ const hasOptions = Array.isArray(field.options) && field.options.length > 0;
836
+ if (hasOptions && !field.allowOther) {
837
+ schema.items = {
838
+ type: "string",
839
+ enum: field.options
840
+ };
841
+ }
842
+ if (typeof field.minSelections === "number") {
843
+ schema.minItems = field.minSelections;
844
+ }
845
+ if (typeof field.maxSelections === "number") {
846
+ schema.maxItems = field.maxSelections;
847
+ }
848
+ return schema;
849
+ }
850
+ default: {
851
+ return {
852
+ ...common
853
+ };
854
+ }
855
+ }
856
+ }
857
+ function exportJSONSchema(template) {
858
+ const properties = {};
859
+ const required = [];
860
+ for (const section of template.sections) {
861
+ for (const field of section.fields) {
862
+ properties[field.id] = mapFieldTypeToJSONSchemaCore(field);
863
+ if (field.required === true && !field.requiredIf && !field.visibleIf) {
864
+ required.push(field.id);
865
+ }
866
+ }
867
+ }
868
+ const schema = {
869
+ $schema: "https://json-schema.org/draft/2020-12/schema",
870
+ type: "object",
871
+ properties,
872
+ additionalProperties: false,
873
+ "x-frt-templateTitle": template.title,
874
+ "x-frt-templateDescription": template.description,
875
+ "x-frt-version": template.version
876
+ };
877
+ if (required.length > 0) {
878
+ schema.required = required;
879
+ }
880
+ return schema;
881
+ }
177
882
  export {
178
883
  CORE_FIELD_DEFAULTS,
884
+ Condition,
179
885
  DEFAULT_FIELD_LABEL,
886
+ FieldRegistry,
180
887
  REPORT_TEMPLATE_FIELD_TYPES,
181
888
  REPORT_TEMPLATE_VERSION,
889
+ RepeatGroupFieldSchema,
182
890
  ReportTemplateFieldSchema,
183
891
  ReportTemplateSchemaValidator,
184
892
  ReportTemplateSectionSchema,
893
+ buildBaseFieldSchema,
894
+ buildResponseSchemaWithConditions,
185
895
  createUniqueId,
896
+ diffTemplates,
897
+ evaluateCondition,
898
+ exportJSONSchema,
186
899
  migrateLegacySchema,
187
900
  normalizeReportTemplateSchema,
188
901
  parseReportTemplateSchema,
189
902
  parseReportTemplateSchemaFromString,
190
- serializeReportTemplateSchema
903
+ serializeReportTemplateSchema,
904
+ validateReportResponse,
905
+ validateReportResponseDetailed
191
906
  };