@frt-platform/report-core 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +210 -134
- package/dist/index.d.ts +210 -134
- package/dist/index.js +542 -367
- package/dist/index.mjs +542 -370
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -10,7 +10,6 @@ var REPORT_TEMPLATE_FIELD_TYPES = [
|
|
|
10
10
|
"singleSelect",
|
|
11
11
|
"multiSelect",
|
|
12
12
|
"repeatGroup"
|
|
13
|
-
// NEW FIELD TYPE
|
|
14
13
|
];
|
|
15
14
|
var Condition = z.lazy(
|
|
16
15
|
() => z.union([
|
|
@@ -37,15 +36,17 @@ var BaseFieldSchema = z.object({
|
|
|
37
36
|
/^[a-z0-9_-]+$/,
|
|
38
37
|
"Use lowercase letters, numbers, underscores, or dashes for field identifiers."
|
|
39
38
|
),
|
|
40
|
-
type: z.enum(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
type: z.enum(
|
|
40
|
+
[
|
|
41
|
+
"shortText",
|
|
42
|
+
"longText",
|
|
43
|
+
"number",
|
|
44
|
+
"date",
|
|
45
|
+
"checkbox",
|
|
46
|
+
"singleSelect",
|
|
47
|
+
"multiSelect"
|
|
48
|
+
]
|
|
49
|
+
),
|
|
49
50
|
label: z.string().min(1, "Field labels cannot be empty.").max(200, "Field labels must be 200 characters or fewer."),
|
|
50
51
|
required: z.boolean().optional(),
|
|
51
52
|
description: z.string().max(400, "Descriptions must be 400 characters or fewer.").optional(),
|
|
@@ -75,7 +76,9 @@ var BaseFieldSchema = z.object({
|
|
|
75
76
|
minSelections: z.number().int().min(0).optional(),
|
|
76
77
|
maxSelections: z.number().int().min(0).optional(),
|
|
77
78
|
// Privacy
|
|
78
|
-
dataClassification: z.enum(["none", "personal", "special"]).optional()
|
|
79
|
+
dataClassification: z.enum(["none", "personal", "special"]).optional(),
|
|
80
|
+
// Computed expression (backend/platform evaluated)
|
|
81
|
+
computed: z.string().max(200, "Computed expressions must be 200 characters or fewer.").optional()
|
|
79
82
|
});
|
|
80
83
|
var RepeatGroupFieldSchema = z.lazy(
|
|
81
84
|
() => z.object({
|
|
@@ -88,24 +91,19 @@ var RepeatGroupFieldSchema = z.lazy(
|
|
|
88
91
|
// Conditional logic for the whole group
|
|
89
92
|
visibleIf: Condition.optional(),
|
|
90
93
|
requiredIf: Condition.optional(),
|
|
91
|
-
//
|
|
94
|
+
// Static + dynamic min/max
|
|
92
95
|
min: z.number().int().min(0).optional(),
|
|
93
96
|
max: z.number().int().min(1).optional(),
|
|
94
97
|
minIf: Condition.optional(),
|
|
95
98
|
maxIf: Condition.optional(),
|
|
96
99
|
// Contents: nested fields (recursive)
|
|
97
|
-
fields: z.array(
|
|
98
|
-
z.lazy(() => ReportTemplateFieldSchema)
|
|
99
|
-
).min(1, "Repeat groups must contain at least one field."),
|
|
100
|
+
fields: z.array(z.lazy(() => ReportTemplateFieldSchema)).min(1, "Repeat groups must contain at least one field."),
|
|
100
101
|
// Pre-populated rows allowed
|
|
101
102
|
defaultValue: z.array(z.record(z.any())).optional()
|
|
102
103
|
})
|
|
103
104
|
);
|
|
104
105
|
var ReportTemplateFieldSchema = z.lazy(
|
|
105
|
-
() => z.union([
|
|
106
|
-
BaseFieldSchema,
|
|
107
|
-
RepeatGroupFieldSchema
|
|
108
|
-
])
|
|
106
|
+
() => z.union([BaseFieldSchema, RepeatGroupFieldSchema])
|
|
109
107
|
);
|
|
110
108
|
var ReportTemplateSectionSchema = z.object({
|
|
111
109
|
id: z.string().min(1, "Section identifiers cannot be empty.").max(60, "Section identifiers must be 60 characters or fewer.").regex(
|
|
@@ -123,62 +121,82 @@ var ReportTemplateSchemaValidator = z.object({
|
|
|
123
121
|
sections: z.array(ReportTemplateSectionSchema).max(25, "Templates can include at most 25 sections.").default([])
|
|
124
122
|
});
|
|
125
123
|
|
|
124
|
+
// src/template/ids.ts
|
|
125
|
+
function createUniqueId(prefix, existing) {
|
|
126
|
+
const used = new Set(existing);
|
|
127
|
+
let attempt = used.size + 1;
|
|
128
|
+
let candidate = `${prefix}-${attempt}`;
|
|
129
|
+
while (used.has(candidate)) {
|
|
130
|
+
attempt += 1;
|
|
131
|
+
candidate = `${prefix}-${attempt}`;
|
|
132
|
+
}
|
|
133
|
+
return candidate;
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
// src/template/migrate.ts
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
"text
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"multi-select": "multiSelect",
|
|
144
|
-
multiselect: "multiSelect",
|
|
145
|
-
checkboxes: "multiSelect"
|
|
146
|
-
};
|
|
137
|
+
function mapLegacyFieldType(type) {
|
|
138
|
+
if (!type) return "shortText";
|
|
139
|
+
const t = type.toLowerCase();
|
|
140
|
+
if (t === "text" || t === "short_text" || t === "input") return "shortText";
|
|
141
|
+
if (t === "textarea" || t === "long_text") return "longText";
|
|
142
|
+
if (t === "number" || t === "numeric") return "number";
|
|
143
|
+
if (t === "date" || t === "datetime" || t === "datetime-local") return "date";
|
|
144
|
+
if (t === "checkbox") return "checkbox";
|
|
145
|
+
if (t === "checkboxes" || t === "multi_select" || t === "multiselect") {
|
|
146
|
+
return "multiSelect";
|
|
147
|
+
}
|
|
148
|
+
if (t === "select" || t === "single_select" || t === "dropdown") {
|
|
149
|
+
return "singleSelect";
|
|
150
|
+
}
|
|
151
|
+
return type;
|
|
152
|
+
}
|
|
147
153
|
function migrateLegacySchema(raw) {
|
|
148
|
-
if (!raw || typeof raw !== "object")
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
154
|
+
if (!raw || typeof raw !== "object") {
|
|
155
|
+
return {
|
|
156
|
+
version: REPORT_TEMPLATE_VERSION,
|
|
157
|
+
sections: []
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const legacy = raw;
|
|
161
|
+
const version = typeof legacy.version === "number" ? legacy.version : REPORT_TEMPLATE_VERSION;
|
|
162
|
+
function mapFields(fields) {
|
|
163
|
+
if (!Array.isArray(fields)) return [];
|
|
164
|
+
let counter = 0;
|
|
165
|
+
return fields.map((field) => {
|
|
166
|
+
counter += 1;
|
|
167
|
+
const fallbackId = `field_${counter}`;
|
|
168
|
+
return {
|
|
169
|
+
...field,
|
|
170
|
+
id: field.id ?? fallbackId,
|
|
171
|
+
type: mapLegacyFieldType(field.type)
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
let sections;
|
|
176
|
+
if (Array.isArray(legacy.sections)) {
|
|
177
|
+
sections = legacy.sections.map((sec, index) => ({
|
|
178
|
+
id: sec.id ?? `section_${index + 1}`,
|
|
179
|
+
title: sec.title ?? legacy.title,
|
|
180
|
+
description: sec.description ?? legacy.description,
|
|
181
|
+
fields: mapFields(sec.fields)
|
|
182
|
+
}));
|
|
183
|
+
} else if (Array.isArray(legacy.fields)) {
|
|
184
|
+
sections = [
|
|
152
185
|
{
|
|
153
186
|
id: "section_1",
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
fields: obj.fields.map((f, i) => ({
|
|
158
|
-
...f,
|
|
159
|
-
id: f.id || `field_${i + 1}`,
|
|
160
|
-
// ⬅ underscore here too
|
|
161
|
-
type: LEGACY_FIELD_TYPE_MAP[f.type?.toLowerCase()] ?? f.type
|
|
162
|
-
}))
|
|
187
|
+
title: legacy.title,
|
|
188
|
+
description: legacy.description,
|
|
189
|
+
fields: mapFields(legacy.fields)
|
|
163
190
|
}
|
|
164
191
|
];
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
if (Array.isArray(obj.sections)) {
|
|
169
|
-
obj.sections = obj.sections.map((sec, i) => ({
|
|
170
|
-
...sec,
|
|
171
|
-
id: sec.id || `section_${i + 1}`,
|
|
172
|
-
// ⬅ underscore
|
|
173
|
-
fields: (sec.fields || []).map((f, idx) => ({
|
|
174
|
-
...f,
|
|
175
|
-
id: f.id || `field_${idx + 1}`,
|
|
176
|
-
// ⬅ underscore
|
|
177
|
-
type: LEGACY_FIELD_TYPE_MAP[f.type?.toLowerCase()] ?? f.type
|
|
178
|
-
}))
|
|
179
|
-
}));
|
|
192
|
+
} else {
|
|
193
|
+
sections = legacy.sections ?? [];
|
|
180
194
|
}
|
|
181
|
-
return
|
|
195
|
+
return {
|
|
196
|
+
...legacy,
|
|
197
|
+
version,
|
|
198
|
+
sections
|
|
199
|
+
};
|
|
182
200
|
}
|
|
183
201
|
|
|
184
202
|
// src/template/normalize.ts
|
|
@@ -290,27 +308,8 @@ function serializeReportTemplateSchema(schema, options = {}) {
|
|
|
290
308
|
}
|
|
291
309
|
|
|
292
310
|
// src/template/diff.ts
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
items.forEach((item, i) => map[item.id] = i);
|
|
296
|
-
return map;
|
|
297
|
-
}
|
|
298
|
-
function diffObjectProperties(before, after, ignoreKeys = []) {
|
|
299
|
-
const changes = [];
|
|
300
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
301
|
-
for (const key of keys) {
|
|
302
|
-
if (ignoreKeys.includes(key)) continue;
|
|
303
|
-
const b = before[key];
|
|
304
|
-
const a = after[key];
|
|
305
|
-
const changed = Array.isArray(b) && Array.isArray(a) ? JSON.stringify(b) !== JSON.stringify(a) : b !== a;
|
|
306
|
-
if (changed) {
|
|
307
|
-
changes.push({ key, before: b, after: a });
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return changes;
|
|
311
|
-
}
|
|
312
|
-
function diffRepeatGroupFields(beforeFields, afterFields, sectionId, groupId) {
|
|
313
|
-
const empty = {
|
|
311
|
+
function createEmptyDiff() {
|
|
312
|
+
return {
|
|
314
313
|
addedSections: [],
|
|
315
314
|
removedSections: [],
|
|
316
315
|
reorderedSections: [],
|
|
@@ -321,137 +320,191 @@ function diffRepeatGroupFields(beforeFields, afterFields, sectionId, groupId) {
|
|
|
321
320
|
modifiedFields: [],
|
|
322
321
|
nestedFieldDiffs: []
|
|
323
322
|
};
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
323
|
+
}
|
|
324
|
+
function shallowDiff(before, after, ignoreKeys) {
|
|
325
|
+
const changes = [];
|
|
326
|
+
const keys = /* @__PURE__ */ new Set([
|
|
327
|
+
...Object.keys(before ?? {}),
|
|
328
|
+
...Object.keys(after ?? {})
|
|
329
|
+
]);
|
|
330
|
+
for (const key of keys) {
|
|
331
|
+
if (ignoreKeys.has(key)) continue;
|
|
332
|
+
const beforeVal = before[key];
|
|
333
|
+
const afterVal = after[key];
|
|
334
|
+
if (beforeVal !== afterVal) {
|
|
335
|
+
changes.push({ key, before: beforeVal, after: afterVal });
|
|
329
336
|
}
|
|
330
337
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
338
|
+
return changes;
|
|
339
|
+
}
|
|
340
|
+
function diffTemplates(before, after) {
|
|
341
|
+
const diff = createEmptyDiff();
|
|
342
|
+
const beforeSections = before.sections ?? [];
|
|
343
|
+
const afterSections = after.sections ?? [];
|
|
344
|
+
const beforeSectionIndex = /* @__PURE__ */ new Map();
|
|
345
|
+
const afterSectionIndex = /* @__PURE__ */ new Map();
|
|
346
|
+
beforeSections.forEach((s, idx) => beforeSectionIndex.set(s.id, idx));
|
|
347
|
+
afterSections.forEach((s, idx) => afterSectionIndex.set(s.id, idx));
|
|
348
|
+
for (const [id, idx] of afterSectionIndex) {
|
|
349
|
+
if (!beforeSectionIndex.has(id)) {
|
|
350
|
+
diff.addedSections.push({ sectionId: id, index: idx });
|
|
334
351
|
}
|
|
335
352
|
}
|
|
336
|
-
for (const
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
const to = afterIndex[f.id];
|
|
340
|
-
if (from !== to) {
|
|
341
|
-
empty.reorderedFields.push({
|
|
342
|
-
sectionId,
|
|
343
|
-
fieldId: f.id,
|
|
344
|
-
from,
|
|
345
|
-
to
|
|
346
|
-
});
|
|
347
|
-
}
|
|
353
|
+
for (const [id, idx] of beforeSectionIndex) {
|
|
354
|
+
if (!afterSectionIndex.has(id)) {
|
|
355
|
+
diff.removedSections.push({ sectionId: id, index: idx });
|
|
348
356
|
}
|
|
349
357
|
}
|
|
350
|
-
for (const
|
|
351
|
-
const
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
sectionId,
|
|
358
|
-
fieldId: f.id,
|
|
359
|
-
before,
|
|
360
|
-
after: f,
|
|
361
|
-
changes
|
|
358
|
+
for (const [id, beforeIdx] of beforeSectionIndex) {
|
|
359
|
+
const afterIdx = afterSectionIndex.get(id);
|
|
360
|
+
if (afterIdx != null && afterIdx !== beforeIdx) {
|
|
361
|
+
diff.reorderedSections.push({
|
|
362
|
+
sectionId: id,
|
|
363
|
+
from: beforeIdx,
|
|
364
|
+
to: afterIdx
|
|
362
365
|
});
|
|
363
366
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
367
|
+
}
|
|
368
|
+
function getSectionById(sections, id) {
|
|
369
|
+
return sections.find((s) => s.id === id);
|
|
370
|
+
}
|
|
371
|
+
for (const [id] of beforeSectionIndex) {
|
|
372
|
+
if (!afterSectionIndex.has(id)) continue;
|
|
373
|
+
const beforeSec = getSectionById(beforeSections, id);
|
|
374
|
+
const afterSec = getSectionById(afterSections, id);
|
|
375
|
+
const changes = shallowDiff(
|
|
376
|
+
beforeSec,
|
|
377
|
+
afterSec,
|
|
378
|
+
/* @__PURE__ */ new Set(["id", "fields"])
|
|
379
|
+
);
|
|
380
|
+
if (changes.length > 0) {
|
|
381
|
+
diff.modifiedSections.push({ sectionId: id, changes });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function diffFieldsForSection(sectionId, beforeFields, afterFields) {
|
|
385
|
+
const beforeIndex = /* @__PURE__ */ new Map();
|
|
386
|
+
const afterIndex = /* @__PURE__ */ new Map();
|
|
387
|
+
beforeFields.forEach((f, idx) => beforeIndex.set(f.id, idx));
|
|
388
|
+
afterFields.forEach((f, idx) => afterIndex.set(f.id, idx));
|
|
389
|
+
for (const [fieldId, idx] of afterIndex) {
|
|
390
|
+
if (!beforeIndex.has(fieldId)) {
|
|
391
|
+
diff.addedFields.push({ sectionId, fieldId, index: idx });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const [fieldId, idx] of beforeIndex) {
|
|
395
|
+
if (!afterIndex.has(fieldId)) {
|
|
396
|
+
diff.removedFields.push({ sectionId, fieldId, index: idx });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const [fieldId, beforeIdx] of beforeIndex) {
|
|
400
|
+
const afterIdx = afterIndex.get(fieldId);
|
|
401
|
+
if (afterIdx != null && afterIdx !== beforeIdx) {
|
|
402
|
+
diff.reorderedFields.push({
|
|
403
|
+
sectionId,
|
|
404
|
+
fieldId,
|
|
405
|
+
from: beforeIdx,
|
|
406
|
+
to: afterIdx
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const ignoreFieldKeys = /* @__PURE__ */ new Set(["id", "fields"]);
|
|
411
|
+
const getField = (fields, id) => fields.find((f) => f.id === id);
|
|
412
|
+
for (const [fieldId] of beforeIndex) {
|
|
413
|
+
if (!afterIndex.has(fieldId)) continue;
|
|
414
|
+
const beforeField = getField(beforeFields, fieldId);
|
|
415
|
+
const afterField = getField(afterFields, fieldId);
|
|
416
|
+
const changes = shallowDiff(
|
|
417
|
+
beforeField,
|
|
418
|
+
afterField,
|
|
419
|
+
ignoreFieldKeys
|
|
372
420
|
);
|
|
373
|
-
if (
|
|
374
|
-
|
|
421
|
+
if (changes.length > 0) {
|
|
422
|
+
diff.modifiedFields.push({
|
|
375
423
|
sectionId,
|
|
376
|
-
|
|
377
|
-
|
|
424
|
+
fieldId,
|
|
425
|
+
changes
|
|
378
426
|
});
|
|
379
427
|
}
|
|
428
|
+
if (beforeField.type === "repeatGroup" && afterField.type === "repeatGroup") {
|
|
429
|
+
const nestedBeforeFields = beforeField.fields ?? [];
|
|
430
|
+
const nestedAfterFields = afterField.fields ?? [];
|
|
431
|
+
const nestedDiff = createEmptyDiff();
|
|
432
|
+
diffFieldsInto(
|
|
433
|
+
sectionId,
|
|
434
|
+
fieldId,
|
|
435
|
+
nestedBeforeFields,
|
|
436
|
+
nestedAfterFields,
|
|
437
|
+
nestedDiff
|
|
438
|
+
);
|
|
439
|
+
if (hasAnyFieldDiff(nestedDiff)) {
|
|
440
|
+
diff.nestedFieldDiffs.push({
|
|
441
|
+
sectionId,
|
|
442
|
+
groupId: fieldId,
|
|
443
|
+
diffs: nestedDiff
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
380
447
|
}
|
|
381
448
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
removedFields: [],
|
|
392
|
-
reorderedFields: [],
|
|
393
|
-
modifiedFields: [],
|
|
394
|
-
nestedFieldDiffs: []
|
|
395
|
-
};
|
|
396
|
-
const beforeSections = before.sections;
|
|
397
|
-
const afterSections = after.sections;
|
|
398
|
-
const beforeSecIndex = indexById(beforeSections);
|
|
399
|
-
const afterSecIndex = indexById(afterSections);
|
|
400
|
-
for (const sec of afterSections) {
|
|
401
|
-
if (!(sec.id in beforeSecIndex)) {
|
|
402
|
-
diff.addedSections.push({
|
|
403
|
-
sectionId: sec.id,
|
|
404
|
-
index: afterSecIndex[sec.id]
|
|
405
|
-
});
|
|
449
|
+
function diffFieldsInto(sectionId, groupId, beforeFields, afterFields, target) {
|
|
450
|
+
const beforeIndex = /* @__PURE__ */ new Map();
|
|
451
|
+
const afterIndex = /* @__PURE__ */ new Map();
|
|
452
|
+
beforeFields.forEach((f, idx) => beforeIndex.set(f.id, idx));
|
|
453
|
+
afterFields.forEach((f, idx) => afterIndex.set(f.id, idx));
|
|
454
|
+
for (const [fieldId, idx] of afterIndex) {
|
|
455
|
+
if (!beforeIndex.has(fieldId)) {
|
|
456
|
+
target.addedFields.push({ sectionId, fieldId, index: idx });
|
|
457
|
+
}
|
|
406
458
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
sectionId: sec.id,
|
|
412
|
-
index: beforeSecIndex[sec.id]
|
|
413
|
-
});
|
|
459
|
+
for (const [fieldId, idx] of beforeIndex) {
|
|
460
|
+
if (!afterIndex.has(fieldId)) {
|
|
461
|
+
target.removedFields.push({ sectionId, fieldId, index: idx });
|
|
462
|
+
}
|
|
414
463
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
464
|
+
for (const [fieldId, beforeIdx] of beforeIndex) {
|
|
465
|
+
const afterIdx = afterIndex.get(fieldId);
|
|
466
|
+
if (afterIdx != null && afterIdx !== beforeIdx) {
|
|
467
|
+
target.reorderedFields.push({
|
|
468
|
+
sectionId,
|
|
469
|
+
fieldId,
|
|
470
|
+
from: beforeIdx,
|
|
471
|
+
to: afterIdx
|
|
472
|
+
});
|
|
422
473
|
}
|
|
423
474
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
475
|
+
const ignoreFieldKeys = /* @__PURE__ */ new Set(["id", "fields"]);
|
|
476
|
+
const getField = (fields, id) => fields.find((f) => f.id === id);
|
|
477
|
+
for (const [fieldId] of beforeIndex) {
|
|
478
|
+
if (!afterIndex.has(fieldId)) continue;
|
|
479
|
+
const beforeField = getField(beforeFields, fieldId);
|
|
480
|
+
const afterField = getField(afterFields, fieldId);
|
|
481
|
+
const changes = shallowDiff(
|
|
482
|
+
beforeField,
|
|
483
|
+
afterField,
|
|
484
|
+
ignoreFieldKeys
|
|
485
|
+
);
|
|
486
|
+
if (changes.length > 0) {
|
|
487
|
+
target.modifiedFields.push({
|
|
488
|
+
sectionId,
|
|
489
|
+
fieldId,
|
|
490
|
+
changes
|
|
491
|
+
});
|
|
492
|
+
}
|
|
435
493
|
}
|
|
436
494
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
495
|
+
function hasAnyFieldDiff(d) {
|
|
496
|
+
return d.addedFields.length > 0 || d.removedFields.length > 0 || d.reorderedFields.length > 0 || d.modifiedFields.length > 0 || d.addedSections.length > 0 || d.removedSections.length > 0 || d.reorderedSections.length > 0 || d.modifiedSections.length > 0 || d.nestedFieldDiffs.length > 0;
|
|
497
|
+
}
|
|
498
|
+
for (const [id, beforeIdx] of beforeSectionIndex) {
|
|
499
|
+
const afterIdx = afterSectionIndex.get(id);
|
|
500
|
+
if (afterIdx == null) continue;
|
|
441
501
|
const beforeSec = beforeSections[beforeIdx];
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
secId,
|
|
448
|
-
""
|
|
502
|
+
const afterSec = afterSections[afterIdx];
|
|
503
|
+
diffFieldsForSection(
|
|
504
|
+
id,
|
|
505
|
+
beforeSec.fields ?? [],
|
|
506
|
+
afterSec.fields ?? []
|
|
449
507
|
);
|
|
450
|
-
diff.addedFields.push(...nestedDiff.addedFields);
|
|
451
|
-
diff.removedFields.push(...nestedDiff.removedFields);
|
|
452
|
-
diff.reorderedFields.push(...nestedDiff.reorderedFields);
|
|
453
|
-
diff.modifiedFields.push(...nestedDiff.modifiedFields);
|
|
454
|
-
diff.nestedFieldDiffs.push(...nestedDiff.nestedFieldDiffs);
|
|
455
508
|
}
|
|
456
509
|
return diff;
|
|
457
510
|
}
|
|
@@ -465,7 +518,9 @@ function mapFieldTypeToJSONSchemaCore(field) {
|
|
|
465
518
|
"x-frt-placeholder": field.placeholder,
|
|
466
519
|
"x-frt-dataClassification": field.dataClassification,
|
|
467
520
|
"x-frt-visibleIf": field.visibleIf,
|
|
468
|
-
"x-frt-requiredIf": field.requiredIf
|
|
521
|
+
"x-frt-requiredIf": field.requiredIf,
|
|
522
|
+
// ⭐ surface computed expression as vendor extension
|
|
523
|
+
"x-frt-computed": field.computed
|
|
469
524
|
};
|
|
470
525
|
switch (field.type) {
|
|
471
526
|
case "shortText":
|
|
@@ -609,58 +664,86 @@ function exportJSONSchema(template) {
|
|
|
609
664
|
return schema;
|
|
610
665
|
}
|
|
611
666
|
|
|
667
|
+
// src/validation/conditions.ts
|
|
668
|
+
function evaluateCondition(condition, response) {
|
|
669
|
+
if (!condition) return true;
|
|
670
|
+
if ("equals" in condition) {
|
|
671
|
+
const entries = Object.entries(condition.equals ?? {});
|
|
672
|
+
if (entries.length === 0) return true;
|
|
673
|
+
return entries.every(([key, expected]) => {
|
|
674
|
+
const value = response[key];
|
|
675
|
+
return value === expected;
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
if ("any" in condition) {
|
|
679
|
+
const children = condition.any ?? [];
|
|
680
|
+
if (children.length === 0) return false;
|
|
681
|
+
return children.some(
|
|
682
|
+
(child) => evaluateCondition(child, response)
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
if ("all" in condition) {
|
|
686
|
+
const children = condition.all ?? [];
|
|
687
|
+
if (children.length === 0) return true;
|
|
688
|
+
return children.every(
|
|
689
|
+
(child) => evaluateCondition(child, response)
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
if ("not" in condition) {
|
|
693
|
+
return !evaluateCondition(condition.not, response);
|
|
694
|
+
}
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/validation/baseSchema.ts
|
|
699
|
+
import { z as z2 } from "zod";
|
|
700
|
+
|
|
612
701
|
// src/fields/defaults.ts
|
|
613
|
-
var DEFAULT_FIELD_LABEL = "Untitled
|
|
702
|
+
var DEFAULT_FIELD_LABEL = "Untitled field";
|
|
614
703
|
var CORE_FIELD_DEFAULTS = {
|
|
615
704
|
shortText: {
|
|
705
|
+
type: "shortText",
|
|
616
706
|
label: DEFAULT_FIELD_LABEL,
|
|
617
707
|
placeholder: "Short answer text"
|
|
618
708
|
},
|
|
619
709
|
longText: {
|
|
710
|
+
type: "longText",
|
|
620
711
|
label: DEFAULT_FIELD_LABEL,
|
|
621
712
|
placeholder: "Long answer text"
|
|
622
713
|
},
|
|
623
714
|
number: {
|
|
715
|
+
type: "number",
|
|
624
716
|
label: DEFAULT_FIELD_LABEL,
|
|
625
717
|
placeholder: "123"
|
|
626
718
|
},
|
|
627
719
|
date: {
|
|
720
|
+
type: "date",
|
|
628
721
|
label: DEFAULT_FIELD_LABEL
|
|
629
722
|
},
|
|
630
723
|
checkbox: {
|
|
631
|
-
|
|
632
|
-
|
|
724
|
+
type: "checkbox",
|
|
725
|
+
label: DEFAULT_FIELD_LABEL
|
|
633
726
|
},
|
|
634
727
|
singleSelect: {
|
|
728
|
+
type: "singleSelect",
|
|
635
729
|
label: DEFAULT_FIELD_LABEL,
|
|
636
730
|
options: ["Option 1", "Option 2", "Option 3"]
|
|
637
731
|
},
|
|
638
732
|
multiSelect: {
|
|
733
|
+
type: "multiSelect",
|
|
639
734
|
label: DEFAULT_FIELD_LABEL,
|
|
640
735
|
options: ["Option 1", "Option 2", "Option 3"],
|
|
641
736
|
allowOther: false
|
|
642
737
|
},
|
|
643
738
|
repeatGroup: {
|
|
644
|
-
|
|
645
|
-
// to configure nested fields + min/max as needed.
|
|
739
|
+
type: "repeatGroup",
|
|
646
740
|
label: DEFAULT_FIELD_LABEL,
|
|
741
|
+
// The actual shape is filled in by the builder;
|
|
742
|
+
// for tests we just need this to be an array.
|
|
647
743
|
fields: []
|
|
648
|
-
// nested fields go here in the UI layer
|
|
649
744
|
}
|
|
650
745
|
};
|
|
651
746
|
|
|
652
|
-
// src/fields/ids.ts
|
|
653
|
-
function createUniqueId(prefix, existing) {
|
|
654
|
-
const used = new Set(existing);
|
|
655
|
-
let attempt = used.size + 1;
|
|
656
|
-
let candidate = `${prefix}-${attempt}`;
|
|
657
|
-
while (used.has(candidate)) {
|
|
658
|
-
attempt++;
|
|
659
|
-
candidate = `${prefix}-${attempt}`;
|
|
660
|
-
}
|
|
661
|
-
return candidate;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
747
|
// src/fields/registry.ts
|
|
665
748
|
var FieldRegistryClass = class {
|
|
666
749
|
constructor() {
|
|
@@ -684,55 +767,191 @@ var FieldRegistryClass = class {
|
|
|
684
767
|
}
|
|
685
768
|
};
|
|
686
769
|
var FieldRegistry = new FieldRegistryClass();
|
|
687
|
-
|
|
688
|
-
"shortText",
|
|
689
|
-
"longText",
|
|
690
|
-
"number",
|
|
691
|
-
"date",
|
|
692
|
-
"checkbox",
|
|
693
|
-
"singleSelect",
|
|
694
|
-
"multiSelect",
|
|
695
|
-
"repeatGroup"
|
|
696
|
-
];
|
|
697
|
-
for (const type of CORE_TYPES) {
|
|
770
|
+
for (const type of REPORT_TEMPLATE_FIELD_TYPES) {
|
|
698
771
|
FieldRegistry.register(type, {
|
|
699
772
|
defaults: CORE_FIELD_DEFAULTS[type]
|
|
700
773
|
// Core types rely on existing validation logic.
|
|
701
|
-
// buildResponseSchema is optional —
|
|
774
|
+
// buildResponseSchema is optional — the validation engine provides
|
|
775
|
+
// built-in handlers for all core types.
|
|
702
776
|
});
|
|
703
777
|
}
|
|
704
778
|
|
|
705
|
-
// src/validation/
|
|
706
|
-
function
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
779
|
+
// src/validation/baseSchema.ts
|
|
780
|
+
function buildBaseFieldSchema(field) {
|
|
781
|
+
const isRequired = Boolean(field.required);
|
|
782
|
+
const registry = FieldRegistry.get(field.type);
|
|
783
|
+
if (registry?.buildResponseSchema) {
|
|
784
|
+
return registry.buildResponseSchema(field);
|
|
711
785
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
786
|
+
switch (field.type) {
|
|
787
|
+
case "shortText":
|
|
788
|
+
case "longText": {
|
|
789
|
+
let schema = z2.string();
|
|
790
|
+
if (typeof field.minLength === "number") {
|
|
791
|
+
schema = schema.min(field.minLength);
|
|
792
|
+
}
|
|
793
|
+
if (typeof field.maxLength === "number") {
|
|
794
|
+
schema = schema.max(field.maxLength);
|
|
795
|
+
}
|
|
796
|
+
return isRequired ? schema : schema.optional();
|
|
797
|
+
}
|
|
798
|
+
case "number": {
|
|
799
|
+
let schema = z2.number();
|
|
800
|
+
if (typeof field.minValue === "number") {
|
|
801
|
+
schema = schema.min(field.minValue);
|
|
802
|
+
}
|
|
803
|
+
if (typeof field.maxValue === "number") {
|
|
804
|
+
schema = schema.max(field.maxValue);
|
|
805
|
+
}
|
|
806
|
+
return isRequired ? schema : schema.optional();
|
|
807
|
+
}
|
|
808
|
+
case "date": {
|
|
809
|
+
const schema = z2.string();
|
|
810
|
+
return isRequired ? schema : schema.optional();
|
|
811
|
+
}
|
|
812
|
+
case "checkbox": {
|
|
813
|
+
if (isRequired) return z2.literal(true);
|
|
814
|
+
return z2.boolean().optional();
|
|
815
|
+
}
|
|
816
|
+
case "singleSelect": {
|
|
817
|
+
let schema = z2.string();
|
|
818
|
+
const opts = field.options;
|
|
819
|
+
const hasOptions = Array.isArray(opts) && opts.length > 0;
|
|
820
|
+
if (hasOptions && !field.allowOther) {
|
|
821
|
+
const allowed = new Set(opts);
|
|
822
|
+
schema = schema.refine(
|
|
823
|
+
(value) => allowed.has(value),
|
|
824
|
+
"Value must be one of the defined options."
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
return isRequired ? schema : schema.optional();
|
|
828
|
+
}
|
|
829
|
+
case "multiSelect": {
|
|
830
|
+
let schema = z2.array(z2.string());
|
|
831
|
+
const opts = field.options;
|
|
832
|
+
const hasOptions = Array.isArray(opts) && opts.length > 0;
|
|
833
|
+
if (hasOptions && !field.allowOther) {
|
|
834
|
+
const allowed = new Set(opts);
|
|
835
|
+
schema = schema.refine(
|
|
836
|
+
(values) => values.every((v) => allowed.has(v)),
|
|
837
|
+
"All values must be among the defined options."
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
if (typeof field.minSelections === "number") {
|
|
841
|
+
schema = schema.min(
|
|
842
|
+
field.minSelections,
|
|
843
|
+
`Select at least ${field.minSelections} option(s).`
|
|
844
|
+
);
|
|
845
|
+
} else if (isRequired) {
|
|
846
|
+
schema = schema.min(
|
|
847
|
+
1,
|
|
848
|
+
"Select at least one option."
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
if (typeof field.maxSelections === "number") {
|
|
852
|
+
schema = schema.max(
|
|
853
|
+
field.maxSelections,
|
|
854
|
+
`Select at most ${field.maxSelections} option(s).`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
return isRequired ? schema : schema.optional();
|
|
858
|
+
}
|
|
859
|
+
case "repeatGroup": {
|
|
860
|
+
return z2.any();
|
|
861
|
+
}
|
|
862
|
+
default:
|
|
863
|
+
return z2.any();
|
|
716
864
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/validation/responses.ts
|
|
868
|
+
import { z as z4 } from "zod";
|
|
869
|
+
|
|
870
|
+
// src/validation/errors.ts
|
|
871
|
+
import { z as z3, ZodError } from "zod";
|
|
872
|
+
function mapZodIssueToFieldErrorCode(issue) {
|
|
873
|
+
switch (issue.code) {
|
|
874
|
+
case z3.ZodIssueCode.invalid_type:
|
|
875
|
+
return "field.invalid_type";
|
|
876
|
+
case z3.ZodIssueCode.too_small:
|
|
877
|
+
return "field.too_small";
|
|
878
|
+
case z3.ZodIssueCode.too_big:
|
|
879
|
+
return "field.too_big";
|
|
880
|
+
case z3.ZodIssueCode.invalid_enum_value:
|
|
881
|
+
case z3.ZodIssueCode.invalid_literal:
|
|
882
|
+
return "field.invalid_option";
|
|
883
|
+
case z3.ZodIssueCode.custom:
|
|
884
|
+
return "field.custom";
|
|
885
|
+
default:
|
|
886
|
+
return "field.custom";
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function buildFieldMetadataLookup(template) {
|
|
890
|
+
const map = /* @__PURE__ */ new Map();
|
|
891
|
+
for (const section of template.sections) {
|
|
892
|
+
for (const field of section.fields) {
|
|
893
|
+
map.set(field.id, {
|
|
894
|
+
sectionId: section.id,
|
|
895
|
+
sectionTitle: section.title,
|
|
896
|
+
label: field.label
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return map;
|
|
901
|
+
}
|
|
902
|
+
function mapZodIssuesToResponseErrors(template, issues) {
|
|
903
|
+
const fieldMeta = buildFieldMetadataLookup(template);
|
|
904
|
+
const errors = [];
|
|
905
|
+
for (const issue of issues) {
|
|
906
|
+
const path = issue.path ?? [];
|
|
907
|
+
const topLevelFieldId = path.find(
|
|
908
|
+
(p) => typeof p === "string"
|
|
720
909
|
);
|
|
910
|
+
const rowIndex = path.find(
|
|
911
|
+
(p) => typeof p === "number"
|
|
912
|
+
);
|
|
913
|
+
const nestedFieldId = path.length >= 3 && typeof path[path.length - 1] === "string" ? path[path.length - 1] : void 0;
|
|
914
|
+
const fieldId = topLevelFieldId ?? "";
|
|
915
|
+
const meta = fieldId ? fieldMeta.get(fieldId) : void 0;
|
|
916
|
+
const code = mapZodIssueToFieldErrorCode(issue);
|
|
917
|
+
const sectionLabel = meta?.sectionTitle ?? meta?.sectionId;
|
|
918
|
+
const fieldLabel = meta?.label ?? fieldId;
|
|
919
|
+
let message;
|
|
920
|
+
if (rowIndex != null && nestedFieldId && meta && sectionLabel) {
|
|
921
|
+
message = `Section "${sectionLabel}" \u2192 "${meta.label}" (row ${rowIndex + 1}, field "${nestedFieldId}"): ${issue.message}`;
|
|
922
|
+
} else if (meta && sectionLabel && fieldLabel) {
|
|
923
|
+
message = `Section "${sectionLabel}" \u2192 Field "${fieldLabel}": ${issue.message}`;
|
|
924
|
+
} else if (fieldLabel) {
|
|
925
|
+
message = `Field "${fieldLabel}": ${issue.message}`;
|
|
926
|
+
} else {
|
|
927
|
+
message = issue.message;
|
|
928
|
+
}
|
|
929
|
+
errors.push({
|
|
930
|
+
fieldId,
|
|
931
|
+
sectionId: meta?.sectionId,
|
|
932
|
+
sectionTitle: meta?.sectionTitle,
|
|
933
|
+
label: meta?.label,
|
|
934
|
+
code,
|
|
935
|
+
message,
|
|
936
|
+
rawIssue: issue
|
|
937
|
+
});
|
|
721
938
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
939
|
+
return errors;
|
|
940
|
+
}
|
|
941
|
+
function explainValidationError(template, error) {
|
|
942
|
+
if (error instanceof ZodError) {
|
|
943
|
+
return mapZodIssuesToResponseErrors(template, error.issues);
|
|
725
944
|
}
|
|
726
|
-
return
|
|
945
|
+
return null;
|
|
727
946
|
}
|
|
728
947
|
|
|
729
948
|
// src/validation/responses.ts
|
|
730
|
-
|
|
731
|
-
z as z2,
|
|
732
|
-
ZodError
|
|
733
|
-
} from "zod";
|
|
734
|
-
function buildBaseFieldSchema(field) {
|
|
949
|
+
function buildBaseFieldSchema2(field) {
|
|
735
950
|
const isRequired = Boolean(field.required);
|
|
951
|
+
const isComputed = Boolean(field.computed);
|
|
952
|
+
if (isComputed) {
|
|
953
|
+
return z4.any().optional();
|
|
954
|
+
}
|
|
736
955
|
const registry = FieldRegistry.get(field.type);
|
|
737
956
|
if (registry?.buildResponseSchema) {
|
|
738
957
|
return registry.buildResponseSchema(field);
|
|
@@ -740,7 +959,7 @@ function buildBaseFieldSchema(field) {
|
|
|
740
959
|
switch (field.type) {
|
|
741
960
|
case "shortText":
|
|
742
961
|
case "longText": {
|
|
743
|
-
let schema =
|
|
962
|
+
let schema = z4.string();
|
|
744
963
|
if (typeof field.minLength === "number") {
|
|
745
964
|
schema = schema.min(field.minLength);
|
|
746
965
|
}
|
|
@@ -750,7 +969,7 @@ function buildBaseFieldSchema(field) {
|
|
|
750
969
|
return isRequired ? schema : schema.optional();
|
|
751
970
|
}
|
|
752
971
|
case "number": {
|
|
753
|
-
let schema =
|
|
972
|
+
let schema = z4.number();
|
|
754
973
|
if (typeof field.minValue === "number") {
|
|
755
974
|
schema = schema.min(field.minValue);
|
|
756
975
|
}
|
|
@@ -760,15 +979,15 @@ function buildBaseFieldSchema(field) {
|
|
|
760
979
|
return isRequired ? schema : schema.optional();
|
|
761
980
|
}
|
|
762
981
|
case "date": {
|
|
763
|
-
let schema =
|
|
982
|
+
let schema = z4.string();
|
|
764
983
|
return isRequired ? schema : schema.optional();
|
|
765
984
|
}
|
|
766
985
|
case "checkbox": {
|
|
767
|
-
if (isRequired) return
|
|
768
|
-
return
|
|
986
|
+
if (isRequired) return z4.literal(true);
|
|
987
|
+
return z4.boolean().optional();
|
|
769
988
|
}
|
|
770
989
|
case "singleSelect": {
|
|
771
|
-
let schema =
|
|
990
|
+
let schema = z4.string();
|
|
772
991
|
if (Array.isArray(field.options) && field.options.length > 0) {
|
|
773
992
|
const allowed = new Set(field.options);
|
|
774
993
|
schema = schema.refine(
|
|
@@ -779,11 +998,13 @@ function buildBaseFieldSchema(field) {
|
|
|
779
998
|
return isRequired ? schema : schema.optional();
|
|
780
999
|
}
|
|
781
1000
|
case "multiSelect": {
|
|
782
|
-
let schema =
|
|
1001
|
+
let schema = z4.array(z4.string());
|
|
783
1002
|
if (Array.isArray(field.options) && field.options.length > 0 && !field.allowOther) {
|
|
784
1003
|
const allowed = new Set(field.options);
|
|
785
1004
|
schema = schema.refine(
|
|
786
|
-
(values) => values.every(
|
|
1005
|
+
(values) => values.every(
|
|
1006
|
+
(value) => allowed.has(value)
|
|
1007
|
+
),
|
|
787
1008
|
"All values must be among the defined options."
|
|
788
1009
|
);
|
|
789
1010
|
}
|
|
@@ -810,11 +1031,11 @@ function buildBaseFieldSchema(field) {
|
|
|
810
1031
|
const nestedFields = Array.isArray(field.fields) ? field.fields : [];
|
|
811
1032
|
const rowShape = {};
|
|
812
1033
|
for (const nested of nestedFields) {
|
|
813
|
-
rowShape[nested.id] =
|
|
1034
|
+
rowShape[nested.id] = buildBaseFieldSchema2(
|
|
814
1035
|
nested
|
|
815
1036
|
);
|
|
816
1037
|
}
|
|
817
|
-
let schema =
|
|
1038
|
+
let schema = z4.array(z4.object(rowShape));
|
|
818
1039
|
if (typeof field.min === "number") {
|
|
819
1040
|
schema = schema.min(
|
|
820
1041
|
field.min,
|
|
@@ -830,12 +1051,11 @@ function buildBaseFieldSchema(field) {
|
|
|
830
1051
|
return isRequired ? schema : schema.optional();
|
|
831
1052
|
}
|
|
832
1053
|
default: {
|
|
833
|
-
return
|
|
1054
|
+
return z4.any();
|
|
834
1055
|
}
|
|
835
1056
|
}
|
|
836
1057
|
}
|
|
837
1058
|
function buildRepeatGroupSchemaWithConditions(field, response) {
|
|
838
|
-
const isRequired = Boolean(field.required);
|
|
839
1059
|
const nestedFields = Array.isArray(field.fields) ? field.fields : [];
|
|
840
1060
|
const rowShape = {};
|
|
841
1061
|
for (const nested of nestedFields) {
|
|
@@ -844,48 +1064,58 @@ function buildRepeatGroupSchemaWithConditions(field, response) {
|
|
|
844
1064
|
response
|
|
845
1065
|
);
|
|
846
1066
|
}
|
|
847
|
-
let schema =
|
|
1067
|
+
let schema = z4.array(z4.object(rowShape));
|
|
848
1068
|
const hasStaticMin = typeof field.min === "number";
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
if (
|
|
853
|
-
const
|
|
1069
|
+
const hasStaticMax = typeof field.max === "number";
|
|
1070
|
+
let effectiveMin = hasStaticMin ? field.min : void 0;
|
|
1071
|
+
let effectiveMax = hasStaticMax ? field.max : void 0;
|
|
1072
|
+
if (field.minIf) {
|
|
1073
|
+
const applyDynamicMin = evaluateCondition(field.minIf, response);
|
|
1074
|
+
if (applyDynamicMin && typeof field.min === "number") {
|
|
1075
|
+
effectiveMin = field.min;
|
|
1076
|
+
} else if (!applyDynamicMin) {
|
|
1077
|
+
effectiveMin = void 0;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (field.maxIf) {
|
|
1081
|
+
const applyDynamicMax = evaluateCondition(field.maxIf, response);
|
|
1082
|
+
if (applyDynamicMax && typeof field.max === "number") {
|
|
1083
|
+
effectiveMax = field.max;
|
|
1084
|
+
} else if (!applyDynamicMax) {
|
|
1085
|
+
effectiveMax = void 0;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (typeof effectiveMin === "number") {
|
|
854
1089
|
schema = schema.min(
|
|
855
|
-
|
|
856
|
-
`Add at least ${
|
|
1090
|
+
effectiveMin,
|
|
1091
|
+
`Add at least ${effectiveMin} item(s).`
|
|
857
1092
|
);
|
|
858
1093
|
}
|
|
859
|
-
|
|
860
|
-
const hasMaxIf = Boolean(field.maxIf);
|
|
861
|
-
const maxIfCondition = field.maxIf;
|
|
862
|
-
const shouldEnforceMax = hasStaticMax && (!hasMaxIf || evaluateCondition(maxIfCondition, response));
|
|
863
|
-
if (shouldEnforceMax) {
|
|
864
|
-
const maxValue = field.max;
|
|
1094
|
+
if (typeof effectiveMax === "number") {
|
|
865
1095
|
schema = schema.max(
|
|
866
|
-
|
|
867
|
-
`Add at most ${
|
|
1096
|
+
effectiveMax,
|
|
1097
|
+
`Add at most ${effectiveMax} item(s).`
|
|
868
1098
|
);
|
|
869
1099
|
}
|
|
870
|
-
return
|
|
1100
|
+
return schema;
|
|
871
1101
|
}
|
|
872
1102
|
function buildConditionalFieldSchema(field, response) {
|
|
873
1103
|
if (field.visibleIf) {
|
|
874
1104
|
const visible = evaluateCondition(field.visibleIf, response);
|
|
875
1105
|
if (!visible) {
|
|
876
|
-
return
|
|
1106
|
+
return z4.any().optional();
|
|
877
1107
|
}
|
|
878
1108
|
}
|
|
879
1109
|
let schema;
|
|
880
1110
|
if (field.type === "repeatGroup") {
|
|
881
1111
|
schema = buildRepeatGroupSchemaWithConditions(field, response);
|
|
882
1112
|
} else {
|
|
883
|
-
schema =
|
|
1113
|
+
schema = buildBaseFieldSchema2(field);
|
|
884
1114
|
}
|
|
885
1115
|
if (field.requiredIf) {
|
|
886
1116
|
const shouldBeRequired = evaluateCondition(field.requiredIf, response);
|
|
887
1117
|
if (shouldBeRequired) {
|
|
888
|
-
if (schema instanceof
|
|
1118
|
+
if (schema instanceof z4.ZodOptional) {
|
|
889
1119
|
schema = schema.unwrap();
|
|
890
1120
|
}
|
|
891
1121
|
}
|
|
@@ -899,18 +1129,12 @@ function buildResponseSchemaWithConditions(template, response) {
|
|
|
899
1129
|
shape[field.id] = buildConditionalFieldSchema(field, response);
|
|
900
1130
|
}
|
|
901
1131
|
}
|
|
902
|
-
return
|
|
1132
|
+
return z4.object(shape);
|
|
903
1133
|
}
|
|
904
1134
|
function buildResponseSchema(template) {
|
|
905
1135
|
return buildResponseSchemaWithConditions(template, {});
|
|
906
1136
|
}
|
|
907
|
-
function
|
|
908
|
-
if (typeof data !== "object" || data === null) {
|
|
909
|
-
throw new Error("Response must be an object");
|
|
910
|
-
}
|
|
911
|
-
const response = data;
|
|
912
|
-
const schema = buildResponseSchemaWithConditions(template, response);
|
|
913
|
-
const parsed = schema.parse(response);
|
|
1137
|
+
function stripInvisibleFields(template, response, parsed) {
|
|
914
1138
|
for (const section of template.sections) {
|
|
915
1139
|
for (const field of section.fields) {
|
|
916
1140
|
if (field.visibleIf) {
|
|
@@ -921,76 +1145,38 @@ function validateReportResponse(template, data) {
|
|
|
921
1145
|
}
|
|
922
1146
|
}
|
|
923
1147
|
}
|
|
924
|
-
return parsed;
|
|
925
|
-
}
|
|
926
|
-
function mapZodIssueToFieldErrorCode(issue) {
|
|
927
|
-
switch (issue.code) {
|
|
928
|
-
case z2.ZodIssueCode.invalid_type:
|
|
929
|
-
return "field.invalid_type";
|
|
930
|
-
case z2.ZodIssueCode.too_small:
|
|
931
|
-
return "field.too_small";
|
|
932
|
-
case z2.ZodIssueCode.too_big:
|
|
933
|
-
return "field.too_big";
|
|
934
|
-
case z2.ZodIssueCode.invalid_enum_value:
|
|
935
|
-
case z2.ZodIssueCode.invalid_literal:
|
|
936
|
-
return "field.invalid_option";
|
|
937
|
-
case z2.ZodIssueCode.custom:
|
|
938
|
-
return "field.custom";
|
|
939
|
-
default:
|
|
940
|
-
return "field.custom";
|
|
941
|
-
}
|
|
942
1148
|
}
|
|
943
|
-
function
|
|
944
|
-
const map = /* @__PURE__ */ new Map();
|
|
1149
|
+
function stripComputedFields(template, parsed) {
|
|
945
1150
|
for (const section of template.sections) {
|
|
946
1151
|
for (const field of section.fields) {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1152
|
+
if (field.computed) {
|
|
1153
|
+
delete parsed[field.id];
|
|
1154
|
+
}
|
|
1155
|
+
if (field.type === "repeatGroup" && Array.isArray(parsed[field.id])) {
|
|
1156
|
+
const rows = parsed[field.id];
|
|
1157
|
+
const nestedFields = Array.isArray(field.fields) ? field.fields : [];
|
|
1158
|
+
for (const row of rows) {
|
|
1159
|
+
if (!row || typeof row !== "object") continue;
|
|
1160
|
+
for (const nested of nestedFields) {
|
|
1161
|
+
if (nested.computed) {
|
|
1162
|
+
delete row[nested.id];
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
952
1167
|
}
|
|
953
1168
|
}
|
|
954
|
-
return map;
|
|
955
1169
|
}
|
|
956
|
-
function
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
for (const issue of issues) {
|
|
960
|
-
const path = issue.path ?? [];
|
|
961
|
-
const topLevelFieldId = path.find(
|
|
962
|
-
(p) => typeof p === "string"
|
|
963
|
-
);
|
|
964
|
-
const rowIndex = path.find(
|
|
965
|
-
(p) => typeof p === "number"
|
|
966
|
-
);
|
|
967
|
-
const nestedFieldId = path.length >= 3 && typeof path[path.length - 1] === "string" ? path[path.length - 1] : void 0;
|
|
968
|
-
const fieldId = topLevelFieldId ?? "";
|
|
969
|
-
const meta = fieldId ? fieldMeta.get(fieldId) : void 0;
|
|
970
|
-
const code = mapZodIssueToFieldErrorCode(issue);
|
|
971
|
-
const sectionLabel = meta?.sectionTitle ?? meta?.sectionId;
|
|
972
|
-
const fieldLabel = meta?.label ?? fieldId;
|
|
973
|
-
let message;
|
|
974
|
-
if (rowIndex != null && nestedFieldId && meta && sectionLabel) {
|
|
975
|
-
message = `Section "${sectionLabel}" \u2192 "${meta.label}" (row ${rowIndex + 1}, field "${nestedFieldId}"): ${issue.message}`;
|
|
976
|
-
} else if (meta && sectionLabel && fieldLabel) {
|
|
977
|
-
message = `Section "${sectionLabel}" \u2192 Field "${fieldLabel}": ${issue.message}`;
|
|
978
|
-
} else if (fieldLabel) {
|
|
979
|
-
message = `Field "${fieldLabel}": ${issue.message}`;
|
|
980
|
-
} else {
|
|
981
|
-
message = issue.message;
|
|
982
|
-
}
|
|
983
|
-
errors.push({
|
|
984
|
-
fieldId,
|
|
985
|
-
sectionId: meta?.sectionId,
|
|
986
|
-
sectionTitle: meta?.sectionTitle,
|
|
987
|
-
label: meta?.label,
|
|
988
|
-
code,
|
|
989
|
-
message,
|
|
990
|
-
rawIssue: issue
|
|
991
|
-
});
|
|
1170
|
+
function validateReportResponse(template, data) {
|
|
1171
|
+
if (typeof data !== "object" || data === null) {
|
|
1172
|
+
throw new Error("Response must be an object");
|
|
992
1173
|
}
|
|
993
|
-
|
|
1174
|
+
const response = data;
|
|
1175
|
+
const schema = buildResponseSchemaWithConditions(template, response);
|
|
1176
|
+
const parsed = schema.parse(response);
|
|
1177
|
+
stripInvisibleFields(template, response, parsed);
|
|
1178
|
+
stripComputedFields(template, parsed);
|
|
1179
|
+
return parsed;
|
|
994
1180
|
}
|
|
995
1181
|
function validateReportResponseDetailed(template, data) {
|
|
996
1182
|
if (typeof data !== "object" || data === null) {
|
|
@@ -1010,27 +1196,13 @@ function validateReportResponseDetailed(template, data) {
|
|
|
1010
1196
|
const result = schema.safeParse(response);
|
|
1011
1197
|
if (result.success) {
|
|
1012
1198
|
const parsed = { ...result.data };
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
if (field.visibleIf) {
|
|
1016
|
-
const visible = evaluateCondition(field.visibleIf, response);
|
|
1017
|
-
if (!visible) {
|
|
1018
|
-
delete parsed[field.id];
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1199
|
+
stripInvisibleFields(template, response, parsed);
|
|
1200
|
+
stripComputedFields(template, parsed);
|
|
1023
1201
|
return { success: true, value: parsed };
|
|
1024
1202
|
}
|
|
1025
1203
|
const errors = mapZodIssuesToResponseErrors(template, result.error.issues);
|
|
1026
1204
|
return { success: false, errors };
|
|
1027
1205
|
}
|
|
1028
|
-
function explainValidationError(template, error) {
|
|
1029
|
-
if (error instanceof ZodError) {
|
|
1030
|
-
return mapZodIssuesToResponseErrors(template, error.issues);
|
|
1031
|
-
}
|
|
1032
|
-
return null;
|
|
1033
|
-
}
|
|
1034
1206
|
export {
|
|
1035
1207
|
CORE_FIELD_DEFAULTS,
|
|
1036
1208
|
Condition,
|