@frt-platform/report-core 1.1.0 → 1.2.1
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/README.md +382 -53
- package/dist/index.d.mts +211 -298
- package/dist/index.d.ts +211 -298
- package/dist/index.js +647 -43
- package/dist/index.mjs +637 -40
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21,21 +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,
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
buildBaseFieldSchema: () => buildBaseFieldSchema,
|
|
34
|
+
buildResponseSchemaWithConditions: () => buildResponseSchemaWithConditions,
|
|
32
35
|
createUniqueId: () => createUniqueId,
|
|
36
|
+
diffTemplates: () => diffTemplates,
|
|
37
|
+
evaluateCondition: () => evaluateCondition,
|
|
38
|
+
exportJSONSchema: () => exportJSONSchema,
|
|
33
39
|
migrateLegacySchema: () => migrateLegacySchema,
|
|
34
40
|
normalizeReportTemplateSchema: () => normalizeReportTemplateSchema,
|
|
35
41
|
parseReportTemplateSchema: () => parseReportTemplateSchema,
|
|
36
42
|
parseReportTemplateSchemaFromString: () => parseReportTemplateSchemaFromString,
|
|
37
43
|
serializeReportTemplateSchema: () => serializeReportTemplateSchema,
|
|
38
|
-
validateReportResponse: () => validateReportResponse
|
|
44
|
+
validateReportResponse: () => validateReportResponse,
|
|
45
|
+
validateReportResponseDetailed: () => validateReportResponseDetailed
|
|
39
46
|
});
|
|
40
47
|
module.exports = __toCommonJS(index_exports);
|
|
41
48
|
|
|
@@ -49,14 +56,44 @@ var REPORT_TEMPLATE_FIELD_TYPES = [
|
|
|
49
56
|
"date",
|
|
50
57
|
"checkbox",
|
|
51
58
|
"singleSelect",
|
|
52
|
-
"multiSelect"
|
|
59
|
+
"multiSelect",
|
|
60
|
+
"repeatGroup"
|
|
61
|
+
// NEW FIELD TYPE
|
|
53
62
|
];
|
|
54
|
-
var
|
|
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({
|
|
55
84
|
id: import_zod.z.string().min(1, "Field identifiers cannot be empty.").max(60, "Field identifiers must be 60 characters or fewer.").regex(
|
|
56
85
|
/^[a-z0-9_-]+$/,
|
|
57
86
|
"Use lowercase letters, numbers, underscores, or dashes for field identifiers."
|
|
58
87
|
),
|
|
59
|
-
type: import_zod.z.enum(
|
|
88
|
+
type: import_zod.z.enum([
|
|
89
|
+
"shortText",
|
|
90
|
+
"longText",
|
|
91
|
+
"number",
|
|
92
|
+
"date",
|
|
93
|
+
"checkbox",
|
|
94
|
+
"singleSelect",
|
|
95
|
+
"multiSelect"
|
|
96
|
+
]),
|
|
60
97
|
label: import_zod.z.string().min(1, "Field labels cannot be empty.").max(200, "Field labels must be 200 characters or fewer."),
|
|
61
98
|
required: import_zod.z.boolean().optional(),
|
|
62
99
|
description: import_zod.z.string().max(400, "Descriptions must be 400 characters or fewer.").optional(),
|
|
@@ -65,16 +102,59 @@ var ReportTemplateFieldSchema = import_zod.z.object({
|
|
|
65
102
|
import_zod.z.string().min(1, "Options cannot be empty.").max(120, "Options must be 120 characters or fewer.")
|
|
66
103
|
).optional(),
|
|
67
104
|
allowOther: import_zod.z.boolean().optional(),
|
|
68
|
-
defaultValue: import_zod.z.union([
|
|
105
|
+
defaultValue: import_zod.z.union([
|
|
106
|
+
import_zod.z.string(),
|
|
107
|
+
import_zod.z.number(),
|
|
108
|
+
import_zod.z.boolean(),
|
|
109
|
+
import_zod.z.array(import_zod.z.string()),
|
|
110
|
+
import_zod.z.null()
|
|
111
|
+
]).optional(),
|
|
112
|
+
// Conditional logic
|
|
113
|
+
visibleIf: Condition.optional(),
|
|
114
|
+
requiredIf: Condition.optional(),
|
|
115
|
+
// Text constraints
|
|
69
116
|
minLength: import_zod.z.number().int().min(0).optional(),
|
|
70
117
|
maxLength: import_zod.z.number().int().min(0).optional(),
|
|
118
|
+
// Number constraints
|
|
71
119
|
minValue: import_zod.z.number().optional(),
|
|
72
120
|
maxValue: import_zod.z.number().optional(),
|
|
73
121
|
step: import_zod.z.number().nonnegative().optional(),
|
|
122
|
+
// Multi-select constraints
|
|
74
123
|
minSelections: import_zod.z.number().int().min(0).optional(),
|
|
75
124
|
maxSelections: import_zod.z.number().int().min(0).optional(),
|
|
125
|
+
// Privacy
|
|
76
126
|
dataClassification: import_zod.z.enum(["none", "personal", "special"]).optional()
|
|
77
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
|
+
);
|
|
78
158
|
var ReportTemplateSectionSchema = import_zod.z.object({
|
|
79
159
|
id: import_zod.z.string().min(1, "Section identifiers cannot be empty.").max(60, "Section identifiers must be 60 characters or fewer.").regex(
|
|
80
160
|
/^[a-z0-9_-]+$/,
|
|
@@ -121,6 +201,13 @@ var CORE_FIELD_DEFAULTS = {
|
|
|
121
201
|
label: DEFAULT_FIELD_LABEL,
|
|
122
202
|
options: ["Option 1", "Option 2", "Option 3"],
|
|
123
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
|
|
124
211
|
}
|
|
125
212
|
};
|
|
126
213
|
|
|
@@ -191,46 +278,77 @@ function migrateLegacySchema(raw) {
|
|
|
191
278
|
}
|
|
192
279
|
|
|
193
280
|
// src/normalize.ts
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
return
|
|
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;
|
|
198
324
|
}
|
|
199
325
|
function normalizeReportTemplateSchema(schema) {
|
|
200
326
|
const sections = schema.sections.length > 0 ? schema.sections : [
|
|
201
327
|
{
|
|
202
|
-
id: "
|
|
328
|
+
id: "section_1",
|
|
203
329
|
title: "",
|
|
204
330
|
description: "",
|
|
205
331
|
fields: []
|
|
206
332
|
}
|
|
207
333
|
];
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const safeId = seen.has(trimmed) ? `field-${idx + 1}` : trimmed;
|
|
214
|
-
seen.add(safeId);
|
|
215
|
-
return {
|
|
216
|
-
...field,
|
|
217
|
-
id: safeId
|
|
218
|
-
};
|
|
219
|
-
});
|
|
220
|
-
return {
|
|
221
|
-
...sec,
|
|
222
|
-
id: safeSectionId,
|
|
223
|
-
fields
|
|
224
|
-
};
|
|
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;
|
|
225
339
|
});
|
|
226
340
|
return {
|
|
227
341
|
...schema,
|
|
228
342
|
sections: normalizedSections
|
|
229
343
|
};
|
|
230
344
|
}
|
|
345
|
+
function parseReportTemplateSchema(raw) {
|
|
346
|
+
const migrated = migrateLegacySchema(raw);
|
|
347
|
+
const parsed = ReportTemplateSchemaValidator.parse(migrated);
|
|
348
|
+
return normalizeReportTemplateSchema(parsed);
|
|
349
|
+
}
|
|
231
350
|
function parseReportTemplateSchemaFromString(raw) {
|
|
232
|
-
|
|
233
|
-
return parseReportTemplateSchema(obj);
|
|
351
|
+
return parseReportTemplateSchema(JSON.parse(raw));
|
|
234
352
|
}
|
|
235
353
|
function serializeReportTemplateSchema(schema) {
|
|
236
354
|
return JSON.stringify(schema, null, 2);
|
|
@@ -238,8 +356,78 @@ function serializeReportTemplateSchema(schema) {
|
|
|
238
356
|
|
|
239
357
|
// src/responses.ts
|
|
240
358
|
var import_zod2 = require("zod");
|
|
241
|
-
|
|
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) {
|
|
242
426
|
const isRequired = Boolean(field.required);
|
|
427
|
+
const registry = FieldRegistry.get(field.type);
|
|
428
|
+
if (registry?.buildResponseSchema) {
|
|
429
|
+
return registry.buildResponseSchema(field);
|
|
430
|
+
}
|
|
243
431
|
switch (field.type) {
|
|
244
432
|
case "shortText":
|
|
245
433
|
case "longText": {
|
|
@@ -267,9 +455,7 @@ function buildFieldResponseSchema(field) {
|
|
|
267
455
|
return isRequired ? schema : schema.optional();
|
|
268
456
|
}
|
|
269
457
|
case "checkbox": {
|
|
270
|
-
if (isRequired)
|
|
271
|
-
return import_zod2.z.literal(true);
|
|
272
|
-
}
|
|
458
|
+
if (isRequired) return import_zod2.z.literal(true);
|
|
273
459
|
return import_zod2.z.boolean().optional();
|
|
274
460
|
}
|
|
275
461
|
case "singleSelect": {
|
|
@@ -312,40 +498,458 @@ function buildFieldResponseSchema(field) {
|
|
|
312
498
|
return isRequired ? schema : schema.optional();
|
|
313
499
|
}
|
|
314
500
|
default: {
|
|
315
|
-
const _exhaustive = field.type;
|
|
316
501
|
return import_zod2.z.any();
|
|
317
502
|
}
|
|
318
503
|
}
|
|
319
504
|
}
|
|
320
|
-
function
|
|
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) {
|
|
321
524
|
const shape = {};
|
|
322
525
|
for (const section of template.sections) {
|
|
323
526
|
for (const field of section.fields) {
|
|
324
|
-
shape[field.id] =
|
|
527
|
+
shape[field.id] = buildConditionalFieldSchema(field, response);
|
|
325
528
|
}
|
|
326
529
|
}
|
|
327
530
|
return import_zod2.z.object(shape);
|
|
328
531
|
}
|
|
329
532
|
function validateReportResponse(template, data) {
|
|
330
|
-
|
|
331
|
-
|
|
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;
|
|
332
929
|
}
|
|
333
930
|
// Annotate the CommonJS export names for ESM import in node:
|
|
334
931
|
0 && (module.exports = {
|
|
335
932
|
CORE_FIELD_DEFAULTS,
|
|
933
|
+
Condition,
|
|
336
934
|
DEFAULT_FIELD_LABEL,
|
|
935
|
+
FieldRegistry,
|
|
337
936
|
REPORT_TEMPLATE_FIELD_TYPES,
|
|
338
937
|
REPORT_TEMPLATE_VERSION,
|
|
938
|
+
RepeatGroupFieldSchema,
|
|
339
939
|
ReportTemplateFieldSchema,
|
|
340
940
|
ReportTemplateSchemaValidator,
|
|
341
941
|
ReportTemplateSectionSchema,
|
|
342
|
-
|
|
343
|
-
|
|
942
|
+
buildBaseFieldSchema,
|
|
943
|
+
buildResponseSchemaWithConditions,
|
|
344
944
|
createUniqueId,
|
|
945
|
+
diffTemplates,
|
|
946
|
+
evaluateCondition,
|
|
947
|
+
exportJSONSchema,
|
|
345
948
|
migrateLegacySchema,
|
|
346
949
|
normalizeReportTemplateSchema,
|
|
347
950
|
parseReportTemplateSchema,
|
|
348
951
|
parseReportTemplateSchemaFromString,
|
|
349
952
|
serializeReportTemplateSchema,
|
|
350
|
-
validateReportResponse
|
|
953
|
+
validateReportResponse,
|
|
954
|
+
validateReportResponseDetailed
|
|
351
955
|
});
|