@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.js
CHANGED
|
@@ -60,7 +60,6 @@ var REPORT_TEMPLATE_FIELD_TYPES = [
|
|
|
60
60
|
"singleSelect",
|
|
61
61
|
"multiSelect",
|
|
62
62
|
"repeatGroup"
|
|
63
|
-
// NEW FIELD TYPE
|
|
64
63
|
];
|
|
65
64
|
var Condition = import_zod.z.lazy(
|
|
66
65
|
() => import_zod.z.union([
|
|
@@ -87,15 +86,17 @@ var BaseFieldSchema = import_zod.z.object({
|
|
|
87
86
|
/^[a-z0-9_-]+$/,
|
|
88
87
|
"Use lowercase letters, numbers, underscores, or dashes for field identifiers."
|
|
89
88
|
),
|
|
90
|
-
type: import_zod.z.enum(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
type: import_zod.z.enum(
|
|
90
|
+
[
|
|
91
|
+
"shortText",
|
|
92
|
+
"longText",
|
|
93
|
+
"number",
|
|
94
|
+
"date",
|
|
95
|
+
"checkbox",
|
|
96
|
+
"singleSelect",
|
|
97
|
+
"multiSelect"
|
|
98
|
+
]
|
|
99
|
+
),
|
|
99
100
|
label: import_zod.z.string().min(1, "Field labels cannot be empty.").max(200, "Field labels must be 200 characters or fewer."),
|
|
100
101
|
required: import_zod.z.boolean().optional(),
|
|
101
102
|
description: import_zod.z.string().max(400, "Descriptions must be 400 characters or fewer.").optional(),
|
|
@@ -125,7 +126,9 @@ var BaseFieldSchema = import_zod.z.object({
|
|
|
125
126
|
minSelections: import_zod.z.number().int().min(0).optional(),
|
|
126
127
|
maxSelections: import_zod.z.number().int().min(0).optional(),
|
|
127
128
|
// Privacy
|
|
128
|
-
dataClassification: import_zod.z.enum(["none", "personal", "special"]).optional()
|
|
129
|
+
dataClassification: import_zod.z.enum(["none", "personal", "special"]).optional(),
|
|
130
|
+
// Computed expression (backend/platform evaluated)
|
|
131
|
+
computed: import_zod.z.string().max(200, "Computed expressions must be 200 characters or fewer.").optional()
|
|
129
132
|
});
|
|
130
133
|
var RepeatGroupFieldSchema = import_zod.z.lazy(
|
|
131
134
|
() => import_zod.z.object({
|
|
@@ -138,24 +141,19 @@ var RepeatGroupFieldSchema = import_zod.z.lazy(
|
|
|
138
141
|
// Conditional logic for the whole group
|
|
139
142
|
visibleIf: Condition.optional(),
|
|
140
143
|
requiredIf: Condition.optional(),
|
|
141
|
-
//
|
|
144
|
+
// Static + dynamic min/max
|
|
142
145
|
min: import_zod.z.number().int().min(0).optional(),
|
|
143
146
|
max: import_zod.z.number().int().min(1).optional(),
|
|
144
147
|
minIf: Condition.optional(),
|
|
145
148
|
maxIf: Condition.optional(),
|
|
146
149
|
// Contents: nested fields (recursive)
|
|
147
|
-
fields: import_zod.z.array(
|
|
148
|
-
import_zod.z.lazy(() => ReportTemplateFieldSchema)
|
|
149
|
-
).min(1, "Repeat groups must contain at least one field."),
|
|
150
|
+
fields: import_zod.z.array(import_zod.z.lazy(() => ReportTemplateFieldSchema)).min(1, "Repeat groups must contain at least one field."),
|
|
150
151
|
// Pre-populated rows allowed
|
|
151
152
|
defaultValue: import_zod.z.array(import_zod.z.record(import_zod.z.any())).optional()
|
|
152
153
|
})
|
|
153
154
|
);
|
|
154
155
|
var ReportTemplateFieldSchema = import_zod.z.lazy(
|
|
155
|
-
() => import_zod.z.union([
|
|
156
|
-
BaseFieldSchema,
|
|
157
|
-
RepeatGroupFieldSchema
|
|
158
|
-
])
|
|
156
|
+
() => import_zod.z.union([BaseFieldSchema, RepeatGroupFieldSchema])
|
|
159
157
|
);
|
|
160
158
|
var ReportTemplateSectionSchema = import_zod.z.object({
|
|
161
159
|
id: import_zod.z.string().min(1, "Section identifiers cannot be empty.").max(60, "Section identifiers must be 60 characters or fewer.").regex(
|
|
@@ -173,62 +171,82 @@ var ReportTemplateSchemaValidator = import_zod.z.object({
|
|
|
173
171
|
sections: import_zod.z.array(ReportTemplateSectionSchema).max(25, "Templates can include at most 25 sections.").default([])
|
|
174
172
|
});
|
|
175
173
|
|
|
174
|
+
// src/template/ids.ts
|
|
175
|
+
function createUniqueId(prefix, existing) {
|
|
176
|
+
const used = new Set(existing);
|
|
177
|
+
let attempt = used.size + 1;
|
|
178
|
+
let candidate = `${prefix}-${attempt}`;
|
|
179
|
+
while (used.has(candidate)) {
|
|
180
|
+
attempt += 1;
|
|
181
|
+
candidate = `${prefix}-${attempt}`;
|
|
182
|
+
}
|
|
183
|
+
return candidate;
|
|
184
|
+
}
|
|
185
|
+
|
|
176
186
|
// src/template/migrate.ts
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"text
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"multi-select": "multiSelect",
|
|
194
|
-
multiselect: "multiSelect",
|
|
195
|
-
checkboxes: "multiSelect"
|
|
196
|
-
};
|
|
187
|
+
function mapLegacyFieldType(type) {
|
|
188
|
+
if (!type) return "shortText";
|
|
189
|
+
const t = type.toLowerCase();
|
|
190
|
+
if (t === "text" || t === "short_text" || t === "input") return "shortText";
|
|
191
|
+
if (t === "textarea" || t === "long_text") return "longText";
|
|
192
|
+
if (t === "number" || t === "numeric") return "number";
|
|
193
|
+
if (t === "date" || t === "datetime" || t === "datetime-local") return "date";
|
|
194
|
+
if (t === "checkbox") return "checkbox";
|
|
195
|
+
if (t === "checkboxes" || t === "multi_select" || t === "multiselect") {
|
|
196
|
+
return "multiSelect";
|
|
197
|
+
}
|
|
198
|
+
if (t === "select" || t === "single_select" || t === "dropdown") {
|
|
199
|
+
return "singleSelect";
|
|
200
|
+
}
|
|
201
|
+
return type;
|
|
202
|
+
}
|
|
197
203
|
function migrateLegacySchema(raw) {
|
|
198
|
-
if (!raw || typeof raw !== "object")
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
204
|
+
if (!raw || typeof raw !== "object") {
|
|
205
|
+
return {
|
|
206
|
+
version: REPORT_TEMPLATE_VERSION,
|
|
207
|
+
sections: []
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const legacy = raw;
|
|
211
|
+
const version = typeof legacy.version === "number" ? legacy.version : REPORT_TEMPLATE_VERSION;
|
|
212
|
+
function mapFields(fields) {
|
|
213
|
+
if (!Array.isArray(fields)) return [];
|
|
214
|
+
let counter = 0;
|
|
215
|
+
return fields.map((field) => {
|
|
216
|
+
counter += 1;
|
|
217
|
+
const fallbackId = `field_${counter}`;
|
|
218
|
+
return {
|
|
219
|
+
...field,
|
|
220
|
+
id: field.id ?? fallbackId,
|
|
221
|
+
type: mapLegacyFieldType(field.type)
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
let sections;
|
|
226
|
+
if (Array.isArray(legacy.sections)) {
|
|
227
|
+
sections = legacy.sections.map((sec, index) => ({
|
|
228
|
+
id: sec.id ?? `section_${index + 1}`,
|
|
229
|
+
title: sec.title ?? legacy.title,
|
|
230
|
+
description: sec.description ?? legacy.description,
|
|
231
|
+
fields: mapFields(sec.fields)
|
|
232
|
+
}));
|
|
233
|
+
} else if (Array.isArray(legacy.fields)) {
|
|
234
|
+
sections = [
|
|
202
235
|
{
|
|
203
236
|
id: "section_1",
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
fields: obj.fields.map((f, i) => ({
|
|
208
|
-
...f,
|
|
209
|
-
id: f.id || `field_${i + 1}`,
|
|
210
|
-
// ⬅ underscore here too
|
|
211
|
-
type: LEGACY_FIELD_TYPE_MAP[f.type?.toLowerCase()] ?? f.type
|
|
212
|
-
}))
|
|
237
|
+
title: legacy.title,
|
|
238
|
+
description: legacy.description,
|
|
239
|
+
fields: mapFields(legacy.fields)
|
|
213
240
|
}
|
|
214
241
|
];
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
if (Array.isArray(obj.sections)) {
|
|
219
|
-
obj.sections = obj.sections.map((sec, i) => ({
|
|
220
|
-
...sec,
|
|
221
|
-
id: sec.id || `section_${i + 1}`,
|
|
222
|
-
// ⬅ underscore
|
|
223
|
-
fields: (sec.fields || []).map((f, idx) => ({
|
|
224
|
-
...f,
|
|
225
|
-
id: f.id || `field_${idx + 1}`,
|
|
226
|
-
// ⬅ underscore
|
|
227
|
-
type: LEGACY_FIELD_TYPE_MAP[f.type?.toLowerCase()] ?? f.type
|
|
228
|
-
}))
|
|
229
|
-
}));
|
|
242
|
+
} else {
|
|
243
|
+
sections = legacy.sections ?? [];
|
|
230
244
|
}
|
|
231
|
-
return
|
|
245
|
+
return {
|
|
246
|
+
...legacy,
|
|
247
|
+
version,
|
|
248
|
+
sections
|
|
249
|
+
};
|
|
232
250
|
}
|
|
233
251
|
|
|
234
252
|
// src/template/normalize.ts
|
|
@@ -340,27 +358,8 @@ function serializeReportTemplateSchema(schema, options = {}) {
|
|
|
340
358
|
}
|
|
341
359
|
|
|
342
360
|
// src/template/diff.ts
|
|
343
|
-
function
|
|
344
|
-
|
|
345
|
-
items.forEach((item, i) => map[item.id] = i);
|
|
346
|
-
return map;
|
|
347
|
-
}
|
|
348
|
-
function diffObjectProperties(before, after, ignoreKeys = []) {
|
|
349
|
-
const changes = [];
|
|
350
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
351
|
-
for (const key of keys) {
|
|
352
|
-
if (ignoreKeys.includes(key)) continue;
|
|
353
|
-
const b = before[key];
|
|
354
|
-
const a = after[key];
|
|
355
|
-
const changed = Array.isArray(b) && Array.isArray(a) ? JSON.stringify(b) !== JSON.stringify(a) : b !== a;
|
|
356
|
-
if (changed) {
|
|
357
|
-
changes.push({ key, before: b, after: a });
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
return changes;
|
|
361
|
-
}
|
|
362
|
-
function diffRepeatGroupFields(beforeFields, afterFields, sectionId, groupId) {
|
|
363
|
-
const empty = {
|
|
361
|
+
function createEmptyDiff() {
|
|
362
|
+
return {
|
|
364
363
|
addedSections: [],
|
|
365
364
|
removedSections: [],
|
|
366
365
|
reorderedSections: [],
|
|
@@ -371,137 +370,191 @@ function diffRepeatGroupFields(beforeFields, afterFields, sectionId, groupId) {
|
|
|
371
370
|
modifiedFields: [],
|
|
372
371
|
nestedFieldDiffs: []
|
|
373
372
|
};
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
373
|
+
}
|
|
374
|
+
function shallowDiff(before, after, ignoreKeys) {
|
|
375
|
+
const changes = [];
|
|
376
|
+
const keys = /* @__PURE__ */ new Set([
|
|
377
|
+
...Object.keys(before ?? {}),
|
|
378
|
+
...Object.keys(after ?? {})
|
|
379
|
+
]);
|
|
380
|
+
for (const key of keys) {
|
|
381
|
+
if (ignoreKeys.has(key)) continue;
|
|
382
|
+
const beforeVal = before[key];
|
|
383
|
+
const afterVal = after[key];
|
|
384
|
+
if (beforeVal !== afterVal) {
|
|
385
|
+
changes.push({ key, before: beforeVal, after: afterVal });
|
|
379
386
|
}
|
|
380
387
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
388
|
+
return changes;
|
|
389
|
+
}
|
|
390
|
+
function diffTemplates(before, after) {
|
|
391
|
+
const diff = createEmptyDiff();
|
|
392
|
+
const beforeSections = before.sections ?? [];
|
|
393
|
+
const afterSections = after.sections ?? [];
|
|
394
|
+
const beforeSectionIndex = /* @__PURE__ */ new Map();
|
|
395
|
+
const afterSectionIndex = /* @__PURE__ */ new Map();
|
|
396
|
+
beforeSections.forEach((s, idx) => beforeSectionIndex.set(s.id, idx));
|
|
397
|
+
afterSections.forEach((s, idx) => afterSectionIndex.set(s.id, idx));
|
|
398
|
+
for (const [id, idx] of afterSectionIndex) {
|
|
399
|
+
if (!beforeSectionIndex.has(id)) {
|
|
400
|
+
diff.addedSections.push({ sectionId: id, index: idx });
|
|
384
401
|
}
|
|
385
402
|
}
|
|
386
|
-
for (const
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
const to = afterIndex[f.id];
|
|
390
|
-
if (from !== to) {
|
|
391
|
-
empty.reorderedFields.push({
|
|
392
|
-
sectionId,
|
|
393
|
-
fieldId: f.id,
|
|
394
|
-
from,
|
|
395
|
-
to
|
|
396
|
-
});
|
|
397
|
-
}
|
|
403
|
+
for (const [id, idx] of beforeSectionIndex) {
|
|
404
|
+
if (!afterSectionIndex.has(id)) {
|
|
405
|
+
diff.removedSections.push({ sectionId: id, index: idx });
|
|
398
406
|
}
|
|
399
407
|
}
|
|
400
|
-
for (const
|
|
401
|
-
const
|
|
402
|
-
if (
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
sectionId,
|
|
408
|
-
fieldId: f.id,
|
|
409
|
-
before,
|
|
410
|
-
after: f,
|
|
411
|
-
changes
|
|
408
|
+
for (const [id, beforeIdx] of beforeSectionIndex) {
|
|
409
|
+
const afterIdx = afterSectionIndex.get(id);
|
|
410
|
+
if (afterIdx != null && afterIdx !== beforeIdx) {
|
|
411
|
+
diff.reorderedSections.push({
|
|
412
|
+
sectionId: id,
|
|
413
|
+
from: beforeIdx,
|
|
414
|
+
to: afterIdx
|
|
412
415
|
});
|
|
413
416
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
417
|
+
}
|
|
418
|
+
function getSectionById(sections, id) {
|
|
419
|
+
return sections.find((s) => s.id === id);
|
|
420
|
+
}
|
|
421
|
+
for (const [id] of beforeSectionIndex) {
|
|
422
|
+
if (!afterSectionIndex.has(id)) continue;
|
|
423
|
+
const beforeSec = getSectionById(beforeSections, id);
|
|
424
|
+
const afterSec = getSectionById(afterSections, id);
|
|
425
|
+
const changes = shallowDiff(
|
|
426
|
+
beforeSec,
|
|
427
|
+
afterSec,
|
|
428
|
+
/* @__PURE__ */ new Set(["id", "fields"])
|
|
429
|
+
);
|
|
430
|
+
if (changes.length > 0) {
|
|
431
|
+
diff.modifiedSections.push({ sectionId: id, changes });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function diffFieldsForSection(sectionId, beforeFields, afterFields) {
|
|
435
|
+
const beforeIndex = /* @__PURE__ */ new Map();
|
|
436
|
+
const afterIndex = /* @__PURE__ */ new Map();
|
|
437
|
+
beforeFields.forEach((f, idx) => beforeIndex.set(f.id, idx));
|
|
438
|
+
afterFields.forEach((f, idx) => afterIndex.set(f.id, idx));
|
|
439
|
+
for (const [fieldId, idx] of afterIndex) {
|
|
440
|
+
if (!beforeIndex.has(fieldId)) {
|
|
441
|
+
diff.addedFields.push({ sectionId, fieldId, index: idx });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (const [fieldId, idx] of beforeIndex) {
|
|
445
|
+
if (!afterIndex.has(fieldId)) {
|
|
446
|
+
diff.removedFields.push({ sectionId, fieldId, index: idx });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
for (const [fieldId, beforeIdx] of beforeIndex) {
|
|
450
|
+
const afterIdx = afterIndex.get(fieldId);
|
|
451
|
+
if (afterIdx != null && afterIdx !== beforeIdx) {
|
|
452
|
+
diff.reorderedFields.push({
|
|
453
|
+
sectionId,
|
|
454
|
+
fieldId,
|
|
455
|
+
from: beforeIdx,
|
|
456
|
+
to: afterIdx
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const ignoreFieldKeys = /* @__PURE__ */ new Set(["id", "fields"]);
|
|
461
|
+
const getField = (fields, id) => fields.find((f) => f.id === id);
|
|
462
|
+
for (const [fieldId] of beforeIndex) {
|
|
463
|
+
if (!afterIndex.has(fieldId)) continue;
|
|
464
|
+
const beforeField = getField(beforeFields, fieldId);
|
|
465
|
+
const afterField = getField(afterFields, fieldId);
|
|
466
|
+
const changes = shallowDiff(
|
|
467
|
+
beforeField,
|
|
468
|
+
afterField,
|
|
469
|
+
ignoreFieldKeys
|
|
422
470
|
);
|
|
423
|
-
if (
|
|
424
|
-
|
|
471
|
+
if (changes.length > 0) {
|
|
472
|
+
diff.modifiedFields.push({
|
|
425
473
|
sectionId,
|
|
426
|
-
|
|
427
|
-
|
|
474
|
+
fieldId,
|
|
475
|
+
changes
|
|
428
476
|
});
|
|
429
477
|
}
|
|
478
|
+
if (beforeField.type === "repeatGroup" && afterField.type === "repeatGroup") {
|
|
479
|
+
const nestedBeforeFields = beforeField.fields ?? [];
|
|
480
|
+
const nestedAfterFields = afterField.fields ?? [];
|
|
481
|
+
const nestedDiff = createEmptyDiff();
|
|
482
|
+
diffFieldsInto(
|
|
483
|
+
sectionId,
|
|
484
|
+
fieldId,
|
|
485
|
+
nestedBeforeFields,
|
|
486
|
+
nestedAfterFields,
|
|
487
|
+
nestedDiff
|
|
488
|
+
);
|
|
489
|
+
if (hasAnyFieldDiff(nestedDiff)) {
|
|
490
|
+
diff.nestedFieldDiffs.push({
|
|
491
|
+
sectionId,
|
|
492
|
+
groupId: fieldId,
|
|
493
|
+
diffs: nestedDiff
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
430
497
|
}
|
|
431
498
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
removedFields: [],
|
|
442
|
-
reorderedFields: [],
|
|
443
|
-
modifiedFields: [],
|
|
444
|
-
nestedFieldDiffs: []
|
|
445
|
-
};
|
|
446
|
-
const beforeSections = before.sections;
|
|
447
|
-
const afterSections = after.sections;
|
|
448
|
-
const beforeSecIndex = indexById(beforeSections);
|
|
449
|
-
const afterSecIndex = indexById(afterSections);
|
|
450
|
-
for (const sec of afterSections) {
|
|
451
|
-
if (!(sec.id in beforeSecIndex)) {
|
|
452
|
-
diff.addedSections.push({
|
|
453
|
-
sectionId: sec.id,
|
|
454
|
-
index: afterSecIndex[sec.id]
|
|
455
|
-
});
|
|
499
|
+
function diffFieldsInto(sectionId, groupId, beforeFields, afterFields, target) {
|
|
500
|
+
const beforeIndex = /* @__PURE__ */ new Map();
|
|
501
|
+
const afterIndex = /* @__PURE__ */ new Map();
|
|
502
|
+
beforeFields.forEach((f, idx) => beforeIndex.set(f.id, idx));
|
|
503
|
+
afterFields.forEach((f, idx) => afterIndex.set(f.id, idx));
|
|
504
|
+
for (const [fieldId, idx] of afterIndex) {
|
|
505
|
+
if (!beforeIndex.has(fieldId)) {
|
|
506
|
+
target.addedFields.push({ sectionId, fieldId, index: idx });
|
|
507
|
+
}
|
|
456
508
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
sectionId: sec.id,
|
|
462
|
-
index: beforeSecIndex[sec.id]
|
|
463
|
-
});
|
|
509
|
+
for (const [fieldId, idx] of beforeIndex) {
|
|
510
|
+
if (!afterIndex.has(fieldId)) {
|
|
511
|
+
target.removedFields.push({ sectionId, fieldId, index: idx });
|
|
512
|
+
}
|
|
464
513
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
514
|
+
for (const [fieldId, beforeIdx] of beforeIndex) {
|
|
515
|
+
const afterIdx = afterIndex.get(fieldId);
|
|
516
|
+
if (afterIdx != null && afterIdx !== beforeIdx) {
|
|
517
|
+
target.reorderedFields.push({
|
|
518
|
+
sectionId,
|
|
519
|
+
fieldId,
|
|
520
|
+
from: beforeIdx,
|
|
521
|
+
to: afterIdx
|
|
522
|
+
});
|
|
472
523
|
}
|
|
473
524
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
525
|
+
const ignoreFieldKeys = /* @__PURE__ */ new Set(["id", "fields"]);
|
|
526
|
+
const getField = (fields, id) => fields.find((f) => f.id === id);
|
|
527
|
+
for (const [fieldId] of beforeIndex) {
|
|
528
|
+
if (!afterIndex.has(fieldId)) continue;
|
|
529
|
+
const beforeField = getField(beforeFields, fieldId);
|
|
530
|
+
const afterField = getField(afterFields, fieldId);
|
|
531
|
+
const changes = shallowDiff(
|
|
532
|
+
beforeField,
|
|
533
|
+
afterField,
|
|
534
|
+
ignoreFieldKeys
|
|
535
|
+
);
|
|
536
|
+
if (changes.length > 0) {
|
|
537
|
+
target.modifiedFields.push({
|
|
538
|
+
sectionId,
|
|
539
|
+
fieldId,
|
|
540
|
+
changes
|
|
541
|
+
});
|
|
542
|
+
}
|
|
485
543
|
}
|
|
486
544
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
545
|
+
function hasAnyFieldDiff(d) {
|
|
546
|
+
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;
|
|
547
|
+
}
|
|
548
|
+
for (const [id, beforeIdx] of beforeSectionIndex) {
|
|
549
|
+
const afterIdx = afterSectionIndex.get(id);
|
|
550
|
+
if (afterIdx == null) continue;
|
|
491
551
|
const beforeSec = beforeSections[beforeIdx];
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
secId,
|
|
498
|
-
""
|
|
552
|
+
const afterSec = afterSections[afterIdx];
|
|
553
|
+
diffFieldsForSection(
|
|
554
|
+
id,
|
|
555
|
+
beforeSec.fields ?? [],
|
|
556
|
+
afterSec.fields ?? []
|
|
499
557
|
);
|
|
500
|
-
diff.addedFields.push(...nestedDiff.addedFields);
|
|
501
|
-
diff.removedFields.push(...nestedDiff.removedFields);
|
|
502
|
-
diff.reorderedFields.push(...nestedDiff.reorderedFields);
|
|
503
|
-
diff.modifiedFields.push(...nestedDiff.modifiedFields);
|
|
504
|
-
diff.nestedFieldDiffs.push(...nestedDiff.nestedFieldDiffs);
|
|
505
558
|
}
|
|
506
559
|
return diff;
|
|
507
560
|
}
|
|
@@ -515,7 +568,9 @@ function mapFieldTypeToJSONSchemaCore(field) {
|
|
|
515
568
|
"x-frt-placeholder": field.placeholder,
|
|
516
569
|
"x-frt-dataClassification": field.dataClassification,
|
|
517
570
|
"x-frt-visibleIf": field.visibleIf,
|
|
518
|
-
"x-frt-requiredIf": field.requiredIf
|
|
571
|
+
"x-frt-requiredIf": field.requiredIf,
|
|
572
|
+
// ⭐ surface computed expression as vendor extension
|
|
573
|
+
"x-frt-computed": field.computed
|
|
519
574
|
};
|
|
520
575
|
switch (field.type) {
|
|
521
576
|
case "shortText":
|
|
@@ -659,58 +714,86 @@ function exportJSONSchema(template) {
|
|
|
659
714
|
return schema;
|
|
660
715
|
}
|
|
661
716
|
|
|
717
|
+
// src/validation/conditions.ts
|
|
718
|
+
function evaluateCondition(condition, response) {
|
|
719
|
+
if (!condition) return true;
|
|
720
|
+
if ("equals" in condition) {
|
|
721
|
+
const entries = Object.entries(condition.equals ?? {});
|
|
722
|
+
if (entries.length === 0) return true;
|
|
723
|
+
return entries.every(([key, expected]) => {
|
|
724
|
+
const value = response[key];
|
|
725
|
+
return value === expected;
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
if ("any" in condition) {
|
|
729
|
+
const children = condition.any ?? [];
|
|
730
|
+
if (children.length === 0) return false;
|
|
731
|
+
return children.some(
|
|
732
|
+
(child) => evaluateCondition(child, response)
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
if ("all" in condition) {
|
|
736
|
+
const children = condition.all ?? [];
|
|
737
|
+
if (children.length === 0) return true;
|
|
738
|
+
return children.every(
|
|
739
|
+
(child) => evaluateCondition(child, response)
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
if ("not" in condition) {
|
|
743
|
+
return !evaluateCondition(condition.not, response);
|
|
744
|
+
}
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/validation/baseSchema.ts
|
|
749
|
+
var import_zod2 = require("zod");
|
|
750
|
+
|
|
662
751
|
// src/fields/defaults.ts
|
|
663
|
-
var DEFAULT_FIELD_LABEL = "Untitled
|
|
752
|
+
var DEFAULT_FIELD_LABEL = "Untitled field";
|
|
664
753
|
var CORE_FIELD_DEFAULTS = {
|
|
665
754
|
shortText: {
|
|
755
|
+
type: "shortText",
|
|
666
756
|
label: DEFAULT_FIELD_LABEL,
|
|
667
757
|
placeholder: "Short answer text"
|
|
668
758
|
},
|
|
669
759
|
longText: {
|
|
760
|
+
type: "longText",
|
|
670
761
|
label: DEFAULT_FIELD_LABEL,
|
|
671
762
|
placeholder: "Long answer text"
|
|
672
763
|
},
|
|
673
764
|
number: {
|
|
765
|
+
type: "number",
|
|
674
766
|
label: DEFAULT_FIELD_LABEL,
|
|
675
767
|
placeholder: "123"
|
|
676
768
|
},
|
|
677
769
|
date: {
|
|
770
|
+
type: "date",
|
|
678
771
|
label: DEFAULT_FIELD_LABEL
|
|
679
772
|
},
|
|
680
773
|
checkbox: {
|
|
681
|
-
|
|
682
|
-
|
|
774
|
+
type: "checkbox",
|
|
775
|
+
label: DEFAULT_FIELD_LABEL
|
|
683
776
|
},
|
|
684
777
|
singleSelect: {
|
|
778
|
+
type: "singleSelect",
|
|
685
779
|
label: DEFAULT_FIELD_LABEL,
|
|
686
780
|
options: ["Option 1", "Option 2", "Option 3"]
|
|
687
781
|
},
|
|
688
782
|
multiSelect: {
|
|
783
|
+
type: "multiSelect",
|
|
689
784
|
label: DEFAULT_FIELD_LABEL,
|
|
690
785
|
options: ["Option 1", "Option 2", "Option 3"],
|
|
691
786
|
allowOther: false
|
|
692
787
|
},
|
|
693
788
|
repeatGroup: {
|
|
694
|
-
|
|
695
|
-
// to configure nested fields + min/max as needed.
|
|
789
|
+
type: "repeatGroup",
|
|
696
790
|
label: DEFAULT_FIELD_LABEL,
|
|
791
|
+
// The actual shape is filled in by the builder;
|
|
792
|
+
// for tests we just need this to be an array.
|
|
697
793
|
fields: []
|
|
698
|
-
// nested fields go here in the UI layer
|
|
699
794
|
}
|
|
700
795
|
};
|
|
701
796
|
|
|
702
|
-
// src/fields/ids.ts
|
|
703
|
-
function createUniqueId(prefix, existing) {
|
|
704
|
-
const used = new Set(existing);
|
|
705
|
-
let attempt = used.size + 1;
|
|
706
|
-
let candidate = `${prefix}-${attempt}`;
|
|
707
|
-
while (used.has(candidate)) {
|
|
708
|
-
attempt++;
|
|
709
|
-
candidate = `${prefix}-${attempt}`;
|
|
710
|
-
}
|
|
711
|
-
return candidate;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
797
|
// src/fields/registry.ts
|
|
715
798
|
var FieldRegistryClass = class {
|
|
716
799
|
constructor() {
|
|
@@ -734,52 +817,191 @@ var FieldRegistryClass = class {
|
|
|
734
817
|
}
|
|
735
818
|
};
|
|
736
819
|
var FieldRegistry = new FieldRegistryClass();
|
|
737
|
-
|
|
738
|
-
"shortText",
|
|
739
|
-
"longText",
|
|
740
|
-
"number",
|
|
741
|
-
"date",
|
|
742
|
-
"checkbox",
|
|
743
|
-
"singleSelect",
|
|
744
|
-
"multiSelect",
|
|
745
|
-
"repeatGroup"
|
|
746
|
-
];
|
|
747
|
-
for (const type of CORE_TYPES) {
|
|
820
|
+
for (const type of REPORT_TEMPLATE_FIELD_TYPES) {
|
|
748
821
|
FieldRegistry.register(type, {
|
|
749
822
|
defaults: CORE_FIELD_DEFAULTS[type]
|
|
750
823
|
// Core types rely on existing validation logic.
|
|
751
|
-
// buildResponseSchema is optional —
|
|
824
|
+
// buildResponseSchema is optional — the validation engine provides
|
|
825
|
+
// built-in handlers for all core types.
|
|
752
826
|
});
|
|
753
827
|
}
|
|
754
828
|
|
|
755
|
-
// src/validation/
|
|
756
|
-
function
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
829
|
+
// src/validation/baseSchema.ts
|
|
830
|
+
function buildBaseFieldSchema(field) {
|
|
831
|
+
const isRequired = Boolean(field.required);
|
|
832
|
+
const registry = FieldRegistry.get(field.type);
|
|
833
|
+
if (registry?.buildResponseSchema) {
|
|
834
|
+
return registry.buildResponseSchema(field);
|
|
761
835
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
836
|
+
switch (field.type) {
|
|
837
|
+
case "shortText":
|
|
838
|
+
case "longText": {
|
|
839
|
+
let schema = import_zod2.z.string();
|
|
840
|
+
if (typeof field.minLength === "number") {
|
|
841
|
+
schema = schema.min(field.minLength);
|
|
842
|
+
}
|
|
843
|
+
if (typeof field.maxLength === "number") {
|
|
844
|
+
schema = schema.max(field.maxLength);
|
|
845
|
+
}
|
|
846
|
+
return isRequired ? schema : schema.optional();
|
|
847
|
+
}
|
|
848
|
+
case "number": {
|
|
849
|
+
let schema = import_zod2.z.number();
|
|
850
|
+
if (typeof field.minValue === "number") {
|
|
851
|
+
schema = schema.min(field.minValue);
|
|
852
|
+
}
|
|
853
|
+
if (typeof field.maxValue === "number") {
|
|
854
|
+
schema = schema.max(field.maxValue);
|
|
855
|
+
}
|
|
856
|
+
return isRequired ? schema : schema.optional();
|
|
857
|
+
}
|
|
858
|
+
case "date": {
|
|
859
|
+
const schema = import_zod2.z.string();
|
|
860
|
+
return isRequired ? schema : schema.optional();
|
|
861
|
+
}
|
|
862
|
+
case "checkbox": {
|
|
863
|
+
if (isRequired) return import_zod2.z.literal(true);
|
|
864
|
+
return import_zod2.z.boolean().optional();
|
|
865
|
+
}
|
|
866
|
+
case "singleSelect": {
|
|
867
|
+
let schema = import_zod2.z.string();
|
|
868
|
+
const opts = field.options;
|
|
869
|
+
const hasOptions = Array.isArray(opts) && opts.length > 0;
|
|
870
|
+
if (hasOptions && !field.allowOther) {
|
|
871
|
+
const allowed = new Set(opts);
|
|
872
|
+
schema = schema.refine(
|
|
873
|
+
(value) => allowed.has(value),
|
|
874
|
+
"Value must be one of the defined options."
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
return isRequired ? schema : schema.optional();
|
|
878
|
+
}
|
|
879
|
+
case "multiSelect": {
|
|
880
|
+
let schema = import_zod2.z.array(import_zod2.z.string());
|
|
881
|
+
const opts = field.options;
|
|
882
|
+
const hasOptions = Array.isArray(opts) && opts.length > 0;
|
|
883
|
+
if (hasOptions && !field.allowOther) {
|
|
884
|
+
const allowed = new Set(opts);
|
|
885
|
+
schema = schema.refine(
|
|
886
|
+
(values) => values.every((v) => allowed.has(v)),
|
|
887
|
+
"All values must be among the defined options."
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
if (typeof field.minSelections === "number") {
|
|
891
|
+
schema = schema.min(
|
|
892
|
+
field.minSelections,
|
|
893
|
+
`Select at least ${field.minSelections} option(s).`
|
|
894
|
+
);
|
|
895
|
+
} else if (isRequired) {
|
|
896
|
+
schema = schema.min(
|
|
897
|
+
1,
|
|
898
|
+
"Select at least one option."
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
if (typeof field.maxSelections === "number") {
|
|
902
|
+
schema = schema.max(
|
|
903
|
+
field.maxSelections,
|
|
904
|
+
`Select at most ${field.maxSelections} option(s).`
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
return isRequired ? schema : schema.optional();
|
|
908
|
+
}
|
|
909
|
+
case "repeatGroup": {
|
|
910
|
+
return import_zod2.z.any();
|
|
911
|
+
}
|
|
912
|
+
default:
|
|
913
|
+
return import_zod2.z.any();
|
|
766
914
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// src/validation/responses.ts
|
|
918
|
+
var import_zod4 = require("zod");
|
|
919
|
+
|
|
920
|
+
// src/validation/errors.ts
|
|
921
|
+
var import_zod3 = require("zod");
|
|
922
|
+
function mapZodIssueToFieldErrorCode(issue) {
|
|
923
|
+
switch (issue.code) {
|
|
924
|
+
case import_zod3.z.ZodIssueCode.invalid_type:
|
|
925
|
+
return "field.invalid_type";
|
|
926
|
+
case import_zod3.z.ZodIssueCode.too_small:
|
|
927
|
+
return "field.too_small";
|
|
928
|
+
case import_zod3.z.ZodIssueCode.too_big:
|
|
929
|
+
return "field.too_big";
|
|
930
|
+
case import_zod3.z.ZodIssueCode.invalid_enum_value:
|
|
931
|
+
case import_zod3.z.ZodIssueCode.invalid_literal:
|
|
932
|
+
return "field.invalid_option";
|
|
933
|
+
case import_zod3.z.ZodIssueCode.custom:
|
|
934
|
+
return "field.custom";
|
|
935
|
+
default:
|
|
936
|
+
return "field.custom";
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function buildFieldMetadataLookup(template) {
|
|
940
|
+
const map = /* @__PURE__ */ new Map();
|
|
941
|
+
for (const section of template.sections) {
|
|
942
|
+
for (const field of section.fields) {
|
|
943
|
+
map.set(field.id, {
|
|
944
|
+
sectionId: section.id,
|
|
945
|
+
sectionTitle: section.title,
|
|
946
|
+
label: field.label
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return map;
|
|
951
|
+
}
|
|
952
|
+
function mapZodIssuesToResponseErrors(template, issues) {
|
|
953
|
+
const fieldMeta = buildFieldMetadataLookup(template);
|
|
954
|
+
const errors = [];
|
|
955
|
+
for (const issue of issues) {
|
|
956
|
+
const path = issue.path ?? [];
|
|
957
|
+
const topLevelFieldId = path.find(
|
|
958
|
+
(p) => typeof p === "string"
|
|
959
|
+
);
|
|
960
|
+
const rowIndex = path.find(
|
|
961
|
+
(p) => typeof p === "number"
|
|
770
962
|
);
|
|
963
|
+
const nestedFieldId = path.length >= 3 && typeof path[path.length - 1] === "string" ? path[path.length - 1] : void 0;
|
|
964
|
+
const fieldId = topLevelFieldId ?? "";
|
|
965
|
+
const meta = fieldId ? fieldMeta.get(fieldId) : void 0;
|
|
966
|
+
const code = mapZodIssueToFieldErrorCode(issue);
|
|
967
|
+
const sectionLabel = meta?.sectionTitle ?? meta?.sectionId;
|
|
968
|
+
const fieldLabel = meta?.label ?? fieldId;
|
|
969
|
+
let message;
|
|
970
|
+
if (rowIndex != null && nestedFieldId && meta && sectionLabel) {
|
|
971
|
+
message = `Section "${sectionLabel}" \u2192 "${meta.label}" (row ${rowIndex + 1}, field "${nestedFieldId}"): ${issue.message}`;
|
|
972
|
+
} else if (meta && sectionLabel && fieldLabel) {
|
|
973
|
+
message = `Section "${sectionLabel}" \u2192 Field "${fieldLabel}": ${issue.message}`;
|
|
974
|
+
} else if (fieldLabel) {
|
|
975
|
+
message = `Field "${fieldLabel}": ${issue.message}`;
|
|
976
|
+
} else {
|
|
977
|
+
message = issue.message;
|
|
978
|
+
}
|
|
979
|
+
errors.push({
|
|
980
|
+
fieldId,
|
|
981
|
+
sectionId: meta?.sectionId,
|
|
982
|
+
sectionTitle: meta?.sectionTitle,
|
|
983
|
+
label: meta?.label,
|
|
984
|
+
code,
|
|
985
|
+
message,
|
|
986
|
+
rawIssue: issue
|
|
987
|
+
});
|
|
771
988
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
989
|
+
return errors;
|
|
990
|
+
}
|
|
991
|
+
function explainValidationError(template, error) {
|
|
992
|
+
if (error instanceof import_zod3.ZodError) {
|
|
993
|
+
return mapZodIssuesToResponseErrors(template, error.issues);
|
|
775
994
|
}
|
|
776
|
-
return
|
|
995
|
+
return null;
|
|
777
996
|
}
|
|
778
997
|
|
|
779
998
|
// src/validation/responses.ts
|
|
780
|
-
|
|
781
|
-
function buildBaseFieldSchema(field) {
|
|
999
|
+
function buildBaseFieldSchema2(field) {
|
|
782
1000
|
const isRequired = Boolean(field.required);
|
|
1001
|
+
const isComputed = Boolean(field.computed);
|
|
1002
|
+
if (isComputed) {
|
|
1003
|
+
return import_zod4.z.any().optional();
|
|
1004
|
+
}
|
|
783
1005
|
const registry = FieldRegistry.get(field.type);
|
|
784
1006
|
if (registry?.buildResponseSchema) {
|
|
785
1007
|
return registry.buildResponseSchema(field);
|
|
@@ -787,7 +1009,7 @@ function buildBaseFieldSchema(field) {
|
|
|
787
1009
|
switch (field.type) {
|
|
788
1010
|
case "shortText":
|
|
789
1011
|
case "longText": {
|
|
790
|
-
let schema =
|
|
1012
|
+
let schema = import_zod4.z.string();
|
|
791
1013
|
if (typeof field.minLength === "number") {
|
|
792
1014
|
schema = schema.min(field.minLength);
|
|
793
1015
|
}
|
|
@@ -797,7 +1019,7 @@ function buildBaseFieldSchema(field) {
|
|
|
797
1019
|
return isRequired ? schema : schema.optional();
|
|
798
1020
|
}
|
|
799
1021
|
case "number": {
|
|
800
|
-
let schema =
|
|
1022
|
+
let schema = import_zod4.z.number();
|
|
801
1023
|
if (typeof field.minValue === "number") {
|
|
802
1024
|
schema = schema.min(field.minValue);
|
|
803
1025
|
}
|
|
@@ -807,15 +1029,15 @@ function buildBaseFieldSchema(field) {
|
|
|
807
1029
|
return isRequired ? schema : schema.optional();
|
|
808
1030
|
}
|
|
809
1031
|
case "date": {
|
|
810
|
-
let schema =
|
|
1032
|
+
let schema = import_zod4.z.string();
|
|
811
1033
|
return isRequired ? schema : schema.optional();
|
|
812
1034
|
}
|
|
813
1035
|
case "checkbox": {
|
|
814
|
-
if (isRequired) return
|
|
815
|
-
return
|
|
1036
|
+
if (isRequired) return import_zod4.z.literal(true);
|
|
1037
|
+
return import_zod4.z.boolean().optional();
|
|
816
1038
|
}
|
|
817
1039
|
case "singleSelect": {
|
|
818
|
-
let schema =
|
|
1040
|
+
let schema = import_zod4.z.string();
|
|
819
1041
|
if (Array.isArray(field.options) && field.options.length > 0) {
|
|
820
1042
|
const allowed = new Set(field.options);
|
|
821
1043
|
schema = schema.refine(
|
|
@@ -826,11 +1048,13 @@ function buildBaseFieldSchema(field) {
|
|
|
826
1048
|
return isRequired ? schema : schema.optional();
|
|
827
1049
|
}
|
|
828
1050
|
case "multiSelect": {
|
|
829
|
-
let schema =
|
|
1051
|
+
let schema = import_zod4.z.array(import_zod4.z.string());
|
|
830
1052
|
if (Array.isArray(field.options) && field.options.length > 0 && !field.allowOther) {
|
|
831
1053
|
const allowed = new Set(field.options);
|
|
832
1054
|
schema = schema.refine(
|
|
833
|
-
(values) => values.every(
|
|
1055
|
+
(values) => values.every(
|
|
1056
|
+
(value) => allowed.has(value)
|
|
1057
|
+
),
|
|
834
1058
|
"All values must be among the defined options."
|
|
835
1059
|
);
|
|
836
1060
|
}
|
|
@@ -857,11 +1081,11 @@ function buildBaseFieldSchema(field) {
|
|
|
857
1081
|
const nestedFields = Array.isArray(field.fields) ? field.fields : [];
|
|
858
1082
|
const rowShape = {};
|
|
859
1083
|
for (const nested of nestedFields) {
|
|
860
|
-
rowShape[nested.id] =
|
|
1084
|
+
rowShape[nested.id] = buildBaseFieldSchema2(
|
|
861
1085
|
nested
|
|
862
1086
|
);
|
|
863
1087
|
}
|
|
864
|
-
let schema =
|
|
1088
|
+
let schema = import_zod4.z.array(import_zod4.z.object(rowShape));
|
|
865
1089
|
if (typeof field.min === "number") {
|
|
866
1090
|
schema = schema.min(
|
|
867
1091
|
field.min,
|
|
@@ -877,12 +1101,11 @@ function buildBaseFieldSchema(field) {
|
|
|
877
1101
|
return isRequired ? schema : schema.optional();
|
|
878
1102
|
}
|
|
879
1103
|
default: {
|
|
880
|
-
return
|
|
1104
|
+
return import_zod4.z.any();
|
|
881
1105
|
}
|
|
882
1106
|
}
|
|
883
1107
|
}
|
|
884
1108
|
function buildRepeatGroupSchemaWithConditions(field, response) {
|
|
885
|
-
const isRequired = Boolean(field.required);
|
|
886
1109
|
const nestedFields = Array.isArray(field.fields) ? field.fields : [];
|
|
887
1110
|
const rowShape = {};
|
|
888
1111
|
for (const nested of nestedFields) {
|
|
@@ -891,48 +1114,58 @@ function buildRepeatGroupSchemaWithConditions(field, response) {
|
|
|
891
1114
|
response
|
|
892
1115
|
);
|
|
893
1116
|
}
|
|
894
|
-
let schema =
|
|
1117
|
+
let schema = import_zod4.z.array(import_zod4.z.object(rowShape));
|
|
895
1118
|
const hasStaticMin = typeof field.min === "number";
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
if (
|
|
900
|
-
const
|
|
1119
|
+
const hasStaticMax = typeof field.max === "number";
|
|
1120
|
+
let effectiveMin = hasStaticMin ? field.min : void 0;
|
|
1121
|
+
let effectiveMax = hasStaticMax ? field.max : void 0;
|
|
1122
|
+
if (field.minIf) {
|
|
1123
|
+
const applyDynamicMin = evaluateCondition(field.minIf, response);
|
|
1124
|
+
if (applyDynamicMin && typeof field.min === "number") {
|
|
1125
|
+
effectiveMin = field.min;
|
|
1126
|
+
} else if (!applyDynamicMin) {
|
|
1127
|
+
effectiveMin = void 0;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (field.maxIf) {
|
|
1131
|
+
const applyDynamicMax = evaluateCondition(field.maxIf, response);
|
|
1132
|
+
if (applyDynamicMax && typeof field.max === "number") {
|
|
1133
|
+
effectiveMax = field.max;
|
|
1134
|
+
} else if (!applyDynamicMax) {
|
|
1135
|
+
effectiveMax = void 0;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
if (typeof effectiveMin === "number") {
|
|
901
1139
|
schema = schema.min(
|
|
902
|
-
|
|
903
|
-
`Add at least ${
|
|
1140
|
+
effectiveMin,
|
|
1141
|
+
`Add at least ${effectiveMin} item(s).`
|
|
904
1142
|
);
|
|
905
1143
|
}
|
|
906
|
-
|
|
907
|
-
const hasMaxIf = Boolean(field.maxIf);
|
|
908
|
-
const maxIfCondition = field.maxIf;
|
|
909
|
-
const shouldEnforceMax = hasStaticMax && (!hasMaxIf || evaluateCondition(maxIfCondition, response));
|
|
910
|
-
if (shouldEnforceMax) {
|
|
911
|
-
const maxValue = field.max;
|
|
1144
|
+
if (typeof effectiveMax === "number") {
|
|
912
1145
|
schema = schema.max(
|
|
913
|
-
|
|
914
|
-
`Add at most ${
|
|
1146
|
+
effectiveMax,
|
|
1147
|
+
`Add at most ${effectiveMax} item(s).`
|
|
915
1148
|
);
|
|
916
1149
|
}
|
|
917
|
-
return
|
|
1150
|
+
return schema;
|
|
918
1151
|
}
|
|
919
1152
|
function buildConditionalFieldSchema(field, response) {
|
|
920
1153
|
if (field.visibleIf) {
|
|
921
1154
|
const visible = evaluateCondition(field.visibleIf, response);
|
|
922
1155
|
if (!visible) {
|
|
923
|
-
return
|
|
1156
|
+
return import_zod4.z.any().optional();
|
|
924
1157
|
}
|
|
925
1158
|
}
|
|
926
1159
|
let schema;
|
|
927
1160
|
if (field.type === "repeatGroup") {
|
|
928
1161
|
schema = buildRepeatGroupSchemaWithConditions(field, response);
|
|
929
1162
|
} else {
|
|
930
|
-
schema =
|
|
1163
|
+
schema = buildBaseFieldSchema2(field);
|
|
931
1164
|
}
|
|
932
1165
|
if (field.requiredIf) {
|
|
933
1166
|
const shouldBeRequired = evaluateCondition(field.requiredIf, response);
|
|
934
1167
|
if (shouldBeRequired) {
|
|
935
|
-
if (schema instanceof
|
|
1168
|
+
if (schema instanceof import_zod4.z.ZodOptional) {
|
|
936
1169
|
schema = schema.unwrap();
|
|
937
1170
|
}
|
|
938
1171
|
}
|
|
@@ -946,18 +1179,12 @@ function buildResponseSchemaWithConditions(template, response) {
|
|
|
946
1179
|
shape[field.id] = buildConditionalFieldSchema(field, response);
|
|
947
1180
|
}
|
|
948
1181
|
}
|
|
949
|
-
return
|
|
1182
|
+
return import_zod4.z.object(shape);
|
|
950
1183
|
}
|
|
951
1184
|
function buildResponseSchema(template) {
|
|
952
1185
|
return buildResponseSchemaWithConditions(template, {});
|
|
953
1186
|
}
|
|
954
|
-
function
|
|
955
|
-
if (typeof data !== "object" || data === null) {
|
|
956
|
-
throw new Error("Response must be an object");
|
|
957
|
-
}
|
|
958
|
-
const response = data;
|
|
959
|
-
const schema = buildResponseSchemaWithConditions(template, response);
|
|
960
|
-
const parsed = schema.parse(response);
|
|
1187
|
+
function stripInvisibleFields(template, response, parsed) {
|
|
961
1188
|
for (const section of template.sections) {
|
|
962
1189
|
for (const field of section.fields) {
|
|
963
1190
|
if (field.visibleIf) {
|
|
@@ -968,76 +1195,38 @@ function validateReportResponse(template, data) {
|
|
|
968
1195
|
}
|
|
969
1196
|
}
|
|
970
1197
|
}
|
|
971
|
-
return parsed;
|
|
972
|
-
}
|
|
973
|
-
function mapZodIssueToFieldErrorCode(issue) {
|
|
974
|
-
switch (issue.code) {
|
|
975
|
-
case import_zod2.z.ZodIssueCode.invalid_type:
|
|
976
|
-
return "field.invalid_type";
|
|
977
|
-
case import_zod2.z.ZodIssueCode.too_small:
|
|
978
|
-
return "field.too_small";
|
|
979
|
-
case import_zod2.z.ZodIssueCode.too_big:
|
|
980
|
-
return "field.too_big";
|
|
981
|
-
case import_zod2.z.ZodIssueCode.invalid_enum_value:
|
|
982
|
-
case import_zod2.z.ZodIssueCode.invalid_literal:
|
|
983
|
-
return "field.invalid_option";
|
|
984
|
-
case import_zod2.z.ZodIssueCode.custom:
|
|
985
|
-
return "field.custom";
|
|
986
|
-
default:
|
|
987
|
-
return "field.custom";
|
|
988
|
-
}
|
|
989
1198
|
}
|
|
990
|
-
function
|
|
991
|
-
const map = /* @__PURE__ */ new Map();
|
|
1199
|
+
function stripComputedFields(template, parsed) {
|
|
992
1200
|
for (const section of template.sections) {
|
|
993
1201
|
for (const field of section.fields) {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1202
|
+
if (field.computed) {
|
|
1203
|
+
delete parsed[field.id];
|
|
1204
|
+
}
|
|
1205
|
+
if (field.type === "repeatGroup" && Array.isArray(parsed[field.id])) {
|
|
1206
|
+
const rows = parsed[field.id];
|
|
1207
|
+
const nestedFields = Array.isArray(field.fields) ? field.fields : [];
|
|
1208
|
+
for (const row of rows) {
|
|
1209
|
+
if (!row || typeof row !== "object") continue;
|
|
1210
|
+
for (const nested of nestedFields) {
|
|
1211
|
+
if (nested.computed) {
|
|
1212
|
+
delete row[nested.id];
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
999
1217
|
}
|
|
1000
1218
|
}
|
|
1001
|
-
return map;
|
|
1002
1219
|
}
|
|
1003
|
-
function
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
for (const issue of issues) {
|
|
1007
|
-
const path = issue.path ?? [];
|
|
1008
|
-
const topLevelFieldId = path.find(
|
|
1009
|
-
(p) => typeof p === "string"
|
|
1010
|
-
);
|
|
1011
|
-
const rowIndex = path.find(
|
|
1012
|
-
(p) => typeof p === "number"
|
|
1013
|
-
);
|
|
1014
|
-
const nestedFieldId = path.length >= 3 && typeof path[path.length - 1] === "string" ? path[path.length - 1] : void 0;
|
|
1015
|
-
const fieldId = topLevelFieldId ?? "";
|
|
1016
|
-
const meta = fieldId ? fieldMeta.get(fieldId) : void 0;
|
|
1017
|
-
const code = mapZodIssueToFieldErrorCode(issue);
|
|
1018
|
-
const sectionLabel = meta?.sectionTitle ?? meta?.sectionId;
|
|
1019
|
-
const fieldLabel = meta?.label ?? fieldId;
|
|
1020
|
-
let message;
|
|
1021
|
-
if (rowIndex != null && nestedFieldId && meta && sectionLabel) {
|
|
1022
|
-
message = `Section "${sectionLabel}" \u2192 "${meta.label}" (row ${rowIndex + 1}, field "${nestedFieldId}"): ${issue.message}`;
|
|
1023
|
-
} else if (meta && sectionLabel && fieldLabel) {
|
|
1024
|
-
message = `Section "${sectionLabel}" \u2192 Field "${fieldLabel}": ${issue.message}`;
|
|
1025
|
-
} else if (fieldLabel) {
|
|
1026
|
-
message = `Field "${fieldLabel}": ${issue.message}`;
|
|
1027
|
-
} else {
|
|
1028
|
-
message = issue.message;
|
|
1029
|
-
}
|
|
1030
|
-
errors.push({
|
|
1031
|
-
fieldId,
|
|
1032
|
-
sectionId: meta?.sectionId,
|
|
1033
|
-
sectionTitle: meta?.sectionTitle,
|
|
1034
|
-
label: meta?.label,
|
|
1035
|
-
code,
|
|
1036
|
-
message,
|
|
1037
|
-
rawIssue: issue
|
|
1038
|
-
});
|
|
1220
|
+
function validateReportResponse(template, data) {
|
|
1221
|
+
if (typeof data !== "object" || data === null) {
|
|
1222
|
+
throw new Error("Response must be an object");
|
|
1039
1223
|
}
|
|
1040
|
-
|
|
1224
|
+
const response = data;
|
|
1225
|
+
const schema = buildResponseSchemaWithConditions(template, response);
|
|
1226
|
+
const parsed = schema.parse(response);
|
|
1227
|
+
stripInvisibleFields(template, response, parsed);
|
|
1228
|
+
stripComputedFields(template, parsed);
|
|
1229
|
+
return parsed;
|
|
1041
1230
|
}
|
|
1042
1231
|
function validateReportResponseDetailed(template, data) {
|
|
1043
1232
|
if (typeof data !== "object" || data === null) {
|
|
@@ -1057,27 +1246,13 @@ function validateReportResponseDetailed(template, data) {
|
|
|
1057
1246
|
const result = schema.safeParse(response);
|
|
1058
1247
|
if (result.success) {
|
|
1059
1248
|
const parsed = { ...result.data };
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
if (field.visibleIf) {
|
|
1063
|
-
const visible = evaluateCondition(field.visibleIf, response);
|
|
1064
|
-
if (!visible) {
|
|
1065
|
-
delete parsed[field.id];
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1249
|
+
stripInvisibleFields(template, response, parsed);
|
|
1250
|
+
stripComputedFields(template, parsed);
|
|
1070
1251
|
return { success: true, value: parsed };
|
|
1071
1252
|
}
|
|
1072
1253
|
const errors = mapZodIssuesToResponseErrors(template, result.error.issues);
|
|
1073
1254
|
return { success: false, errors };
|
|
1074
1255
|
}
|
|
1075
|
-
function explainValidationError(template, error) {
|
|
1076
|
-
if (error instanceof import_zod2.ZodError) {
|
|
1077
|
-
return mapZodIssuesToResponseErrors(template, error.issues);
|
|
1078
|
-
}
|
|
1079
|
-
return null;
|
|
1080
|
-
}
|
|
1081
1256
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1082
1257
|
0 && (module.exports = {
|
|
1083
1258
|
CORE_FIELD_DEFAULTS,
|