@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/README.md +218 -33
- package/dist/index.d.mts +222 -284
- package/dist/index.d.ts +222 -284
- package/dist/index.js +772 -47
- package/dist/index.mjs +761 -46
- package/package.json +1 -1
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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(
|
|
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(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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: {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
return
|
|
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: "
|
|
280
|
+
id: "section_1",
|
|
142
281
|
title: "",
|
|
143
282
|
description: "",
|
|
144
283
|
fields: []
|
|
145
284
|
}
|
|
146
285
|
];
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
};
|