@frt-platform/report-core 1.1.0 → 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 +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.mjs
CHANGED
|
@@ -8,14 +8,44 @@ 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
|
|
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({
|
|
14
36
|
id: z.string().min(1, "Field identifiers cannot be empty.").max(60, "Field identifiers must be 60 characters or fewer.").regex(
|
|
15
37
|
/^[a-z0-9_-]+$/,
|
|
16
38
|
"Use lowercase letters, numbers, underscores, or dashes for field identifiers."
|
|
17
39
|
),
|
|
18
|
-
type: z.enum(
|
|
40
|
+
type: z.enum([
|
|
41
|
+
"shortText",
|
|
42
|
+
"longText",
|
|
43
|
+
"number",
|
|
44
|
+
"date",
|
|
45
|
+
"checkbox",
|
|
46
|
+
"singleSelect",
|
|
47
|
+
"multiSelect"
|
|
48
|
+
]),
|
|
19
49
|
label: z.string().min(1, "Field labels cannot be empty.").max(200, "Field labels must be 200 characters or fewer."),
|
|
20
50
|
required: z.boolean().optional(),
|
|
21
51
|
description: z.string().max(400, "Descriptions must be 400 characters or fewer.").optional(),
|
|
@@ -24,16 +54,59 @@ var ReportTemplateFieldSchema = z.object({
|
|
|
24
54
|
z.string().min(1, "Options cannot be empty.").max(120, "Options must be 120 characters or fewer.")
|
|
25
55
|
).optional(),
|
|
26
56
|
allowOther: z.boolean().optional(),
|
|
27
|
-
defaultValue: z.union([
|
|
57
|
+
defaultValue: z.union([
|
|
58
|
+
z.string(),
|
|
59
|
+
z.number(),
|
|
60
|
+
z.boolean(),
|
|
61
|
+
z.array(z.string()),
|
|
62
|
+
z.null()
|
|
63
|
+
]).optional(),
|
|
64
|
+
// Conditional logic
|
|
65
|
+
visibleIf: Condition.optional(),
|
|
66
|
+
requiredIf: Condition.optional(),
|
|
67
|
+
// Text constraints
|
|
28
68
|
minLength: z.number().int().min(0).optional(),
|
|
29
69
|
maxLength: z.number().int().min(0).optional(),
|
|
70
|
+
// Number constraints
|
|
30
71
|
minValue: z.number().optional(),
|
|
31
72
|
maxValue: z.number().optional(),
|
|
32
73
|
step: z.number().nonnegative().optional(),
|
|
74
|
+
// Multi-select constraints
|
|
33
75
|
minSelections: z.number().int().min(0).optional(),
|
|
34
76
|
maxSelections: z.number().int().min(0).optional(),
|
|
77
|
+
// Privacy
|
|
35
78
|
dataClassification: z.enum(["none", "personal", "special"]).optional()
|
|
36
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
|
+
);
|
|
37
110
|
var ReportTemplateSectionSchema = z.object({
|
|
38
111
|
id: z.string().min(1, "Section identifiers cannot be empty.").max(60, "Section identifiers must be 60 characters or fewer.").regex(
|
|
39
112
|
/^[a-z0-9_-]+$/,
|
|
@@ -80,6 +153,13 @@ var CORE_FIELD_DEFAULTS = {
|
|
|
80
153
|
label: DEFAULT_FIELD_LABEL,
|
|
81
154
|
options: ["Option 1", "Option 2", "Option 3"],
|
|
82
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
|
|
83
163
|
}
|
|
84
164
|
};
|
|
85
165
|
|
|
@@ -150,46 +230,77 @@ function migrateLegacySchema(raw) {
|
|
|
150
230
|
}
|
|
151
231
|
|
|
152
232
|
// src/normalize.ts
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
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;
|
|
157
276
|
}
|
|
158
277
|
function normalizeReportTemplateSchema(schema) {
|
|
159
278
|
const sections = schema.sections.length > 0 ? schema.sections : [
|
|
160
279
|
{
|
|
161
|
-
id: "
|
|
280
|
+
id: "section_1",
|
|
162
281
|
title: "",
|
|
163
282
|
description: "",
|
|
164
283
|
fields: []
|
|
165
284
|
}
|
|
166
285
|
];
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const safeId = seen.has(trimmed) ? `field-${idx + 1}` : trimmed;
|
|
173
|
-
seen.add(safeId);
|
|
174
|
-
return {
|
|
175
|
-
...field,
|
|
176
|
-
id: safeId
|
|
177
|
-
};
|
|
178
|
-
});
|
|
179
|
-
return {
|
|
180
|
-
...sec,
|
|
181
|
-
id: safeSectionId,
|
|
182
|
-
fields
|
|
183
|
-
};
|
|
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;
|
|
184
291
|
});
|
|
185
292
|
return {
|
|
186
293
|
...schema,
|
|
187
294
|
sections: normalizedSections
|
|
188
295
|
};
|
|
189
296
|
}
|
|
297
|
+
function parseReportTemplateSchema(raw) {
|
|
298
|
+
const migrated = migrateLegacySchema(raw);
|
|
299
|
+
const parsed = ReportTemplateSchemaValidator.parse(migrated);
|
|
300
|
+
return normalizeReportTemplateSchema(parsed);
|
|
301
|
+
}
|
|
190
302
|
function parseReportTemplateSchemaFromString(raw) {
|
|
191
|
-
|
|
192
|
-
return parseReportTemplateSchema(obj);
|
|
303
|
+
return parseReportTemplateSchema(JSON.parse(raw));
|
|
193
304
|
}
|
|
194
305
|
function serializeReportTemplateSchema(schema) {
|
|
195
306
|
return JSON.stringify(schema, null, 2);
|
|
@@ -197,8 +308,78 @@ function serializeReportTemplateSchema(schema) {
|
|
|
197
308
|
|
|
198
309
|
// src/responses.ts
|
|
199
310
|
import { z as z2 } from "zod";
|
|
200
|
-
|
|
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) {
|
|
201
378
|
const isRequired = Boolean(field.required);
|
|
379
|
+
const registry = FieldRegistry.get(field.type);
|
|
380
|
+
if (registry?.buildResponseSchema) {
|
|
381
|
+
return registry.buildResponseSchema(field);
|
|
382
|
+
}
|
|
202
383
|
switch (field.type) {
|
|
203
384
|
case "shortText":
|
|
204
385
|
case "longText": {
|
|
@@ -226,9 +407,7 @@ function buildFieldResponseSchema(field) {
|
|
|
226
407
|
return isRequired ? schema : schema.optional();
|
|
227
408
|
}
|
|
228
409
|
case "checkbox": {
|
|
229
|
-
if (isRequired)
|
|
230
|
-
return z2.literal(true);
|
|
231
|
-
}
|
|
410
|
+
if (isRequired) return z2.literal(true);
|
|
232
411
|
return z2.boolean().optional();
|
|
233
412
|
}
|
|
234
413
|
case "singleSelect": {
|
|
@@ -271,39 +450,457 @@ function buildFieldResponseSchema(field) {
|
|
|
271
450
|
return isRequired ? schema : schema.optional();
|
|
272
451
|
}
|
|
273
452
|
default: {
|
|
274
|
-
const _exhaustive = field.type;
|
|
275
453
|
return z2.any();
|
|
276
454
|
}
|
|
277
455
|
}
|
|
278
456
|
}
|
|
279
|
-
function
|
|
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) {
|
|
280
476
|
const shape = {};
|
|
281
477
|
for (const section of template.sections) {
|
|
282
478
|
for (const field of section.fields) {
|
|
283
|
-
shape[field.id] =
|
|
479
|
+
shape[field.id] = buildConditionalFieldSchema(field, response);
|
|
284
480
|
}
|
|
285
481
|
}
|
|
286
482
|
return z2.object(shape);
|
|
287
483
|
}
|
|
288
484
|
function validateReportResponse(template, data) {
|
|
289
|
-
|
|
290
|
-
|
|
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;
|
|
291
881
|
}
|
|
292
882
|
export {
|
|
293
883
|
CORE_FIELD_DEFAULTS,
|
|
884
|
+
Condition,
|
|
294
885
|
DEFAULT_FIELD_LABEL,
|
|
886
|
+
FieldRegistry,
|
|
295
887
|
REPORT_TEMPLATE_FIELD_TYPES,
|
|
296
888
|
REPORT_TEMPLATE_VERSION,
|
|
889
|
+
RepeatGroupFieldSchema,
|
|
297
890
|
ReportTemplateFieldSchema,
|
|
298
891
|
ReportTemplateSchemaValidator,
|
|
299
892
|
ReportTemplateSectionSchema,
|
|
300
|
-
|
|
301
|
-
|
|
893
|
+
buildBaseFieldSchema,
|
|
894
|
+
buildResponseSchemaWithConditions,
|
|
302
895
|
createUniqueId,
|
|
896
|
+
diffTemplates,
|
|
897
|
+
evaluateCondition,
|
|
898
|
+
exportJSONSchema,
|
|
303
899
|
migrateLegacySchema,
|
|
304
900
|
normalizeReportTemplateSchema,
|
|
305
901
|
parseReportTemplateSchema,
|
|
306
902
|
parseReportTemplateSchemaFromString,
|
|
307
903
|
serializeReportTemplateSchema,
|
|
308
|
-
validateReportResponse
|
|
904
|
+
validateReportResponse,
|
|
905
|
+
validateReportResponseDetailed
|
|
309
906
|
};
|