@case-framework/survey-core 0.7.0 → 0.9.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 +150 -143
- package/build/answer-types-DH0gv0Ea.d.mts +20 -0
- package/build/answer-types-DH0gv0Ea.d.mts.map +1 -0
- package/build/answer-types-Dtm6MqZQ.mjs +19 -0
- package/build/answer-types-Dtm6MqZQ.mjs.map +1 -0
- package/build/editor/colors.d.mts +1 -2
- package/build/editor/colors.d.mts.map +1 -1
- package/build/editor/colors.mjs.map +1 -1
- package/build/editor.d.mts +72931 -227
- package/build/editor.d.mts.map +1 -1
- package/build/editor.mjs +1281 -675
- package/build/editor.mjs.map +1 -1
- package/build/index.d.mts +2 -932
- package/build/index.mjs +1419 -1829
- package/build/index.mjs.map +1 -1
- package/build/keyed-object-D1yh_W5v.mjs +45 -0
- package/build/keyed-object-D1yh_W5v.mjs.map +1 -0
- package/build/localization-1lzNpyFX.mjs +1373 -0
- package/build/localization-1lzNpyFX.mjs.map +1 -0
- package/build/package.json +14 -9
- package/build/projection-DrLPFroE.mjs +2344 -0
- package/build/projection-DrLPFroE.mjs.map +1 -0
- package/build/registry-BOXcyWfX.d.mts +54292 -0
- package/build/registry-BOXcyWfX.d.mts.map +1 -0
- package/build/schema.d.mts +3 -0
- package/build/schema.mjs +4 -0
- package/package.json +14 -9
- package/build/index.d.mts.map +0 -1
- package/build/survey-BKblV5rN.mjs +0 -1460
- package/build/survey-BKblV5rN.mjs.map +0 -1
- package/build/survey-DedDemSc.d.mts +0 -997
- package/build/survey-DedDemSc.d.mts.map +0 -1
package/build/index.mjs
CHANGED
|
@@ -1,891 +1,274 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* SurveyItemResponse to store the response of a survey item.
|
|
18
|
-
*/
|
|
19
|
-
var SurveyItemResponse = class SurveyItemResponse {
|
|
20
|
-
itemId;
|
|
21
|
-
meta;
|
|
22
|
-
response;
|
|
23
|
-
itemType;
|
|
24
|
-
constructor(itemDef, response) {
|
|
25
|
-
this.itemId = itemDef.itemId;
|
|
26
|
-
this.response = response;
|
|
27
|
-
this.itemType = itemDef.itemType;
|
|
28
|
-
}
|
|
29
|
-
serialize() {
|
|
30
|
-
return {
|
|
31
|
-
itemId: this.itemId,
|
|
32
|
-
itemType: this.itemType,
|
|
33
|
-
meta: this.meta,
|
|
34
|
-
response: this.response?.serialize()
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
static deserialize(json) {
|
|
38
|
-
if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("Invalid survey item response payload");
|
|
39
|
-
if (typeof json.itemId !== "string" || json.itemId.trim().length === 0) throw new Error("Survey item response is missing a valid itemId");
|
|
40
|
-
if (typeof json.itemType !== "string" || json.itemType.trim().length === 0) throw new Error("Survey item response is missing a valid itemType");
|
|
41
|
-
const newResponse = new SurveyItemResponse({
|
|
42
|
-
itemId: json.itemId,
|
|
43
|
-
itemType: json.itemType
|
|
44
|
-
}, json.response ? ResponseItem.deserialize(json.response) : void 0);
|
|
45
|
-
newResponse.meta = json.meta;
|
|
46
|
-
return newResponse;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
var ResponseItem = class ResponseItem {
|
|
50
|
-
_slots;
|
|
51
|
-
constructor(initialValues) {
|
|
52
|
-
this._slots = new Map(initialValues ?? []);
|
|
53
|
-
}
|
|
54
|
-
get(slotId) {
|
|
55
|
-
return this._slots.get(slotId);
|
|
56
|
-
}
|
|
57
|
-
setSlotValue(slotId, slotResponse) {
|
|
58
|
-
if (slotResponse) this._slots.set(slotId, slotResponse);
|
|
59
|
-
else this._slots.delete(slotId);
|
|
60
|
-
}
|
|
61
|
-
serialize() {
|
|
62
|
-
if (this._slots.size === 0) return;
|
|
63
|
-
return { slots: Object.fromEntries(this._slots.entries()) };
|
|
64
|
-
}
|
|
65
|
-
isEmpty() {
|
|
66
|
-
return this._slots.size === 0;
|
|
67
|
-
}
|
|
68
|
-
clone() {
|
|
69
|
-
return new ResponseItem(Array.from(this._slots.entries()));
|
|
70
|
-
}
|
|
71
|
-
static deserialize(json) {
|
|
72
|
-
if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("Invalid response item payload");
|
|
73
|
-
const { slots } = json;
|
|
74
|
-
if (!slots || typeof slots !== "object" || Array.isArray(slots)) throw new Error("Invalid response slots payload");
|
|
75
|
-
return new ResponseItem(Object.entries(slots).map(([slotId, slotResponse]) => {
|
|
76
|
-
if (slotId.trim().length === 0) throw new Error("Response slot id cannot be empty");
|
|
77
|
-
assertResponseValue(slotResponse, `response.slots.${slotId}`);
|
|
78
|
-
return [slotId, slotResponse];
|
|
79
|
-
}));
|
|
1
|
+
import { $ as inspectAnnotated, A as slotPrefillSchema, B as ValueType, C as createCompositionSchema, Ct as calendarDaySchema, D as followUpBody, Dt as isCalendarDate, E as elementShape, Et as formatCalendarDate, F as richTextImageDimensionSchema, G as stringRecord, H as isResponseValue, I as richTextImageSourceSchema, J as boundField, K as annotated, L as surveyContentSchema, M as templateValueSchema, N as validationRuleSchema, O as itemEditorMetadataSchema, Ot as isCalendarDateUnit, P as jsonValueSchema, Q as extendObject, R as DurationUnits, S as conditionsShape, St as calendarDateToLocalDate, T as createSurveySchema, Tt as differenceCalendarDates, U as ownValue, V as assertResponseValue, W as setOwnValue, X as contentOwnerIds, Y as contentOwner, Z as expressionField, _ as hasRenderableRichTextBlock, _t as CALENDAR_DATE_UNITS, a as getLocalizationItems, at as expressionEditorConfigSchema, b as CURRENT_SURVEY_SCHEMA, bt as calendarDateFromLocalDate, c as listLocalizationEntries, ct as responseValueSchema, d as validateLocalizationRef, dt as remapExpression, et as ownedId, f as ContentType, ft as expressionArguments, g as getPlainTextFromRichTextContent, gt as visitExpression, h as getContentPlainText, ht as unreachable, i as getLocalizationEntryKey, it as transformAnnotated, j as slotTransformSchema, k as layoutPolicySchema, l as readLocalizationContent, lt as valueTypeSchema, m as getAssetUsagesFromContent, mt as transformExpression, n as getLocalizationCellKey, nt as remapAnnotated, o as hasLocalizationText, ot as expressionSchema, p as createRichTextContent, pt as operand, q as annotatedReferences, r as getLocalizationCoverage, rt as schemaAnnotation, s as isEmptySurveyLocalizationEntry, st as functionNameSchema, t as SURVEY_LOCALIZATION_SURFACES, tt as referenceId, u as resolveParticipantContent, ut as collectExpressionDependencies, v as hasRenderableRichTextContent, vt as addCalendarDate, w as createFlowItemSchema, wt as compareCalendarDates, x as CURRENT_SURVEY_SCHEMA_VERSION, xt as calendarDatePrecision, y as isContentEmpty, yt as calendarDateBounds, z as NumberPrecision } from "./localization-1lzNpyFX.mjs";
|
|
2
|
+
import { $ as shuffleArray, A as isAdmissibleInDraft, B as expressionIssues, C as surveyEventSchema, D as DefinitionError, E as hasPermittedChange, F as allocateResponseVariableName, G as responseValueElements, H as functionTypeIssues, I as isValidResponseVariableName, J as FUNCTION_SIGNATURES, K as responseValuesEqual, L as suggestResponseVariableName, M as getCompositionLabel, N as ResponseSlotRegistry, O as Survey, P as RESPONSE_VARIABLE_NAME_MAX_LENGTH, Q as resolveUniqueItemKey, R as ExpressionEvaluationError, S as itemResponseMetadataSchema, T as canWriteSelection, U as inferExpressionReturnType, V as expressionTypeIssues, W as invalidResponseDomainValues, X as generateCodingKey, Y as createSeededRandom, Z as generateId, _ as SlotTransformMode, a as optionIds, b as surveyResponseSchema, c as resolvedSlotEncoding, d as exportSingleResponseSlot, et as shuffleIndices, f as decodeExportColumnId, g as META_COLUMN_ORDER, h as ExportColumnSource, i as localizedExportSlots, j as surveyLocales, k as checkDocumentBudget, l as BooleanExportValues, m as EXPORTER_PROFILE_SCHEMA_VERSION, n as getDefaultProjectionDiagnostics, o as optionMapping, p as encodeExportColumnId, q as validateResponseSlotValue, r as getDefaultProjectionIssues, s as projectSlot, t as formatProjectedValue, tt as structuredCloneMethod, u as DEFAULT_TRANSFORM, v as SURVEY_RESPONSE_SCHEMA_VERSION, w as SurveyStateGraph, x as SurveyEventTypes, y as SurveyResponse, z as evaluateExpression } from "./projection-DrLPFroE.mjs";
|
|
3
|
+
import { t as strictKeyedObject } from "./keyed-object-D1yh_W5v.mjs";
|
|
4
|
+
import { format } from "date-fns/format";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region src/json-patch.ts
|
|
7
|
+
var JsonPatchError = class extends Error {
|
|
8
|
+
code;
|
|
9
|
+
path;
|
|
10
|
+
operationIndex;
|
|
11
|
+
constructor(message, code, path, operationIndex) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.path = path;
|
|
15
|
+
this.operationIndex = operationIndex;
|
|
16
|
+
this.name = "JsonPatchError";
|
|
80
17
|
}
|
|
81
18
|
};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
pageChanged: "pageChanged",
|
|
87
|
-
responeChanged: "responeChanged",
|
|
88
|
-
languageChanged: "languageChanged"
|
|
19
|
+
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
20
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
const assertValidPointerSegment = (segment, pointer) => {
|
|
22
|
+
if (/~(?![01])/.test(segment)) throw new JsonPatchError(`Invalid JSON pointer segment "${segment}" in "${pointer}". Only "~0" and "~1" escapes are allowed.`, "invalid-json-pointer", pointer);
|
|
89
23
|
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* SurveyResponse to store the responses of a survey.
|
|
95
|
-
*/
|
|
96
|
-
var SurveyResponse = class SurveyResponse {
|
|
97
|
-
key;
|
|
98
|
-
participantId;
|
|
99
|
-
submittedAt;
|
|
100
|
-
versionId;
|
|
101
|
-
responses;
|
|
102
|
-
events;
|
|
103
|
-
context;
|
|
104
|
-
constructor(key, versionId) {
|
|
105
|
-
this.key = key;
|
|
106
|
-
this.participantId = "";
|
|
107
|
-
this.submittedAt = 0;
|
|
108
|
-
this.versionId = versionId;
|
|
109
|
-
this.responses = /* @__PURE__ */ new Map();
|
|
110
|
-
this.events = [];
|
|
111
|
-
}
|
|
112
|
-
serialize() {
|
|
113
|
-
const items = Object.fromEntries(Array.from(this.responses.entries()).flatMap(([itemId, response]) => {
|
|
114
|
-
const serialized = response.serialize();
|
|
115
|
-
if (!serialized.response && !serialized.meta) return [];
|
|
116
|
-
return [[itemId, serialized]];
|
|
117
|
-
}));
|
|
118
|
-
return {
|
|
119
|
-
schemaVersion: 1,
|
|
120
|
-
key: this.key,
|
|
121
|
-
participantId: this.participantId,
|
|
122
|
-
submittedAt: this.submittedAt,
|
|
123
|
-
versionId: this.versionId,
|
|
124
|
-
items,
|
|
125
|
-
events: this.events,
|
|
126
|
-
context: this.context
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
static deserialize(json) {
|
|
130
|
-
if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("Invalid survey response payload");
|
|
131
|
-
if (json.schemaVersion !== 1) throw new Error(`Unsupported survey response schema version: ${String(json.schemaVersion)}`);
|
|
132
|
-
if (typeof json.key !== "string" || json.key.trim().length === 0) throw new Error("Survey response is missing a valid key");
|
|
133
|
-
if (typeof json.versionId !== "string" || json.versionId.trim().length === 0) throw new Error("Survey response is missing a valid versionId");
|
|
134
|
-
if (!json.items || typeof json.items !== "object" || Array.isArray(json.items)) throw new Error("Survey response is missing a valid items object");
|
|
135
|
-
const response = new SurveyResponse(json.key, json.versionId);
|
|
136
|
-
response.participantId = json.participantId;
|
|
137
|
-
response.submittedAt = json.submittedAt;
|
|
138
|
-
response.responses = new Map(Object.entries(json.items).map(([itemId, itemResponse]) => {
|
|
139
|
-
const deserialized = SurveyItemResponse.deserialize(itemResponse);
|
|
140
|
-
if (deserialized.itemId !== itemId) throw new Error(`Survey response item key mismatch for '${itemId}'`);
|
|
141
|
-
return [itemId, deserialized];
|
|
142
|
-
}));
|
|
143
|
-
response.events = json.events ?? [];
|
|
144
|
-
response.versionId = json.versionId;
|
|
145
|
-
response.key = json.key;
|
|
146
|
-
if (json.context) {
|
|
147
|
-
if (typeof json.context !== "object" || Array.isArray(json.context)) throw new Error("Survey response context must be an object");
|
|
148
|
-
response.context = Object.fromEntries(Object.entries(json.context).map(([contextKey, value]) => {
|
|
149
|
-
assertResponseValue(value, `context.${contextKey}`);
|
|
150
|
-
return [contextKey, value];
|
|
151
|
-
}));
|
|
152
|
-
}
|
|
153
|
-
return response;
|
|
154
|
-
}
|
|
24
|
+
const decodeJsonPointerSegment = (segment) => {
|
|
25
|
+
assertValidPointerSegment(segment, segment);
|
|
26
|
+
return segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
155
27
|
};
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
case ValueType.number: return {
|
|
165
|
-
type: ValueType.number,
|
|
166
|
-
value: 0
|
|
167
|
-
};
|
|
168
|
-
case ValueType.boolean: return {
|
|
169
|
-
type: ValueType.boolean,
|
|
170
|
-
value: false
|
|
171
|
-
};
|
|
172
|
-
case ValueType.date: return {
|
|
173
|
-
type: ValueType.date,
|
|
174
|
-
value: Math.floor(Date.now() / 1e3)
|
|
175
|
-
};
|
|
176
|
-
case ValueType.stringArray: return {
|
|
177
|
-
type: ValueType.stringArray,
|
|
178
|
-
value: []
|
|
179
|
-
};
|
|
180
|
-
case ValueType.numberArray: return {
|
|
181
|
-
type: ValueType.numberArray,
|
|
182
|
-
value: []
|
|
183
|
-
};
|
|
184
|
-
case ValueType.dateArray: return {
|
|
185
|
-
type: ValueType.dateArray,
|
|
186
|
-
value: []
|
|
187
|
-
};
|
|
188
|
-
case ValueType.duration: return {
|
|
189
|
-
type: ValueType.duration,
|
|
190
|
-
value: 0,
|
|
191
|
-
unit: DurationUnits.seconds
|
|
192
|
-
};
|
|
193
|
-
case ValueType.reference: return {
|
|
194
|
-
type: ValueType.reference,
|
|
195
|
-
value: ""
|
|
196
|
-
};
|
|
197
|
-
case ValueType.durationArray: return {
|
|
198
|
-
type: ValueType.durationArray,
|
|
199
|
-
value: [],
|
|
200
|
-
unit: DurationUnits.seconds
|
|
201
|
-
};
|
|
202
|
-
case ValueType.referenceArray: return {
|
|
203
|
-
type: ValueType.referenceArray,
|
|
204
|
-
value: []
|
|
205
|
-
};
|
|
206
|
-
default: throw new Error("Invalid return type: " + returnType);
|
|
207
|
-
}
|
|
28
|
+
const encodeJsonPointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
29
|
+
const parseJsonPointer = (pointer) => {
|
|
30
|
+
if (pointer === "") return [];
|
|
31
|
+
if (!pointer.startsWith("/")) throw new JsonPatchError(`Invalid JSON pointer "${pointer}". It must be empty or start with "/".`, "invalid-json-pointer", pointer);
|
|
32
|
+
return pointer.slice(1).split("/").map((segment) => {
|
|
33
|
+
assertValidPointerSegment(segment, pointer);
|
|
34
|
+
return segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
35
|
+
});
|
|
208
36
|
};
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
constructor(context) {
|
|
214
|
-
this.context = context;
|
|
215
|
-
}
|
|
216
|
-
eval(expression) {
|
|
217
|
-
if (expression === void 0) return;
|
|
218
|
-
switch (expression.type) {
|
|
219
|
-
case ExpressionType.Const: return this.evaluateConst(expression);
|
|
220
|
-
case ExpressionType.Function: return this.evaluateFunction(expression);
|
|
221
|
-
case ExpressionType.ResponseVariable: return this.evaluateResponseVariable(expression);
|
|
222
|
-
case ExpressionType.ContextVariable: return this.evaluateContextVariable(expression);
|
|
223
|
-
default: throw new Error(`Unsupported expression type: ${expression.type}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
setContext(context) {
|
|
227
|
-
this.context = context;
|
|
228
|
-
}
|
|
229
|
-
evaluateConst(expression) {
|
|
230
|
-
return expression.value;
|
|
231
|
-
}
|
|
232
|
-
evaluateResponseVariable(expression) {
|
|
233
|
-
const varRef = expression.responseVariableRef;
|
|
234
|
-
switch (varRef.name) {
|
|
235
|
-
case "get": return this.context?.responses[varRef.itemId]?.response?.get(varRef.slotId);
|
|
236
|
-
case "isDefined": return {
|
|
237
|
-
type: ValueType.boolean,
|
|
238
|
-
value: this.context?.responses[varRef.itemId]?.response?.get(varRef.slotId) !== void 0
|
|
239
|
-
};
|
|
240
|
-
default: throw new Error(`Unsupported value reference method: ${varRef.name}`);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
evaluateFunction(expression) {
|
|
244
|
-
switch (expression.functionName) {
|
|
245
|
-
case "and": return this.evaluateAnd(expression);
|
|
246
|
-
case "or": return this.evaluateOr(expression);
|
|
247
|
-
case "not": return this.evaluateNot(expression);
|
|
248
|
-
case "str_eq": return this.evaluateStrEq(expression);
|
|
249
|
-
case "eq": return this.evaluateEq(expression);
|
|
250
|
-
case "gt": return this.evaluateGt(expression);
|
|
251
|
-
case "gte": return this.evaluateGte(expression);
|
|
252
|
-
case "lt": return this.evaluateLt(expression);
|
|
253
|
-
case "lte": return this.evaluateLte(expression);
|
|
254
|
-
case "in_range": return this.evaluateInRange(expression);
|
|
255
|
-
case "sum": return this.evaluateSum(expression);
|
|
256
|
-
case "min": return this.evaluateMin(expression);
|
|
257
|
-
case "max": return this.evaluateMax(expression);
|
|
258
|
-
case "list_contains": return this.evaluateListContains(expression);
|
|
259
|
-
default: throw new Error(`Unsupported function: ${expression.functionName}`);
|
|
260
|
-
}
|
|
37
|
+
const parseArrayIndex = ({ segment, arrayLength, allowAppend, allowEnd, path }) => {
|
|
38
|
+
if (segment === "-") {
|
|
39
|
+
if (allowAppend) return arrayLength;
|
|
40
|
+
throw new JsonPatchError(`JSON pointer "${path}" uses "-" where an existing array index is required.`, "invalid-array-index", path);
|
|
261
41
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (flagValue === void 0) return;
|
|
282
|
-
return {
|
|
283
|
-
type: ValueType.string,
|
|
284
|
-
value: flagValue
|
|
285
|
-
};
|
|
286
|
-
case ValueType.number: {
|
|
287
|
-
if (flagValue === void 0) return;
|
|
288
|
-
const numValue = parseFloat(flagValue);
|
|
289
|
-
return isNaN(numValue) ? void 0 : {
|
|
290
|
-
type: ValueType.number,
|
|
291
|
-
value: numValue
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
case ValueType.date: {
|
|
295
|
-
if (flagValue === void 0) return;
|
|
296
|
-
const timestamp = parseFloat(flagValue);
|
|
297
|
-
return isNaN(timestamp) ? void 0 : {
|
|
298
|
-
type: ValueType.date,
|
|
299
|
-
value: timestamp
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
default:
|
|
303
|
-
if (flagValue === void 0) return;
|
|
304
|
-
return {
|
|
305
|
-
type: ValueType.string,
|
|
306
|
-
value: flagValue
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
case ContextVariableType.CustomValue: {
|
|
311
|
-
if (!expression.key) return;
|
|
312
|
-
const customKey = this.eval(expression.key);
|
|
313
|
-
if (customKey?.type !== ValueType.string || customKey.value === void 0) return;
|
|
314
|
-
const customValue = surveyContext.customValues?.[customKey.value];
|
|
315
|
-
if (customValue === void 0) return;
|
|
316
|
-
if (expression.asType !== void 0) {
|
|
317
|
-
if (customValue?.type !== expression.asType) throw new Error(`Custom value type does not match expected type, got ${customValue?.type} and expected ${expression.asType}`);
|
|
318
|
-
}
|
|
319
|
-
return customValue;
|
|
320
|
-
}
|
|
321
|
-
case ContextVariableType.CustomExpression: {
|
|
322
|
-
if (!expression.key) return;
|
|
323
|
-
const expressionKey = this.eval(expression.key);
|
|
324
|
-
if (expressionKey?.type !== ValueType.string || expressionKey.value === void 0) return;
|
|
325
|
-
const customExpression = surveyContext.customExpressions?.[expressionKey.value];
|
|
326
|
-
if (typeof customExpression !== ExpressionType.Function || customExpression === void 0) return;
|
|
327
|
-
try {
|
|
328
|
-
const args = expression.arguments;
|
|
329
|
-
const result = customExpression(args);
|
|
330
|
-
if (expression.asType !== void 0) {
|
|
331
|
-
if (result?.type !== expression.asType) throw new Error(`Custom expression result type does not match expected type, got ${result?.type} and expected ${expression.asType}`);
|
|
332
|
-
}
|
|
333
|
-
return result;
|
|
334
|
-
} catch (error) {
|
|
335
|
-
throw new Error(`Error evaluating custom expression: ${error}`);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
42
|
+
if (!/^(0|[1-9][0-9]*)$/.test(segment)) throw new JsonPatchError(`JSON pointer "${path}" uses invalid array index "${segment}".`, "invalid-array-index", path);
|
|
43
|
+
const index = Number(segment);
|
|
44
|
+
const maximum = allowEnd ? arrayLength : arrayLength - 1;
|
|
45
|
+
if (index < 0 || index > maximum) throw new JsonPatchError(`JSON pointer "${path}" array index ${index} is out of bounds.`, "array-index-out-of-bounds", path);
|
|
46
|
+
return index;
|
|
47
|
+
};
|
|
48
|
+
const getValueAtSegments = (document, segments, path) => {
|
|
49
|
+
let current = document;
|
|
50
|
+
for (const segment of segments) {
|
|
51
|
+
if (Array.isArray(current)) {
|
|
52
|
+
const index = parseArrayIndex({
|
|
53
|
+
segment,
|
|
54
|
+
arrayLength: current.length,
|
|
55
|
+
allowAppend: false,
|
|
56
|
+
allowEnd: false,
|
|
57
|
+
path
|
|
58
|
+
});
|
|
59
|
+
current = current[index];
|
|
60
|
+
continue;
|
|
338
61
|
}
|
|
62
|
+
if (!isRecord(current) || !hasOwn(current, segment)) throw new JsonPatchError(`JSON pointer "${path}" does not exist.`, "path-not-found", path);
|
|
63
|
+
current = current[segment];
|
|
339
64
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
evaluateNot(expression) {
|
|
353
|
-
if (expression.arguments.length !== 1 || expression.arguments[0] === void 0) throw new Error(`Not function expects 1 argument, got ${expression.arguments.length}`);
|
|
354
|
-
const resolvedValue = this.eval(expression.arguments[0]);
|
|
355
|
-
if (resolvedValue?.type !== ValueType.boolean) throw new Error(`Not function expects a boolean argument, got ${resolvedValue?.type}`);
|
|
356
|
-
if (resolvedValue === void 0) return {
|
|
357
|
-
type: ValueType.boolean,
|
|
358
|
-
value: false
|
|
359
|
-
};
|
|
360
|
-
return {
|
|
361
|
-
type: ValueType.boolean,
|
|
362
|
-
value: !resolvedValue.value
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
evaluateListContains(expression) {
|
|
366
|
-
if (expression.arguments.length !== 2) throw new Error(`List contains function expects 2 arguments, got ${expression.arguments.length}`);
|
|
367
|
-
const resolvedList = this.eval(expression.arguments[0]);
|
|
368
|
-
const resolvedItem = this.eval(expression.arguments[1]);
|
|
369
|
-
if (resolvedList === void 0 || resolvedItem === void 0) return {
|
|
370
|
-
type: ValueType.boolean,
|
|
371
|
-
value: false
|
|
372
|
-
};
|
|
373
|
-
const allowedListTypes = [ValueType.stringArray, ValueType.referenceArray];
|
|
374
|
-
const allowedItemTypes = [ValueType.string, ValueType.reference];
|
|
375
|
-
const resolvedListType = resolvedList.type;
|
|
376
|
-
const resolvedItemType = resolvedItem.type;
|
|
377
|
-
if (!allowedListTypes.includes(resolvedListType) || !allowedItemTypes.includes(resolvedItemType)) throw new Error(`List contains function expects a string array list and a string item, got ${resolvedList?.type} and ${resolvedItem?.type}`);
|
|
378
|
-
const list = resolvedList.value;
|
|
379
|
-
const item = resolvedItem.value;
|
|
380
|
-
return {
|
|
381
|
-
type: ValueType.boolean,
|
|
382
|
-
value: list.includes(item)
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
evaluateStrEq(expression) {
|
|
386
|
-
if (expression.arguments.length !== 2) throw new Error(`String equals function expects 2 arguments, got ${expression.arguments.length}`);
|
|
387
|
-
const resolvedA = this.eval(expression.arguments[0]);
|
|
388
|
-
const resolvedB = this.eval(expression.arguments[1]);
|
|
389
|
-
if (resolvedA === void 0 || resolvedB === void 0) return {
|
|
390
|
-
type: ValueType.boolean,
|
|
391
|
-
value: false
|
|
392
|
-
};
|
|
393
|
-
if (resolvedA?.type !== ValueType.string || resolvedB?.type !== ValueType.string) throw new Error(`String equals function expects strings for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
|
|
394
|
-
return {
|
|
395
|
-
type: ValueType.boolean,
|
|
396
|
-
value: resolvedA.value === resolvedB.value
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
evaluateEq(expression) {
|
|
400
|
-
if (expression.arguments.length !== 2) throw new Error(`Equals function expects 2 arguments, got ${expression.arguments.length}`);
|
|
401
|
-
const resolvedA = this.eval(expression.arguments[0]);
|
|
402
|
-
const resolvedB = this.eval(expression.arguments[1]);
|
|
403
|
-
if (resolvedA === void 0 || resolvedB === void 0) return {
|
|
404
|
-
type: ValueType.boolean,
|
|
405
|
-
value: false
|
|
406
|
-
};
|
|
407
|
-
if (resolvedA?.type !== resolvedB?.type) throw new Error(`Equals function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
|
|
408
|
-
return {
|
|
409
|
-
type: ValueType.boolean,
|
|
410
|
-
value: resolvedA.value === resolvedB.value
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
evaluateGt(expression) {
|
|
414
|
-
if (expression.arguments.length !== 2) throw new Error(`Greater than function expects 2 arguments, got ${expression.arguments.length}`);
|
|
415
|
-
const resolvedA = this.eval(expression.arguments[0]);
|
|
416
|
-
const resolvedB = this.eval(expression.arguments[1]);
|
|
417
|
-
if (resolvedA === void 0 || resolvedB === void 0) return {
|
|
418
|
-
type: ValueType.boolean,
|
|
419
|
-
value: false
|
|
420
|
-
};
|
|
421
|
-
if (resolvedA?.type !== resolvedB?.type) throw new Error(`Greater than function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
|
|
422
|
-
return {
|
|
423
|
-
type: ValueType.boolean,
|
|
424
|
-
value: resolvedA.value > resolvedB.value
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
evaluateGte(expression) {
|
|
428
|
-
if (expression.arguments.length !== 2) throw new Error(`Greater than or equal to function expects 2 arguments, got ${expression.arguments.length}`);
|
|
429
|
-
const resolvedA = this.eval(expression.arguments[0]);
|
|
430
|
-
const resolvedB = this.eval(expression.arguments[1]);
|
|
431
|
-
if (resolvedA === void 0 || resolvedB === void 0) return {
|
|
432
|
-
type: ValueType.boolean,
|
|
433
|
-
value: false
|
|
434
|
-
};
|
|
435
|
-
if (resolvedA?.type !== resolvedB?.type) throw new Error(`Greater than or equal to function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
|
|
436
|
-
return {
|
|
437
|
-
type: ValueType.boolean,
|
|
438
|
-
value: resolvedA.value >= resolvedB.value
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
evaluateLt(expression) {
|
|
442
|
-
if (expression.arguments.length !== 2) throw new Error(`Less than function expects 2 arguments, got ${expression.arguments.length}`);
|
|
443
|
-
const resolvedA = this.eval(expression.arguments[0]);
|
|
444
|
-
const resolvedB = this.eval(expression.arguments[1]);
|
|
445
|
-
if (resolvedA === void 0 || resolvedB === void 0) return {
|
|
446
|
-
type: ValueType.boolean,
|
|
447
|
-
value: false
|
|
448
|
-
};
|
|
449
|
-
if (resolvedA?.type !== resolvedB?.type) throw new Error(`Less than function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
|
|
450
|
-
return {
|
|
451
|
-
type: ValueType.boolean,
|
|
452
|
-
value: resolvedA.value < resolvedB.value
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
evaluateLte(expression) {
|
|
456
|
-
if (expression.arguments.length !== 2) throw new Error(`Less than or equal to function expects 2 arguments, got ${expression.arguments.length}`);
|
|
457
|
-
const resolvedA = this.eval(expression.arguments[0]);
|
|
458
|
-
const resolvedB = this.eval(expression.arguments[1]);
|
|
459
|
-
if (resolvedA === void 0 || resolvedB === void 0) return {
|
|
460
|
-
type: ValueType.boolean,
|
|
461
|
-
value: false
|
|
462
|
-
};
|
|
463
|
-
if (resolvedA?.type !== resolvedB?.type) throw new Error(`Less than or equal to function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
|
|
464
|
-
return {
|
|
465
|
-
type: ValueType.boolean,
|
|
466
|
-
value: resolvedA.value <= resolvedB.value
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
evaluateInRange(expression) {
|
|
470
|
-
if (expression.arguments.length !== 4) throw new Error(`In range function expects 4 arguments, got ${expression.arguments.length}`);
|
|
471
|
-
const resolvedValue = this.eval(expression.arguments[0]);
|
|
472
|
-
const resolvedMin = this.eval(expression.arguments[1]);
|
|
473
|
-
const resolvedMax = this.eval(expression.arguments[2]);
|
|
474
|
-
const resolvedInclusive = this.eval(expression.arguments[3])?.value === true;
|
|
475
|
-
if (resolvedValue === void 0 || resolvedMin === void 0 || resolvedMax === void 0) return {
|
|
476
|
-
type: ValueType.boolean,
|
|
477
|
-
value: false
|
|
478
|
-
};
|
|
479
|
-
if (resolvedValue?.type !== ValueType.number || resolvedMin?.type !== ValueType.number || resolvedMax?.type !== ValueType.number) throw new Error(`In range function expects numbers for all arguments, got ${resolvedValue?.type} and ${resolvedMin?.type} and ${resolvedMax?.type}`);
|
|
480
|
-
if (resolvedInclusive) return {
|
|
481
|
-
type: ValueType.boolean,
|
|
482
|
-
value: resolvedValue.value >= resolvedMin.value && resolvedValue.value <= resolvedMax.value
|
|
483
|
-
};
|
|
484
|
-
return {
|
|
485
|
-
type: ValueType.boolean,
|
|
486
|
-
value: resolvedValue.value > resolvedMin.value && resolvedValue.value < resolvedMax.value
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
evaluateSum(expression) {
|
|
490
|
-
if (expression.arguments.length < 1) throw new Error(`Sum function expects at least 1 argument, got ${expression.arguments.length}`);
|
|
491
|
-
return expression.arguments.reduce((sum, arg) => {
|
|
492
|
-
if (arg === void 0) return sum;
|
|
493
|
-
const resolvedValue = this.eval(arg);
|
|
494
|
-
if (resolvedValue === void 0) return sum;
|
|
495
|
-
if (resolvedValue?.type === ValueType.number) return {
|
|
496
|
-
type: ValueType.number,
|
|
497
|
-
value: sum.value + resolvedValue.value
|
|
498
|
-
};
|
|
499
|
-
throw new Error(`Sum function expects all arguments to be numbers, got ${resolvedValue?.type}`);
|
|
500
|
-
}, {
|
|
501
|
-
type: ValueType.number,
|
|
502
|
-
value: 0
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
evaluateMin(expression) {
|
|
506
|
-
if (expression.arguments.length < 1) throw new Error(`Min function expects at least 1 argument, got ${expression.arguments.length}`);
|
|
507
|
-
return expression.arguments.reduce((min, arg) => {
|
|
508
|
-
if (arg === void 0) return min;
|
|
509
|
-
const resolvedValue = this.eval(arg);
|
|
510
|
-
if (resolvedValue === void 0) return min;
|
|
511
|
-
if (resolvedValue?.type === ValueType.number) return {
|
|
512
|
-
type: ValueType.number,
|
|
513
|
-
value: Math.min(min.value, resolvedValue.value)
|
|
514
|
-
};
|
|
515
|
-
throw new Error(`Min function expects all arguments to be numbers, got ${typeof resolvedValue}`);
|
|
516
|
-
}, {
|
|
517
|
-
type: ValueType.number,
|
|
518
|
-
value: Infinity
|
|
519
|
-
});
|
|
65
|
+
return current;
|
|
66
|
+
};
|
|
67
|
+
const getValueAtJsonPointer = (document, pointer) => {
|
|
68
|
+
const segments = parseJsonPointer(pointer);
|
|
69
|
+
return getValueAtSegments(document, segments, pointer);
|
|
70
|
+
};
|
|
71
|
+
const deepEqualJson = (left, right) => {
|
|
72
|
+
if (left === right) return true;
|
|
73
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
74
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
75
|
+
return left.every((entry, index) => deepEqualJson(entry, right[index]));
|
|
520
76
|
}
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if (resolvedValue?.type === ValueType.number) return {
|
|
528
|
-
type: ValueType.number,
|
|
529
|
-
value: Math.max(max.value, resolvedValue.value)
|
|
530
|
-
};
|
|
531
|
-
throw new Error(`Max function expects all arguments to be numbers, got ${typeof resolvedValue}`);
|
|
532
|
-
}, {
|
|
533
|
-
type: ValueType.number,
|
|
534
|
-
value: -Infinity
|
|
535
|
-
});
|
|
77
|
+
if (isRecord(left) || isRecord(right)) {
|
|
78
|
+
if (!isRecord(left) || !isRecord(right)) return false;
|
|
79
|
+
const leftKeys = Object.keys(left).sort();
|
|
80
|
+
const rightKeys = Object.keys(right).sort();
|
|
81
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
82
|
+
return leftKeys.every((key, index) => key === rightKeys[index] && deepEqualJson(left[key], right[key]));
|
|
536
83
|
}
|
|
84
|
+
return false;
|
|
537
85
|
};
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
component: false,
|
|
552
|
-
item: true,
|
|
553
|
-
survey: false
|
|
554
|
-
};
|
|
555
|
-
const ITEM_DISPLAY_DEPENDENCY_SURFACE_POLICY = {
|
|
556
|
-
"display-condition": true,
|
|
557
|
-
"disabled-condition": false,
|
|
558
|
-
validation: false,
|
|
559
|
-
"prefill-condition": false,
|
|
560
|
-
"prefill-source": false,
|
|
561
|
-
"template-value": false
|
|
562
|
-
};
|
|
563
|
-
/**
|
|
564
|
-
* Display-condition cycles are advisory because responses may be populated before visibility is
|
|
565
|
-
* evaluated, including through runtime prefills that are unavailable to static survey analysis.
|
|
566
|
-
* Concrete expression integrity failures remain blocking. This exhaustive map is the source of
|
|
567
|
-
* truth copied onto every analyzer-produced diagnostic.
|
|
568
|
-
*/
|
|
569
|
-
const SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY = {
|
|
570
|
-
"invalid-expression": {
|
|
571
|
-
category: "reference-integrity",
|
|
572
|
-
disposition: "blocking"
|
|
573
|
-
},
|
|
574
|
-
"unknown-response-ref": {
|
|
575
|
-
category: "reference-integrity",
|
|
576
|
-
disposition: "blocking"
|
|
577
|
-
},
|
|
578
|
-
"expression-reference-type-mismatch": {
|
|
579
|
-
category: "reference-integrity",
|
|
580
|
-
disposition: "blocking"
|
|
581
|
-
},
|
|
582
|
-
"unknown-reference-value": {
|
|
583
|
-
category: "reference-integrity",
|
|
584
|
-
disposition: "blocking"
|
|
585
|
-
},
|
|
586
|
-
"display-condition-cycle": {
|
|
587
|
-
category: "display-dependency",
|
|
588
|
-
disposition: "advisory"
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
const createExpressionReferenceDiagnostic = (diagnostic) => ({
|
|
592
|
-
...diagnostic,
|
|
593
|
-
...SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY[diagnostic.code]
|
|
594
|
-
});
|
|
595
|
-
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
596
|
-
const isJsonExpression = (value) => isRecord$1(value) && typeof value.type === "string";
|
|
597
|
-
const collectItemExpressionLocations = (item) => {
|
|
598
|
-
const locations = [];
|
|
599
|
-
const add = (expression, location, ownerScope, surface) => {
|
|
600
|
-
if (!isJsonExpression(expression)) return;
|
|
601
|
-
locations.push({
|
|
602
|
-
expression,
|
|
603
|
-
location,
|
|
604
|
-
ownerItemId: item.id,
|
|
605
|
-
ownerItemKey: item.key,
|
|
606
|
-
ownerScope,
|
|
607
|
-
surface
|
|
86
|
+
const applyAdd = (document, path, value) => {
|
|
87
|
+
const segments = parseJsonPointer(path);
|
|
88
|
+
if (segments.length === 0) return structuredCloneMethod(value);
|
|
89
|
+
const parentSegments = segments.slice(0, -1);
|
|
90
|
+
const key = segments[segments.length - 1];
|
|
91
|
+
const parent = getValueAtSegments(document, parentSegments, path);
|
|
92
|
+
if (Array.isArray(parent)) {
|
|
93
|
+
const index = parseArrayIndex({
|
|
94
|
+
segment: key,
|
|
95
|
+
arrayLength: parent.length,
|
|
96
|
+
allowAppend: true,
|
|
97
|
+
allowEnd: true,
|
|
98
|
+
path
|
|
608
99
|
});
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
for (const [componentId, expression] of Object.entries(item.displayConditions?.components ?? {})) add(expression, `displayConditions.components.${componentId}`, "component", "display-condition");
|
|
612
|
-
for (const [componentId, expression] of Object.entries(item.disabledConditions?.components ?? {})) add(expression, `disabledConditions.components.${componentId}`, "component", "disabled-condition");
|
|
613
|
-
for (const [validationKey, expression] of Object.entries(item.validations ?? {})) add(expression, `validations.${validationKey}`, "item", "validation");
|
|
614
|
-
for (const [prefillIndex, prefill] of (item.prefills ?? []).entries()) {
|
|
615
|
-
const prefillId = prefill.id || String(prefillIndex);
|
|
616
|
-
add(prefill.when, `prefills.${prefillId}.when`, "item", "prefill-condition");
|
|
617
|
-
if (prefill.source.type === "expression") add(prefill.source.expression, `prefills.${prefillId}.source.expression`, "item", "prefill-source");
|
|
618
|
-
}
|
|
619
|
-
return locations;
|
|
620
|
-
};
|
|
621
|
-
const collectSurveyExpressionLocations = (survey) => [...survey.surveyItems.flatMap(collectItemExpressionLocations), ...Object.entries(survey.templateValues ?? {}).flatMap(([templateKey, templateValue]) => templateValue.expression ? [{
|
|
622
|
-
expression: templateValue.expression,
|
|
623
|
-
location: `templateValues.${templateKey}.expression`,
|
|
624
|
-
ownerScope: "survey",
|
|
625
|
-
surface: "template-value"
|
|
626
|
-
}] : [])];
|
|
627
|
-
const candidateRefs = (unknownRef, availableRefs) => {
|
|
628
|
-
let parsed;
|
|
629
|
-
try {
|
|
630
|
-
parsed = new ValueReference(unknownRef);
|
|
631
|
-
} catch {
|
|
632
|
-
return availableRefs.slice(0, 5);
|
|
633
|
-
}
|
|
634
|
-
const sameItemAndMethod = availableRefs.filter((candidate) => {
|
|
635
|
-
try {
|
|
636
|
-
const candidateRef = new ValueReference(candidate);
|
|
637
|
-
return candidateRef.itemId === parsed?.itemId && candidateRef.name === parsed?.name;
|
|
638
|
-
} catch {
|
|
639
|
-
return false;
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
return (sameItemAndMethod.length > 0 ? sameItemAndMethod : availableRefs).slice(0, 5);
|
|
643
|
-
};
|
|
644
|
-
const findCyclicItemDisplayDependencyEdges = ({ edges, itemIds }) => {
|
|
645
|
-
const adjacency = new Map(itemIds.map((itemId) => [itemId, /* @__PURE__ */ new Set()]));
|
|
646
|
-
const reverseAdjacency = new Map(itemIds.map((itemId) => [itemId, /* @__PURE__ */ new Set()]));
|
|
647
|
-
for (const edge of edges) {
|
|
648
|
-
adjacency.get(edge.fromItemId)?.add(edge.toItemId);
|
|
649
|
-
reverseAdjacency.get(edge.toItemId)?.add(edge.fromItemId);
|
|
650
|
-
}
|
|
651
|
-
const finishOrder = [];
|
|
652
|
-
const visited = /* @__PURE__ */ new Set();
|
|
653
|
-
for (const startItemId of itemIds) {
|
|
654
|
-
if (visited.has(startItemId)) continue;
|
|
655
|
-
const stack = [{
|
|
656
|
-
expanded: false,
|
|
657
|
-
itemId: startItemId
|
|
658
|
-
}];
|
|
659
|
-
while (stack.length > 0) {
|
|
660
|
-
const current = stack.pop();
|
|
661
|
-
if (!current) break;
|
|
662
|
-
if (current.expanded) {
|
|
663
|
-
finishOrder.push(current.itemId);
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
if (visited.has(current.itemId)) continue;
|
|
667
|
-
visited.add(current.itemId);
|
|
668
|
-
stack.push({
|
|
669
|
-
expanded: true,
|
|
670
|
-
itemId: current.itemId
|
|
671
|
-
});
|
|
672
|
-
const dependencies = [...adjacency.get(current.itemId) ?? []];
|
|
673
|
-
for (let index = dependencies.length - 1; index >= 0; index -= 1) {
|
|
674
|
-
const dependencyId = dependencies[index];
|
|
675
|
-
if (visited.has(dependencyId)) continue;
|
|
676
|
-
stack.push({
|
|
677
|
-
expanded: false,
|
|
678
|
-
itemId: dependencyId
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
}
|
|
100
|
+
parent.splice(index, 0, structuredCloneMethod(value));
|
|
101
|
+
return document;
|
|
682
102
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const stack = [startItemId];
|
|
690
|
-
componentByItemId.set(startItemId, componentId);
|
|
691
|
-
while (stack.length > 0) {
|
|
692
|
-
const itemId = stack.pop();
|
|
693
|
-
if (!itemId) break;
|
|
694
|
-
members.push(itemId);
|
|
695
|
-
for (const dependencyOwnerId of reverseAdjacency.get(itemId) ?? []) {
|
|
696
|
-
if (componentByItemId.has(dependencyOwnerId)) continue;
|
|
697
|
-
componentByItemId.set(dependencyOwnerId, componentId);
|
|
698
|
-
stack.push(dependencyOwnerId);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
componentMembers.set(componentId, members.sort());
|
|
702
|
-
}
|
|
703
|
-
return edges.flatMap((edge) => {
|
|
704
|
-
const componentId = componentByItemId.get(edge.fromItemId);
|
|
705
|
-
if (componentId === void 0 || componentId !== componentByItemId.get(edge.toItemId)) return [];
|
|
706
|
-
const cycleItemIds = componentMembers.get(componentId) ?? [];
|
|
707
|
-
if (edge.fromItemId !== edge.toItemId && cycleItemIds.length < 2) return [];
|
|
708
|
-
return [{
|
|
709
|
-
edge,
|
|
710
|
-
cycleItemIds
|
|
711
|
-
}];
|
|
103
|
+
if (!isRecord(parent)) throw new JsonPatchError(`JSON pointer "${path}" parent is not an object or array.`, "invalid-target-parent", path);
|
|
104
|
+
Object.defineProperty(parent, key, {
|
|
105
|
+
configurable: true,
|
|
106
|
+
enumerable: true,
|
|
107
|
+
value: structuredCloneMethod(value),
|
|
108
|
+
writable: true
|
|
712
109
|
});
|
|
110
|
+
return document;
|
|
713
111
|
};
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
if (
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
case "lte":
|
|
728
|
-
case "in_range":
|
|
729
|
-
case "list_contains":
|
|
730
|
-
case "str_eq":
|
|
731
|
-
case "date_eq": return ValueType.boolean;
|
|
732
|
-
case "sum":
|
|
733
|
-
case "min":
|
|
734
|
-
case "max": return ValueType.number;
|
|
735
|
-
}
|
|
736
|
-
};
|
|
737
|
-
const itemTypeForList = (type) => {
|
|
738
|
-
switch (type) {
|
|
739
|
-
case ValueType.stringArray: return ValueType.string;
|
|
740
|
-
case ValueType.referenceArray: return ValueType.reference;
|
|
741
|
-
case ValueType.numberArray: return ValueType.number;
|
|
742
|
-
case ValueType.dateArray: return ValueType.date;
|
|
743
|
-
case ValueType.durationArray: return ValueType.duration;
|
|
744
|
-
default: return;
|
|
745
|
-
}
|
|
746
|
-
};
|
|
747
|
-
const inspectFunctionReferenceCompatibility = ({ diagnostics, expression, location, slots }) => {
|
|
748
|
-
if (expression.type !== ExpressionType.Function) return;
|
|
749
|
-
expression.arguments.forEach((argument) => {
|
|
750
|
-
if (argument) inspectFunctionReferenceCompatibility({
|
|
751
|
-
diagnostics,
|
|
752
|
-
expression: argument,
|
|
753
|
-
location,
|
|
754
|
-
slots
|
|
755
|
-
});
|
|
756
|
-
});
|
|
757
|
-
const [left, right] = expression.arguments;
|
|
758
|
-
const leftType = inferExpressionType(left, slots);
|
|
759
|
-
const rightType = inferExpressionType(right, slots);
|
|
760
|
-
if (expression.functionName === "list_contains" ? Boolean(leftType && rightType && itemTypeForList(leftType) !== rightType) : expression.functionName === "eq" ? Boolean(leftType && rightType && leftType !== rightType) : false) diagnostics.push({
|
|
761
|
-
code: "expression-reference-type-mismatch",
|
|
762
|
-
message: `${location.location} compares incompatible values (${leftType} and ${rightType}).`,
|
|
763
|
-
location: location.location,
|
|
764
|
-
ownerItemId: location.ownerItemId,
|
|
765
|
-
ownerItemKey: location.ownerItemKey
|
|
766
|
-
});
|
|
767
|
-
if (expression.functionName !== "eq" && expression.functionName !== "list_contains") return;
|
|
768
|
-
const pairs = [[left, right], [right, left]];
|
|
769
|
-
for (const [variable, constant] of pairs) {
|
|
770
|
-
if (variable?.type !== ExpressionType.ResponseVariable || constant?.type !== ExpressionType.Const || constant.value?.type !== ValueType.reference) continue;
|
|
771
|
-
const slot = slots.get(variable.variableRef);
|
|
772
|
-
if (slot?.allowedReferenceValues && !slot.allowedReferenceValues.includes(constant.value.value)) diagnostics.push({
|
|
773
|
-
code: "unknown-reference-value",
|
|
774
|
-
message: `${location.location} compares "${variable.variableRef}" with missing option "${constant.value.value}".`,
|
|
775
|
-
location: location.location,
|
|
776
|
-
ownerItemId: location.ownerItemId,
|
|
777
|
-
ownerItemKey: location.ownerItemKey,
|
|
778
|
-
reference: variable.variableRef,
|
|
779
|
-
referenceValue: constant.value.value,
|
|
780
|
-
candidates: slot.allowedReferenceValues.slice(0, 5)
|
|
112
|
+
const applyRemove = (document, path) => {
|
|
113
|
+
const segments = parseJsonPointer(path);
|
|
114
|
+
if (segments.length === 0) throw new JsonPatchError("Removing the document root is not supported.", "remove-root", path);
|
|
115
|
+
const parentSegments = segments.slice(0, -1);
|
|
116
|
+
const key = segments[segments.length - 1];
|
|
117
|
+
const parent = getValueAtSegments(document, parentSegments, path);
|
|
118
|
+
if (Array.isArray(parent)) {
|
|
119
|
+
const index = parseArrayIndex({
|
|
120
|
+
segment: key,
|
|
121
|
+
arrayLength: parent.length,
|
|
122
|
+
allowAppend: false,
|
|
123
|
+
allowEnd: false,
|
|
124
|
+
path
|
|
781
125
|
});
|
|
126
|
+
const [removedValue] = parent.splice(index, 1);
|
|
127
|
+
return {
|
|
128
|
+
document,
|
|
129
|
+
removedValue
|
|
130
|
+
};
|
|
782
131
|
}
|
|
132
|
+
if (!isRecord(parent) || !hasOwn(parent, key)) throw new JsonPatchError(`JSON pointer "${path}" does not exist.`, "path-not-found", path);
|
|
133
|
+
const removedValue = parent[key];
|
|
134
|
+
delete parent[key];
|
|
135
|
+
return {
|
|
136
|
+
document,
|
|
137
|
+
removedValue
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
const applyReplace = (document, path, value) => {
|
|
141
|
+
const segments = parseJsonPointer(path);
|
|
142
|
+
if (segments.length === 0) return structuredCloneMethod(value);
|
|
143
|
+
getValueAtSegments(document, segments, path);
|
|
144
|
+
applyRemove(document, path);
|
|
145
|
+
return applyAdd(document, path, value);
|
|
146
|
+
};
|
|
147
|
+
const assertMoveTargetIsNotDescendant = (from, path) => {
|
|
148
|
+
if (from === "" || path === from || !path.startsWith(`${from}/`)) return;
|
|
149
|
+
throw new JsonPatchError(`JSON Patch cannot move "${from}" into its own descendant "${path}".`, "move-into-descendant", path);
|
|
783
150
|
};
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const diagnostics = [];
|
|
788
|
-
const surveyItemIds = survey.surveyItems.map((item) => item.id);
|
|
789
|
-
const surveyItemIdSet = new Set(surveyItemIds);
|
|
790
|
-
const itemDisplayDependencyEdges = [];
|
|
791
|
-
for (const location of collectSurveyExpressionLocations(survey)) {
|
|
792
|
-
let parsed;
|
|
151
|
+
const applyJsonPatch = (document, operations) => {
|
|
152
|
+
let patchedDocument = structuredCloneMethod(document);
|
|
153
|
+
operations.forEach((operation, operationIndex) => {
|
|
793
154
|
try {
|
|
794
|
-
|
|
155
|
+
switch (operation.op) {
|
|
156
|
+
case "add":
|
|
157
|
+
patchedDocument = applyAdd(patchedDocument, operation.path, operation.value);
|
|
158
|
+
break;
|
|
159
|
+
case "remove":
|
|
160
|
+
patchedDocument = applyRemove(patchedDocument, operation.path).document;
|
|
161
|
+
break;
|
|
162
|
+
case "replace":
|
|
163
|
+
patchedDocument = applyReplace(patchedDocument, operation.path, operation.value);
|
|
164
|
+
break;
|
|
165
|
+
case "move": {
|
|
166
|
+
assertMoveTargetIsNotDescendant(operation.from, operation.path);
|
|
167
|
+
if (operation.from === operation.path) break;
|
|
168
|
+
const { document: withoutValue, removedValue } = applyRemove(patchedDocument, operation.from);
|
|
169
|
+
patchedDocument = applyAdd(withoutValue, operation.path, removedValue);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case "copy": {
|
|
173
|
+
const value = getValueAtJsonPointer(patchedDocument, operation.from);
|
|
174
|
+
patchedDocument = applyAdd(patchedDocument, operation.path, value);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case "test": {
|
|
178
|
+
const value = getValueAtJsonPointer(patchedDocument, operation.path);
|
|
179
|
+
if (!deepEqualJson(value, operation.value)) throw new JsonPatchError(`JSON Patch test failed at "${operation.path}".`, "test-failed", operation.path);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
795
183
|
} catch (error) {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
message: `${location.location} contains invalid expression JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
799
|
-
location: location.location,
|
|
800
|
-
ownerItemId: location.ownerItemId,
|
|
801
|
-
ownerItemKey: location.ownerItemKey
|
|
802
|
-
});
|
|
803
|
-
continue;
|
|
804
|
-
}
|
|
805
|
-
for (const ref of parsed?.responseVariableRefs ?? []) {
|
|
806
|
-
const reference = ref.toString();
|
|
807
|
-
if (location.ownerItemId && ITEM_DISPLAY_DEPENDENCY_OWNER_SCOPE_POLICY[location.ownerScope] && ITEM_DISPLAY_DEPENDENCY_SURFACE_POLICY[location.surface] && surveyItemIdSet.has(ref.itemId)) itemDisplayDependencyEdges.push({
|
|
808
|
-
fromItemId: location.ownerItemId,
|
|
809
|
-
location,
|
|
810
|
-
reference,
|
|
811
|
-
toItemId: ref.itemId
|
|
812
|
-
});
|
|
813
|
-
if (!slots.has(reference)) diagnostics.push({
|
|
814
|
-
code: "unknown-response-ref",
|
|
815
|
-
message: `${location.location} references unknown response slot "${reference}".`,
|
|
816
|
-
location: location.location,
|
|
817
|
-
ownerItemId: location.ownerItemId,
|
|
818
|
-
ownerItemKey: location.ownerItemKey,
|
|
819
|
-
reference,
|
|
820
|
-
candidates: candidateRefs(reference, availableRefs)
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
inspectFunctionReferenceCompatibility({
|
|
824
|
-
diagnostics,
|
|
825
|
-
expression: location.expression,
|
|
826
|
-
location,
|
|
827
|
-
slots
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
for (const { cycleItemIds, edge } of findCyclicItemDisplayDependencyEdges({
|
|
831
|
-
edges: itemDisplayDependencyEdges,
|
|
832
|
-
itemIds: surveyItemIds
|
|
833
|
-
})) {
|
|
834
|
-
const isSelfReference = edge.fromItemId === edge.toItemId;
|
|
835
|
-
diagnostics.push({
|
|
836
|
-
code: "display-condition-cycle",
|
|
837
|
-
message: isSelfReference ? `${edge.location.location} for item "${edge.fromItemId}" references its own response "${edge.reference}", creating a circular display dependency.` : `${edge.location.location} for item "${edge.fromItemId}" references item "${edge.toItemId}" within a circular display dependency among ${cycleItemIds.map((itemId) => `"${itemId}"`).join(", ")}.`,
|
|
838
|
-
location: edge.location.location,
|
|
839
|
-
ownerItemId: edge.location.ownerItemId,
|
|
840
|
-
ownerItemKey: edge.location.ownerItemKey,
|
|
841
|
-
reference: edge.reference
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
return diagnostics.map(createExpressionReferenceDiagnostic);
|
|
845
|
-
};
|
|
846
|
-
const expressionReferenceDiagnosticKey = (diagnostic) => [
|
|
847
|
-
diagnostic.code,
|
|
848
|
-
diagnostic.ownerItemId ?? "survey",
|
|
849
|
-
diagnostic.location,
|
|
850
|
-
diagnostic.reference ?? "",
|
|
851
|
-
diagnostic.referenceValue ?? ""
|
|
852
|
-
].join("\0");
|
|
853
|
-
const findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories = ({ after, before }) => {
|
|
854
|
-
const existing = new Set(before.map(expressionReferenceDiagnosticKey));
|
|
855
|
-
return after.filter((diagnostic) => !existing.has(expressionReferenceDiagnosticKey(diagnostic)));
|
|
856
|
-
};
|
|
857
|
-
const isBlockingSurveyExpressionReferenceDiagnostic = (diagnostic) => {
|
|
858
|
-
switch (diagnostic.disposition) {
|
|
859
|
-
case "blocking": return true;
|
|
860
|
-
case "advisory": return false;
|
|
861
|
-
default: return true;
|
|
862
|
-
}
|
|
863
|
-
};
|
|
864
|
-
const findIntroducedBlockingExpressionReferenceDiagnostics = ({ after, before }) => findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories({
|
|
865
|
-
after,
|
|
866
|
-
before
|
|
867
|
-
}).filter(isBlockingSurveyExpressionReferenceDiagnostic);
|
|
868
|
-
const replaceResponseVariableReferences = (value, replacements) => {
|
|
869
|
-
let changed = false;
|
|
870
|
-
const visit = (current) => {
|
|
871
|
-
if (Array.isArray(current)) return current.map(visit);
|
|
872
|
-
if (!isRecord$1(current)) return current;
|
|
873
|
-
const next = Object.fromEntries(Object.entries(current).map(([key, entry]) => [key, visit(entry)]));
|
|
874
|
-
if (next.type === ExpressionType.ResponseVariable && typeof next.variableRef === "string" && replacements[next.variableRef]) {
|
|
875
|
-
next.variableRef = replacements[next.variableRef];
|
|
876
|
-
changed = true;
|
|
184
|
+
if (error instanceof JsonPatchError) throw new JsonPatchError(error.message, error.code, error.path, operationIndex);
|
|
185
|
+
throw error;
|
|
877
186
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
187
|
+
});
|
|
188
|
+
return patchedDocument;
|
|
189
|
+
};
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/expressions/shortcuts/selection.ts
|
|
192
|
+
/**
|
|
193
|
+
* Build a selected-option test joined with `not(ownerState.disabled)` for that option.
|
|
194
|
+
* `multiple` chooses membership instead of equality. The result is an ordinary expression tree;
|
|
195
|
+
* no shortcut node is persisted or handled specially by the evaluator.
|
|
196
|
+
*/
|
|
197
|
+
function selectedAndEnabled(selection, optionId, multiple) {
|
|
881
198
|
return {
|
|
882
|
-
|
|
883
|
-
|
|
199
|
+
type: "function",
|
|
200
|
+
functionName: "and",
|
|
201
|
+
arguments: [{
|
|
202
|
+
type: "function",
|
|
203
|
+
functionName: multiple ? "list_contains" : "eq",
|
|
204
|
+
arguments: [selection, {
|
|
205
|
+
type: "const",
|
|
206
|
+
value: {
|
|
207
|
+
type: "reference",
|
|
208
|
+
value: optionId
|
|
209
|
+
}
|
|
210
|
+
}]
|
|
211
|
+
}, {
|
|
212
|
+
type: "function",
|
|
213
|
+
functionName: "not",
|
|
214
|
+
arguments: [{
|
|
215
|
+
type: "ownerState",
|
|
216
|
+
state: "disabled",
|
|
217
|
+
targetRef: { ownerId: optionId }
|
|
218
|
+
}]
|
|
219
|
+
}]
|
|
884
220
|
};
|
|
885
|
-
}
|
|
886
|
-
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/survey/utils/group-utils.ts
|
|
224
|
+
/**
|
|
225
|
+
* Shared utilities for array operations.
|
|
226
|
+
*/
|
|
227
|
+
/**
|
|
228
|
+
* Swap two elements in an array by index.
|
|
229
|
+
* @returns A new array with the elements swapped
|
|
230
|
+
*/
|
|
231
|
+
function swapArrayElements(arr, fromIndex, toIndex) {
|
|
232
|
+
const result = [...arr];
|
|
233
|
+
[result[fromIndex], result[toIndex]] = [result[toIndex], result[fromIndex]];
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Move an element from one index to another.
|
|
238
|
+
* @returns A new array with the element moved
|
|
239
|
+
*/
|
|
240
|
+
function moveArrayElement(arr, fromIndex, toIndex) {
|
|
241
|
+
const result = [...arr];
|
|
242
|
+
const [moved] = result.splice(fromIndex, 1);
|
|
243
|
+
result.splice(toIndex, 0, moved);
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
887
246
|
//#endregion
|
|
888
247
|
//#region src/expressions/editors/expression-editor.ts
|
|
248
|
+
const constantNode = (value, editorConfig) => {
|
|
249
|
+
const node = expressionSchema.parse({
|
|
250
|
+
type: "const",
|
|
251
|
+
value,
|
|
252
|
+
editorConfig
|
|
253
|
+
});
|
|
254
|
+
if (node.type !== "const") throw new Error("Expected constant node");
|
|
255
|
+
return node;
|
|
256
|
+
};
|
|
257
|
+
/** Editor-state builders produce the canonical JSON tree, never runtime wrappers. */
|
|
258
|
+
const createFunctionExpression = (functionName, args, editorConfig) => ({
|
|
259
|
+
type: "function",
|
|
260
|
+
functionName,
|
|
261
|
+
arguments: expressionArguments(args),
|
|
262
|
+
editorConfig
|
|
263
|
+
});
|
|
264
|
+
const contextNode = (contextType, key, args, asType, editorConfig) => ({
|
|
265
|
+
type: "contextVariable",
|
|
266
|
+
contextType,
|
|
267
|
+
...key ? { key } : {},
|
|
268
|
+
...args ? { arguments: expressionArguments(args) } : {},
|
|
269
|
+
asType,
|
|
270
|
+
editorConfig
|
|
271
|
+
});
|
|
889
272
|
var ExpressionEditor = class {
|
|
890
273
|
returnType;
|
|
891
274
|
_editorConfig;
|
|
@@ -912,7 +295,7 @@ var ConstStringArrayEditor = class extends ExpressionEditor {
|
|
|
912
295
|
this._values = values;
|
|
913
296
|
}
|
|
914
297
|
getExpression() {
|
|
915
|
-
return
|
|
298
|
+
return constantNode({
|
|
916
299
|
type: ValueType.stringArray,
|
|
917
300
|
value: this._values
|
|
918
301
|
}, this._editorConfig);
|
|
@@ -933,7 +316,7 @@ var ConstStringEditor = class extends ExpressionEditor {
|
|
|
933
316
|
this._value = value;
|
|
934
317
|
}
|
|
935
318
|
getExpression() {
|
|
936
|
-
return
|
|
319
|
+
return constantNode({
|
|
937
320
|
type: ValueType.string,
|
|
938
321
|
value: this._value
|
|
939
322
|
}, this._editorConfig);
|
|
@@ -954,7 +337,7 @@ var ConstNumberEditor = class extends ExpressionEditor {
|
|
|
954
337
|
this._value = value;
|
|
955
338
|
}
|
|
956
339
|
getExpression() {
|
|
957
|
-
return
|
|
340
|
+
return constantNode({
|
|
958
341
|
type: ValueType.number,
|
|
959
342
|
value: this._value
|
|
960
343
|
}, this._editorConfig);
|
|
@@ -975,7 +358,7 @@ var ConstBooleanEditor = class extends ExpressionEditor {
|
|
|
975
358
|
this._value = value;
|
|
976
359
|
}
|
|
977
360
|
getExpression() {
|
|
978
|
-
return
|
|
361
|
+
return constantNode({
|
|
979
362
|
type: ValueType.boolean,
|
|
980
363
|
value: this._value
|
|
981
364
|
}, this._editorConfig);
|
|
@@ -996,8 +379,8 @@ var ConstDateEditor = class extends ExpressionEditor {
|
|
|
996
379
|
this._value = value;
|
|
997
380
|
}
|
|
998
381
|
getExpression() {
|
|
999
|
-
const value =
|
|
1000
|
-
return
|
|
382
|
+
const value = this._value;
|
|
383
|
+
return constantNode({
|
|
1001
384
|
type: ValueType.date,
|
|
1002
385
|
value
|
|
1003
386
|
}, this._editorConfig);
|
|
@@ -1018,7 +401,7 @@ var ConstNumberArrayEditor = class extends ExpressionEditor {
|
|
|
1018
401
|
this._values = values;
|
|
1019
402
|
}
|
|
1020
403
|
getExpression() {
|
|
1021
|
-
return
|
|
404
|
+
return constantNode({
|
|
1022
405
|
type: ValueType.numberArray,
|
|
1023
406
|
value: this._values
|
|
1024
407
|
}, this._editorConfig);
|
|
@@ -1039,31 +422,44 @@ var ConstDateArrayEditor = class extends ExpressionEditor {
|
|
|
1039
422
|
this._values = values;
|
|
1040
423
|
}
|
|
1041
424
|
getExpression() {
|
|
1042
|
-
const value = this._values
|
|
1043
|
-
return
|
|
425
|
+
const value = this._values;
|
|
426
|
+
return constantNode({
|
|
1044
427
|
type: ValueType.dateArray,
|
|
1045
428
|
value
|
|
1046
429
|
}, this._editorConfig);
|
|
1047
430
|
}
|
|
1048
431
|
};
|
|
1049
432
|
var ResponseVariableEditor = class extends ExpressionEditor {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
constructor(variableName, variableType, editorConfig) {
|
|
433
|
+
variableRef;
|
|
434
|
+
constructor(variableRef, variableType, editorConfig) {
|
|
1053
435
|
super();
|
|
1054
|
-
this.
|
|
1055
|
-
this._variableRef = new ValueReference(variableName);
|
|
436
|
+
this.variableRef = variableRef;
|
|
1056
437
|
this._editorConfig = editorConfig;
|
|
1057
|
-
this.returnType = variableType;
|
|
438
|
+
this.returnType = variableRef.method === "isDefined" ? ValueType.boolean : variableType;
|
|
1058
439
|
}
|
|
1059
|
-
|
|
1060
|
-
return
|
|
440
|
+
getExpression() {
|
|
441
|
+
return {
|
|
442
|
+
type: "responseVariable",
|
|
443
|
+
variableRef: { ...this.variableRef },
|
|
444
|
+
editorConfig: this._editorConfig
|
|
445
|
+
};
|
|
1061
446
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
447
|
+
};
|
|
448
|
+
var DisabledStateEditor = class extends ExpressionEditor {
|
|
449
|
+
ownerId;
|
|
450
|
+
returnType = ValueType.boolean;
|
|
451
|
+
constructor(ownerId, editorConfig) {
|
|
452
|
+
super();
|
|
453
|
+
this.ownerId = ownerId;
|
|
454
|
+
this._editorConfig = editorConfig;
|
|
1064
455
|
}
|
|
1065
456
|
getExpression() {
|
|
1066
|
-
return
|
|
457
|
+
return {
|
|
458
|
+
type: "ownerState",
|
|
459
|
+
state: "disabled",
|
|
460
|
+
targetRef: { ownerId: this.ownerId },
|
|
461
|
+
editorConfig: this._editorConfig
|
|
462
|
+
};
|
|
1067
463
|
}
|
|
1068
464
|
};
|
|
1069
465
|
var ContextVariableEditor = class extends ExpressionEditor {
|
|
@@ -1080,48 +476,48 @@ var ContextVariableEditor = class extends ExpressionEditor {
|
|
|
1080
476
|
this._editorConfig = editorConfig;
|
|
1081
477
|
}
|
|
1082
478
|
getExpression() {
|
|
1083
|
-
return
|
|
479
|
+
return contextNode(this._contextType, this._key?.getExpression(), this._args?.map((arg) => arg.getExpression()), this._asType, this._editorConfig);
|
|
1084
480
|
}
|
|
1085
481
|
};
|
|
1086
482
|
var CtxLocaleEditor = class extends ContextVariableEditor {
|
|
1087
483
|
returnType = ValueType.string;
|
|
1088
484
|
constructor(editorConfig) {
|
|
1089
|
-
super(
|
|
485
|
+
super("locale", void 0, void 0, ValueType.string, editorConfig);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var CtxCurrentDateEditor = class extends ContextVariableEditor {
|
|
489
|
+
returnType = ValueType.date;
|
|
490
|
+
constructor(editorConfig) {
|
|
491
|
+
super("currentDate", void 0, void 0, ValueType.date, editorConfig);
|
|
1090
492
|
}
|
|
1091
493
|
};
|
|
1092
494
|
var CtxPFlagIsDefinedEditor = class extends ContextVariableEditor {
|
|
1093
495
|
returnType = ValueType.boolean;
|
|
1094
496
|
constructor(key, editorConfig) {
|
|
1095
|
-
super(
|
|
497
|
+
super("participantFlag", key, void 0, ValueType.boolean, editorConfig);
|
|
1096
498
|
}
|
|
1097
499
|
};
|
|
1098
500
|
var CtxPFlagStringEditor = class extends ContextVariableEditor {
|
|
1099
501
|
returnType = ValueType.string;
|
|
1100
502
|
constructor(key, editorConfig) {
|
|
1101
|
-
super(
|
|
503
|
+
super("participantFlag", key, void 0, ValueType.string, editorConfig);
|
|
1102
504
|
}
|
|
1103
505
|
};
|
|
1104
506
|
var CtxPFlagNumEditor = class extends ContextVariableEditor {
|
|
1105
507
|
returnType = ValueType.number;
|
|
1106
508
|
constructor(key, editorConfig) {
|
|
1107
|
-
super(
|
|
509
|
+
super("participantFlag", key, void 0, ValueType.number, editorConfig);
|
|
1108
510
|
}
|
|
1109
511
|
};
|
|
1110
512
|
var CtxPFlagDateEditor = class extends ContextVariableEditor {
|
|
1111
513
|
returnType = ValueType.date;
|
|
1112
514
|
constructor(key, editorConfig) {
|
|
1113
|
-
super(
|
|
515
|
+
super("participantFlag", key, void 0, ValueType.date, editorConfig);
|
|
1114
516
|
}
|
|
1115
517
|
};
|
|
1116
518
|
var CtxCustomValueEditor = class extends ContextVariableEditor {
|
|
1117
519
|
constructor(key, asType, editorConfig) {
|
|
1118
|
-
super(
|
|
1119
|
-
this.returnType = asType;
|
|
1120
|
-
}
|
|
1121
|
-
};
|
|
1122
|
-
var CtxCustomExpressionEditor = class extends ContextVariableEditor {
|
|
1123
|
-
constructor(key, args, asType, editorConfig) {
|
|
1124
|
-
super(ContextVariableType.CustomExpression, key, args, asType, editorConfig);
|
|
520
|
+
super("customValue", key, void 0, asType, editorConfig);
|
|
1125
521
|
this.returnType = asType;
|
|
1126
522
|
}
|
|
1127
523
|
};
|
|
@@ -1146,10 +542,7 @@ var GroupExpressionEditor = class extends ExpressionEditor {
|
|
|
1146
542
|
this._args[position] = arg;
|
|
1147
543
|
}
|
|
1148
544
|
swapArgs(activeIndex, overIndex) {
|
|
1149
|
-
|
|
1150
|
-
newOrder.splice(activeIndex, 1);
|
|
1151
|
-
newOrder.splice(overIndex, 0, this._args[activeIndex]);
|
|
1152
|
-
this._args = newOrder;
|
|
545
|
+
this._args = moveArrayElement(this._args, activeIndex, overIndex);
|
|
1153
546
|
}
|
|
1154
547
|
};
|
|
1155
548
|
var AndExpressionEditor = class extends GroupExpressionEditor {
|
|
@@ -1159,7 +552,7 @@ var AndExpressionEditor = class extends GroupExpressionEditor {
|
|
|
1159
552
|
super(args, editorConfig);
|
|
1160
553
|
}
|
|
1161
554
|
getExpression() {
|
|
1162
|
-
return
|
|
555
|
+
return createFunctionExpression("and", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
1163
556
|
}
|
|
1164
557
|
};
|
|
1165
558
|
var OrExpressionEditor = class extends GroupExpressionEditor {
|
|
@@ -1169,7 +562,7 @@ var OrExpressionEditor = class extends GroupExpressionEditor {
|
|
|
1169
562
|
super(args, editorConfig);
|
|
1170
563
|
}
|
|
1171
564
|
getExpression() {
|
|
1172
|
-
return
|
|
565
|
+
return createFunctionExpression("or", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
1173
566
|
}
|
|
1174
567
|
};
|
|
1175
568
|
var StrListContainsExpressionEditor = class extends ExpressionEditor {
|
|
@@ -1197,7 +590,7 @@ var StrListContainsExpressionEditor = class extends ExpressionEditor {
|
|
|
1197
590
|
this._item = item;
|
|
1198
591
|
}
|
|
1199
592
|
getExpression() {
|
|
1200
|
-
return
|
|
593
|
+
return createFunctionExpression("list_contains", [this._list?.getExpression(), this._item?.getExpression()], this._editorConfig);
|
|
1201
594
|
}
|
|
1202
595
|
};
|
|
1203
596
|
var StrEqExpressionEditor = class extends ExpressionEditor {
|
|
@@ -1223,7 +616,7 @@ var StrEqExpressionEditor = class extends ExpressionEditor {
|
|
|
1223
616
|
this._b = b;
|
|
1224
617
|
}
|
|
1225
618
|
getExpression() {
|
|
1226
|
-
return
|
|
619
|
+
return createFunctionExpression("str_eq", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
1227
620
|
}
|
|
1228
621
|
};
|
|
1229
622
|
var EqExpressionEditor = class extends ExpressionEditor {
|
|
@@ -1249,7 +642,7 @@ var EqExpressionEditor = class extends ExpressionEditor {
|
|
|
1249
642
|
this._b = b;
|
|
1250
643
|
}
|
|
1251
644
|
getExpression() {
|
|
1252
|
-
return
|
|
645
|
+
return createFunctionExpression("eq", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
1253
646
|
}
|
|
1254
647
|
};
|
|
1255
648
|
var GtExpressionEditor = class extends ExpressionEditor {
|
|
@@ -1275,7 +668,7 @@ var GtExpressionEditor = class extends ExpressionEditor {
|
|
|
1275
668
|
this._b = b;
|
|
1276
669
|
}
|
|
1277
670
|
getExpression() {
|
|
1278
|
-
return
|
|
671
|
+
return createFunctionExpression("gt", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
1279
672
|
}
|
|
1280
673
|
};
|
|
1281
674
|
var GteExpressionEditor = class extends ExpressionEditor {
|
|
@@ -1301,7 +694,7 @@ var GteExpressionEditor = class extends ExpressionEditor {
|
|
|
1301
694
|
this._b = b;
|
|
1302
695
|
}
|
|
1303
696
|
getExpression() {
|
|
1304
|
-
return
|
|
697
|
+
return createFunctionExpression("gte", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
1305
698
|
}
|
|
1306
699
|
};
|
|
1307
700
|
var LtExpressionEditor = class extends ExpressionEditor {
|
|
@@ -1327,7 +720,7 @@ var LtExpressionEditor = class extends ExpressionEditor {
|
|
|
1327
720
|
this._b = b;
|
|
1328
721
|
}
|
|
1329
722
|
getExpression() {
|
|
1330
|
-
return
|
|
723
|
+
return createFunctionExpression("lt", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
1331
724
|
}
|
|
1332
725
|
};
|
|
1333
726
|
var LteExpressionEditor = class extends ExpressionEditor {
|
|
@@ -1353,7 +746,7 @@ var LteExpressionEditor = class extends ExpressionEditor {
|
|
|
1353
746
|
this._a = a;
|
|
1354
747
|
}
|
|
1355
748
|
getExpression() {
|
|
1356
|
-
return
|
|
749
|
+
return createFunctionExpression("lte", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
1357
750
|
}
|
|
1358
751
|
};
|
|
1359
752
|
var InRangeExpressionEditor = class extends ExpressionEditor {
|
|
@@ -1371,7 +764,7 @@ var InRangeExpressionEditor = class extends ExpressionEditor {
|
|
|
1371
764
|
this._editorConfig = editorConfig;
|
|
1372
765
|
}
|
|
1373
766
|
getExpression() {
|
|
1374
|
-
return
|
|
767
|
+
return createFunctionExpression("in_range", [
|
|
1375
768
|
this.value?.getExpression(),
|
|
1376
769
|
this.min?.getExpression(),
|
|
1377
770
|
this.max?.getExpression(),
|
|
@@ -1385,7 +778,7 @@ var SumExpressionEditor = class extends GroupExpressionEditor {
|
|
|
1385
778
|
super(args, editorConfig);
|
|
1386
779
|
}
|
|
1387
780
|
getExpression() {
|
|
1388
|
-
return
|
|
781
|
+
return createFunctionExpression("sum", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
1389
782
|
}
|
|
1390
783
|
};
|
|
1391
784
|
var MinExpressionEditor = class extends GroupExpressionEditor {
|
|
@@ -1394,7 +787,7 @@ var MinExpressionEditor = class extends GroupExpressionEditor {
|
|
|
1394
787
|
super(args, editorConfig);
|
|
1395
788
|
}
|
|
1396
789
|
getExpression() {
|
|
1397
|
-
return
|
|
790
|
+
return createFunctionExpression("min", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
1398
791
|
}
|
|
1399
792
|
};
|
|
1400
793
|
var MaxExpressionEditor = class extends GroupExpressionEditor {
|
|
@@ -1403,7 +796,7 @@ var MaxExpressionEditor = class extends GroupExpressionEditor {
|
|
|
1403
796
|
super(args, editorConfig);
|
|
1404
797
|
}
|
|
1405
798
|
getExpression() {
|
|
1406
|
-
return
|
|
799
|
+
return createFunctionExpression("max", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
1407
800
|
}
|
|
1408
801
|
};
|
|
1409
802
|
//#endregion
|
|
@@ -1453,6 +846,9 @@ const response_date_array = (valueRef) => {
|
|
|
1453
846
|
const ctx_locale = () => {
|
|
1454
847
|
return new CtxLocaleEditor();
|
|
1455
848
|
};
|
|
849
|
+
const ctx_current_date = () => {
|
|
850
|
+
return new CtxCurrentDateEditor();
|
|
851
|
+
};
|
|
1456
852
|
const ctx_pflag_is_defined = (key) => {
|
|
1457
853
|
return new CtxPFlagIsDefinedEditor(key);
|
|
1458
854
|
};
|
|
@@ -1468,9 +864,6 @@ const ctx_pflag_date = (key) => {
|
|
|
1468
864
|
const ctx_custom_value = (key, dType) => {
|
|
1469
865
|
return new CtxCustomValueEditor(key, dType);
|
|
1470
866
|
};
|
|
1471
|
-
const ctx_custom_expression = (key, args, returnType) => {
|
|
1472
|
-
return new CtxCustomExpressionEditor(key, args, returnType);
|
|
1473
|
-
};
|
|
1474
867
|
const and = (...args) => {
|
|
1475
868
|
return new AndExpressionEditor(args);
|
|
1476
869
|
};
|
|
@@ -1480,6 +873,14 @@ const or = (...args) => {
|
|
|
1480
873
|
const str_list_contains = (list, item) => {
|
|
1481
874
|
return new StrListContainsExpressionEditor(list, item);
|
|
1482
875
|
};
|
|
876
|
+
/** Number of list entries, or of entries that also appear in `filter` (e.g. listed options). */
|
|
877
|
+
const list_count = (list, filter) => new FunctionCallEditor("list_count", filter ? [list, filter] : [list], ValueType.number);
|
|
878
|
+
/** `then` when the condition is true, otherwise `otherwise` (missing when omitted). */
|
|
879
|
+
const if_else = (condition, then, otherwise) => new FunctionCallEditor("if", otherwise ? [
|
|
880
|
+
condition,
|
|
881
|
+
then,
|
|
882
|
+
otherwise
|
|
883
|
+
] : [condition, then], then.returnType ?? otherwise?.returnType);
|
|
1483
884
|
const str_eq = (a, b) => {
|
|
1484
885
|
return new StrEqExpressionEditor(a, b);
|
|
1485
886
|
};
|
|
@@ -1510,757 +911,827 @@ const min = (...args) => {
|
|
|
1510
911
|
const max = (...args) => {
|
|
1511
912
|
return new MaxExpressionEditor(args);
|
|
1512
913
|
};
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
prefills;
|
|
1523
|
-
_events;
|
|
1524
|
-
_openedAt;
|
|
1525
|
-
dateLocales;
|
|
1526
|
-
prefillEnvironment;
|
|
1527
|
-
cache;
|
|
1528
|
-
constructor(survey, context, prefills, dateLocales, options) {
|
|
1529
|
-
this._openedAt = this._getTimestamp();
|
|
1530
|
-
this.sessionSeedValue = generateId();
|
|
1531
|
-
this.surveyDef = survey;
|
|
1532
|
-
this.context = context ? context : { locale: "en" };
|
|
1533
|
-
this.locale = this.context.locale;
|
|
1534
|
-
this.prefills = prefills ? prefills.reduce((acc, p) => {
|
|
1535
|
-
acc[p.itemId] = SurveyItemResponse.deserialize(p);
|
|
1536
|
-
return acc;
|
|
1537
|
-
}, {}) : void 0;
|
|
1538
|
-
this.prefillEnvironment = options?.prefillEnvironment;
|
|
1539
|
-
this.dateLocales = dateLocales || [{
|
|
1540
|
-
code: "en",
|
|
1541
|
-
locale: enUS
|
|
1542
|
-
}];
|
|
1543
|
-
this.responses = this.initResponseObject(this.surveyDef.surveyItems);
|
|
1544
|
-
this.initCache();
|
|
1545
|
-
this.applyConfiguredPrefills();
|
|
1546
|
-
this.evalExpressions();
|
|
1547
|
-
if (!survey.rootItem) throw new Error("constructor: root item not found");
|
|
1548
|
-
this.renderedSurveyTree = this.renderGroup(survey.rootItem);
|
|
1549
|
-
this._events = [{
|
|
1550
|
-
type: SurveyEventTypes.openSurvey,
|
|
1551
|
-
timestamp: this._getTimestamp(),
|
|
1552
|
-
value: this.context.locale
|
|
1553
|
-
}];
|
|
1554
|
-
}
|
|
1555
|
-
setContext(context) {
|
|
1556
|
-
this.context = context;
|
|
1557
|
-
}
|
|
1558
|
-
getDateLocales() {
|
|
1559
|
-
return this.dateLocales.slice();
|
|
1560
|
-
}
|
|
1561
|
-
getCurrentDateLocale() {
|
|
1562
|
-
const found = this.dateLocales.find((dl) => dl.code === this.locale);
|
|
1563
|
-
if (!found) {
|
|
1564
|
-
console.warn(`Locale '${this.locale}' is not available. Using default locale.`);
|
|
1565
|
-
if (this.dateLocales.length > 0) return this.dateLocales[0].locale;
|
|
1566
|
-
return enUS;
|
|
1567
|
-
}
|
|
1568
|
-
return found?.locale;
|
|
1569
|
-
}
|
|
1570
|
-
updateContext(context) {
|
|
1571
|
-
this.context = context;
|
|
1572
|
-
this.locale = context.locale;
|
|
1573
|
-
this._registerSurveyEvent({
|
|
1574
|
-
type: SurveyEventTypes.languageChanged,
|
|
1575
|
-
timestamp: this._getTimestamp(),
|
|
1576
|
-
value: context.locale
|
|
1577
|
-
});
|
|
1578
|
-
this.evalExpressions();
|
|
1579
|
-
this.reRenderSurveyTree();
|
|
1580
|
-
}
|
|
1581
|
-
setResponse(targetKey, response) {
|
|
1582
|
-
const target = this.getResponseItem(targetKey);
|
|
1583
|
-
if (!target) throw new Error("setResponse: target not found for key: " + targetKey);
|
|
1584
|
-
target.response = response;
|
|
1585
|
-
this._registerSurveyEvent({
|
|
1586
|
-
type: SurveyEventTypes.responeChanged,
|
|
1587
|
-
timestamp: this._getTimestamp(),
|
|
1588
|
-
itemId: targetKey
|
|
1589
|
-
});
|
|
1590
|
-
this.evalExpressions();
|
|
1591
|
-
this.reRenderSurveyTree();
|
|
1592
|
-
}
|
|
1593
|
-
get openedAt() {
|
|
1594
|
-
return this._openedAt;
|
|
1595
|
-
}
|
|
1596
|
-
get sessionSeed() {
|
|
1597
|
-
return this.sessionSeedValue;
|
|
1598
|
-
}
|
|
1599
|
-
get survey() {
|
|
1600
|
-
return this.surveyDef;
|
|
1601
|
-
}
|
|
1602
|
-
getSurveyPages(size) {
|
|
1603
|
-
const renderedSurvey = flattenTree(this.renderedSurveyTree);
|
|
1604
|
-
const pages = new Array();
|
|
1605
|
-
if (!size) size = "large";
|
|
1606
|
-
let currentPage = [];
|
|
1607
|
-
renderedSurvey.forEach((item) => {
|
|
1608
|
-
if (item.type === "page-break") {
|
|
1609
|
-
if (currentPage.length > 0) {
|
|
1610
|
-
pages.push([...currentPage]);
|
|
1611
|
-
currentPage = [];
|
|
1612
|
-
}
|
|
1613
|
-
return;
|
|
1614
|
-
}
|
|
1615
|
-
currentPage.push(item);
|
|
1616
|
-
if (!this.surveyDef.maxItemsPerPage) return;
|
|
1617
|
-
let max = 0;
|
|
1618
|
-
switch (size) {
|
|
1619
|
-
case "large":
|
|
1620
|
-
max = this.surveyDef.maxItemsPerPage.large;
|
|
1621
|
-
break;
|
|
1622
|
-
case "small":
|
|
1623
|
-
max = this.surveyDef.maxItemsPerPage.small;
|
|
1624
|
-
break;
|
|
1625
|
-
}
|
|
1626
|
-
if (currentPage.length >= max) {
|
|
1627
|
-
pages.push([...currentPage]);
|
|
1628
|
-
currentPage = [];
|
|
1629
|
-
}
|
|
1630
|
-
});
|
|
1631
|
-
if (currentPage.length > 0) pages.push([...currentPage]);
|
|
1632
|
-
return pages;
|
|
1633
|
-
}
|
|
1634
|
-
getRenderedSurveyItem(itemId) {
|
|
1635
|
-
return flattenTree(this.renderedSurveyTree).find((item) => item.id === itemId);
|
|
1636
|
-
}
|
|
1637
|
-
onPageChanged(pageIndex) {
|
|
1638
|
-
this._registerSurveyEvent({
|
|
1639
|
-
type: SurveyEventTypes.pageChanged,
|
|
1640
|
-
timestamp: this._getTimestamp(),
|
|
1641
|
-
value: pageIndex
|
|
1642
|
-
});
|
|
1643
|
-
}
|
|
1644
|
-
getEvents() {
|
|
1645
|
-
return this._events.slice();
|
|
914
|
+
var FunctionCallEditor = class extends ExpressionEditor {
|
|
915
|
+
name;
|
|
916
|
+
args;
|
|
917
|
+
returnType;
|
|
918
|
+
constructor(name, args, returnType) {
|
|
919
|
+
super();
|
|
920
|
+
this.name = name;
|
|
921
|
+
this.args = args;
|
|
922
|
+
this.returnType = returnType;
|
|
1646
923
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
value: void 0,
|
|
1691
|
-
templateDef: this.surveyDef.getTemplateValue(templateKey)
|
|
924
|
+
getExpression() {
|
|
925
|
+
return createFunctionExpression(this.name, this.args.map((arg) => arg.getExpression()), this.editorConfig);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
const date_eq = (a, b) => new FunctionCallEditor("date_eq", [a, b], ValueType.boolean);
|
|
929
|
+
const date_gt = (a, b) => new FunctionCallEditor("date_gt", [a, b], ValueType.boolean);
|
|
930
|
+
const date_gte = (a, b) => new FunctionCallEditor("date_gte", [a, b], ValueType.boolean);
|
|
931
|
+
const date_lt = (a, b) => new FunctionCallEditor("date_lt", [a, b], ValueType.boolean);
|
|
932
|
+
const date_lte = (a, b) => new FunctionCallEditor("date_lte", [a, b], ValueType.boolean);
|
|
933
|
+
const date_add_days = (a, b) => new FunctionCallEditor("date_add_days", [a, b], ValueType.date);
|
|
934
|
+
const date_add_months = (a, b) => new FunctionCallEditor("date_add_months", [a, b], ValueType.date);
|
|
935
|
+
const date_add_years = (a, b) => new FunctionCallEditor("date_add_years", [a, b], ValueType.date);
|
|
936
|
+
/** Signed completed units in a - b, e.g. age as `date_diff(ctx_current_date(), birth, "years")`. */
|
|
937
|
+
const date_diff = (a, b, unit) => new FunctionCallEditor("date_diff", [
|
|
938
|
+
a,
|
|
939
|
+
b,
|
|
940
|
+
const_string(unit)
|
|
941
|
+
], ValueType.number);
|
|
942
|
+
const date_min = (...args) => new FunctionCallEditor("date_min", args, ValueType.date);
|
|
943
|
+
const date_max = (...args) => new FunctionCallEditor("date_max", args, ValueType.date);
|
|
944
|
+
const is_disabled = (ownerId) => new DisabledStateEditor(ownerId);
|
|
945
|
+
//#endregion
|
|
946
|
+
//#region src/survey/composition/registry.ts
|
|
947
|
+
function defineElement(definition) {
|
|
948
|
+
const schema = contentOwner(z.strictObject({
|
|
949
|
+
...elementShape,
|
|
950
|
+
elementType: z.literal(definition.elementType),
|
|
951
|
+
config: definition.config.nonoptional()
|
|
952
|
+
}), definition.roles ?? []);
|
|
953
|
+
return {
|
|
954
|
+
elementType: definition.elementType,
|
|
955
|
+
labelByLocale: definition.labelByLocale,
|
|
956
|
+
schema,
|
|
957
|
+
inspect(input) {
|
|
958
|
+
const node = schema.parse(input);
|
|
959
|
+
return {
|
|
960
|
+
slots: [],
|
|
961
|
+
bindings: [],
|
|
962
|
+
followUps: [],
|
|
963
|
+
fullWidth: false,
|
|
964
|
+
minWidthRem: 12,
|
|
965
|
+
componentParents: {},
|
|
966
|
+
...definition.facts?.(node.config, node.id)
|
|
1692
967
|
};
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
console.warn("initCache: validation expression not found: " + itemId + " " + validationKey);
|
|
1701
|
-
return;
|
|
1702
|
-
}
|
|
1703
|
-
this.cache.validations[itemId][validationKey] = {
|
|
1704
|
-
expression: valExp,
|
|
1705
|
-
result: false
|
|
1706
|
-
};
|
|
1707
|
-
});
|
|
1708
|
-
}
|
|
1709
|
-
if (item.displayConditions !== void 0 && (item.displayConditions.root || item.displayConditions.components)) {
|
|
1710
|
-
this.cache.displayConditions[itemId] = {};
|
|
1711
|
-
if (item.displayConditions.root) this.cache.displayConditions[itemId].root = {
|
|
1712
|
-
expression: item.displayConditions.root,
|
|
1713
|
-
result: false
|
|
1714
|
-
};
|
|
1715
|
-
if (item.displayConditions.components) {
|
|
1716
|
-
this.cache.displayConditions[itemId].components = {};
|
|
1717
|
-
Object.keys(item.displayConditions.components).forEach((componentKey) => {
|
|
1718
|
-
const compExp = item.displayConditions?.components?.[componentKey];
|
|
1719
|
-
if (!compExp) {
|
|
1720
|
-
console.warn("initCache: display condition component expression not found: " + itemId + " " + componentKey);
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
this.cache.displayConditions[itemId].components[componentKey] = {
|
|
1724
|
-
expression: compExp,
|
|
1725
|
-
result: false
|
|
1726
|
-
};
|
|
1727
|
-
});
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
if (item.disabledConditions !== void 0 && item.disabledConditions.components !== void 0) {
|
|
1731
|
-
this.cache.disabledConditions[itemId] = { components: {} };
|
|
1732
|
-
Object.keys(item.disabledConditions.components).forEach((componentKey) => {
|
|
1733
|
-
const compExp = item.disabledConditions?.components?.[componentKey];
|
|
1734
|
-
if (!compExp) {
|
|
1735
|
-
console.warn("initCache: disabled condition component expression not found: " + itemId + " " + componentKey);
|
|
1736
|
-
return;
|
|
1737
|
-
}
|
|
1738
|
-
this.cache.disabledConditions[itemId].components[componentKey] = {
|
|
1739
|
-
expression: compExp,
|
|
1740
|
-
result: false
|
|
1741
|
-
};
|
|
1742
|
-
});
|
|
1743
|
-
}
|
|
1744
|
-
});
|
|
1745
|
-
}
|
|
1746
|
-
initResponseObject(items) {
|
|
1747
|
-
const respGroup = {};
|
|
1748
|
-
items.forEach((item, itemId) => {
|
|
1749
|
-
if (!item.isInteractive) return;
|
|
1750
|
-
else {
|
|
1751
|
-
const prefill = this.prefills?.[itemId];
|
|
1752
|
-
const applyPrefill = prefill && prefill.itemType === item.type;
|
|
1753
|
-
respGroup[itemId] = new SurveyItemResponse({
|
|
1754
|
-
itemId,
|
|
1755
|
-
itemType: item.type
|
|
1756
|
-
}, applyPrefill ? prefill.response : void 0);
|
|
1757
|
-
}
|
|
1758
|
-
});
|
|
1759
|
-
return respGroup;
|
|
1760
|
-
}
|
|
1761
|
-
applyConfiguredPrefills() {
|
|
1762
|
-
const evalEngine = new ExpressionEvaluator({
|
|
1763
|
-
responses: this.responses,
|
|
1764
|
-
surveyContext: this.context
|
|
1765
|
-
});
|
|
1766
|
-
const maxPasses = Math.max(this.surveyDef.surveyItems.size, 1);
|
|
1767
|
-
for (let pass = 0; pass < maxPasses; pass++) {
|
|
1768
|
-
this.evalTemplateValues(evalEngine);
|
|
1769
|
-
let changedInPass = false;
|
|
1770
|
-
this.surveyDef.surveyItems.forEach((item) => {
|
|
1771
|
-
if (!item.isInteractive()) return;
|
|
1772
|
-
changedInPass = this.applyConfiguredPrefillsForItem(item, evalEngine) || changedInPass;
|
|
1773
|
-
});
|
|
1774
|
-
if (!changedInPass) break;
|
|
1775
|
-
}
|
|
1776
|
-
this.evalTemplateValues(evalEngine);
|
|
1777
|
-
}
|
|
1778
|
-
applyConfiguredPrefillsForItem(item, evalEngine) {
|
|
1779
|
-
const responseItem = this.responses[item.id];
|
|
1780
|
-
if (!responseItem) return false;
|
|
1781
|
-
let changed = false;
|
|
1782
|
-
for (const prefill of item.prefills ?? []) {
|
|
1783
|
-
const targetSlot = item.resolvePrefillTarget(prefill.target);
|
|
1784
|
-
if (!targetSlot) continue;
|
|
1785
|
-
if ((prefill.apply ?? SurveyItemPrefillApplyMode.ifEmpty) === SurveyItemPrefillApplyMode.ifEmpty && responseItem.response?.get(targetSlot.slotId) !== void 0) continue;
|
|
1786
|
-
if (!this.shouldApplyConfiguredPrefill(prefill, evalEngine)) continue;
|
|
1787
|
-
const resolvedValue = this.resolveConfiguredPrefillValue(prefill, targetSlot, evalEngine);
|
|
1788
|
-
if (!resolvedValue) continue;
|
|
1789
|
-
const normalizedValue = item.normalizePrefillValue(prefill, targetSlot, resolvedValue);
|
|
1790
|
-
if (!normalizedValue) continue;
|
|
1791
|
-
const nextResponse = responseItem.response?.clone() ?? new ResponseItem();
|
|
1792
|
-
nextResponse.setSlotValue(targetSlot.slotId, normalizedValue);
|
|
1793
|
-
responseItem.response = nextResponse.isEmpty() ? void 0 : nextResponse;
|
|
1794
|
-
changed = true;
|
|
968
|
+
},
|
|
969
|
+
compute(input, slotId, read) {
|
|
970
|
+
return definition.compute?.(schema.parse(input).config, slotId, read);
|
|
971
|
+
},
|
|
972
|
+
validate(input, read, evaluate) {
|
|
973
|
+
const node = schema.parse(input);
|
|
974
|
+
return definition.validate?.(node.config, node.id, read, evaluate) ?? [];
|
|
1795
975
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
default: return;
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
shouldRender(itemId, componentId) {
|
|
1823
|
-
const displayConditionResult = this.getDisplayConditionValue(itemId, componentId);
|
|
1824
|
-
if (displayConditionResult !== void 0) return displayConditionResult;
|
|
1825
|
-
return true;
|
|
1826
|
-
}
|
|
1827
|
-
sequentialRender(groupDef) {
|
|
1828
|
-
const newItems = [];
|
|
1829
|
-
for (const itemId of groupDef.items || []) {
|
|
1830
|
-
if (!this.shouldRender(itemId)) continue;
|
|
1831
|
-
const itemDef = this.surveyDef.surveyItems.get(itemId);
|
|
1832
|
-
if (!itemDef) {
|
|
1833
|
-
console.warn("sequentialRender: item not found: " + itemId);
|
|
1834
|
-
continue;
|
|
1835
|
-
}
|
|
1836
|
-
if (itemDef.type === "group") {
|
|
1837
|
-
newItems.push(this.renderGroup(itemDef));
|
|
1838
|
-
continue;
|
|
1839
|
-
}
|
|
1840
|
-
newItems.push({
|
|
1841
|
-
id: itemId,
|
|
1842
|
-
type: itemDef.type
|
|
1843
|
-
});
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
/** A registry is finalized once. Its one recursive schema is shared by built-in and custom bodies. */
|
|
979
|
+
function createElementRegistry(create) {
|
|
980
|
+
const definitions = create(z.lazy(() => composition));
|
|
981
|
+
const byType = /* @__PURE__ */ new Map();
|
|
982
|
+
for (const definition of definitions) {
|
|
983
|
+
if (byType.has(definition.elementType)) throw new Error(`Duplicate element registration: ${definition.elementType}`);
|
|
984
|
+
byType.set(definition.elementType, definition);
|
|
985
|
+
}
|
|
986
|
+
const variants = definitions.map((definition) => definition.schema);
|
|
987
|
+
const element = variants.length ? z.union(variants) : z.never();
|
|
988
|
+
const composition = createCompositionSchema(element);
|
|
989
|
+
return {
|
|
990
|
+
definitions,
|
|
991
|
+
labels: new Map(definitions.map((definition) => [definition.elementType, definition.labelByLocale ?? {}])),
|
|
992
|
+
elementSchema: element,
|
|
993
|
+
bodySchema: composition,
|
|
994
|
+
surveySchema: createSurveySchema(composition),
|
|
995
|
+
get(elementType) {
|
|
996
|
+
const definition = byType.get(elementType);
|
|
997
|
+
if (!definition) throw new Error(`Unknown element type: ${elementType}`);
|
|
998
|
+
return definition;
|
|
1844
999
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
//#endregion
|
|
1003
|
+
//#region src/survey/composition/semantics.ts
|
|
1004
|
+
/** Structural context is independent of an element's explicit accessible label. */
|
|
1005
|
+
function getElementSemanticContext(survey, elementId) {
|
|
1006
|
+
const owner = survey.owners.get(elementId);
|
|
1007
|
+
if (!owner) throw new Error(`Unknown element '${elementId}'`);
|
|
1008
|
+
let ancestor = owner.parentId ? survey.owners.get(owner.parentId) : void 0;
|
|
1009
|
+
while (ancestor && ancestor.followUpTriggerId === owner.followUpTriggerId) {
|
|
1010
|
+
if (ancestor.kind === "section" || ancestor.kind === "item") return ancestor;
|
|
1011
|
+
ancestor = ancestor.parentId ? survey.owners.get(ancestor.parentId) : void 0;
|
|
1850
1012
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1013
|
+
}
|
|
1014
|
+
function resolveElementName(survey, elementId, locales) {
|
|
1015
|
+
const owner = survey.owners.get(elementId);
|
|
1016
|
+
if (!owner) throw new Error(`Unknown element '${elementId}'`);
|
|
1017
|
+
const name = (ownerId, role) => {
|
|
1018
|
+
if (!survey.owners.get(ownerId)?.roles.some((entry) => entry.role === role)) return void 0;
|
|
1019
|
+
const resolved = resolveParticipantContent(survey, ownerId, role, locales);
|
|
1020
|
+
const text = getContentPlainText(resolved?.content).trim();
|
|
1021
|
+
return text && resolved ? {
|
|
1022
|
+
ownerId,
|
|
1023
|
+
role,
|
|
1024
|
+
text,
|
|
1025
|
+
content: resolved.content,
|
|
1026
|
+
locale: resolved.locale
|
|
1027
|
+
} : void 0;
|
|
1028
|
+
};
|
|
1029
|
+
const own = name(elementId, "label");
|
|
1030
|
+
if (own) return own;
|
|
1031
|
+
let ancestor = owner.parentId ? survey.owners.get(owner.parentId) : void 0;
|
|
1032
|
+
while (ancestor) {
|
|
1033
|
+
const current = ancestor;
|
|
1034
|
+
if ([...survey.elements.values()].filter((element) => {
|
|
1035
|
+
const candidate = survey.owners.get(element.node.id);
|
|
1036
|
+
return element.facts.bindings.length > 0 && candidate.followUpTriggerId === owner.followUpTriggerId && current.path.every((part, index) => candidate.path[index] === part);
|
|
1037
|
+
}).length === 1) {
|
|
1038
|
+
const inherited = name(ancestor.id, "title") ?? name(ancestor.id, "label");
|
|
1039
|
+
if (inherited) return inherited;
|
|
1872
1040
|
}
|
|
1873
|
-
return
|
|
1874
|
-
|
|
1875
|
-
type: "group",
|
|
1876
|
-
items: newItems
|
|
1877
|
-
};
|
|
1878
|
-
}
|
|
1879
|
-
renderGroup(groupDef, parent) {
|
|
1880
|
-
if (!parent) parent = {
|
|
1881
|
-
id: groupDef.id,
|
|
1882
|
-
type: "group",
|
|
1883
|
-
items: []
|
|
1884
|
-
};
|
|
1885
|
-
if (groupDef.config.shuffleItems) return this.randomizedItemRender(groupDef, parent);
|
|
1886
|
-
return this.sequentialRender(groupDef);
|
|
1041
|
+
if (ancestor.kind === "section" || ancestor.id === owner.followUpTriggerId) return void 0;
|
|
1042
|
+
ancestor = ancestor.parentId ? survey.owners.get(ancestor.parentId) : void 0;
|
|
1887
1043
|
}
|
|
1888
|
-
|
|
1889
|
-
if (!this.surveyDef.rootItem) throw new Error("reRenderSurveyTree: root item not found");
|
|
1890
|
-
this.renderedSurveyTree = this.renderGroup(this.surveyDef.rootItem);
|
|
1891
|
-
}
|
|
1892
|
-
_registerSurveyEvent(event) {
|
|
1893
|
-
this._events.push(event);
|
|
1894
|
-
}
|
|
1895
|
-
_getTimestamp() {
|
|
1896
|
-
return Math.floor(Date.now() / 1e3);
|
|
1897
|
-
}
|
|
1898
|
-
getResponseItem(itemId) {
|
|
1899
|
-
return this.responses[itemId];
|
|
1900
|
-
}
|
|
1901
|
-
evalExpressions() {
|
|
1902
|
-
const evalEngine = new ExpressionEvaluator({
|
|
1903
|
-
responses: this.responses,
|
|
1904
|
-
surveyContext: this.context
|
|
1905
|
-
});
|
|
1906
|
-
this.evalTemplateValues(evalEngine);
|
|
1907
|
-
this.evalDisplayConditions(evalEngine);
|
|
1908
|
-
this.evalDisableConditions(evalEngine);
|
|
1909
|
-
this.evalValidations(evalEngine);
|
|
1910
|
-
}
|
|
1911
|
-
evalTemplateValues(evalEngine) {
|
|
1912
|
-
Object.keys(this.cache.templateValues).forEach((templateValueKey) => {
|
|
1913
|
-
const templateValue = this.cache.templateValues[templateValueKey];
|
|
1914
|
-
this.cache.templateValues[templateValueKey].value = void 0;
|
|
1915
|
-
if (!templateValue.templateDef?.expression) {
|
|
1916
|
-
console.warn("evalTemplateValues: template value expression not found: " + templateValueKey);
|
|
1917
|
-
return;
|
|
1918
|
-
}
|
|
1919
|
-
let evaluatedValue = evalEngine.eval(templateValue.templateDef.expression);
|
|
1920
|
-
if (evaluatedValue === void 0) {
|
|
1921
|
-
console.warn("evalTemplateValues: template value expression returned undefined: " + templateValueKey);
|
|
1922
|
-
return;
|
|
1923
|
-
}
|
|
1924
|
-
switch (templateValue.templateDef.type) {
|
|
1925
|
-
case "date2string":
|
|
1926
|
-
if (evaluatedValue.type !== ValueType.date) {
|
|
1927
|
-
console.warn("evalTemplateValues: template value expression returned non-date value: " + templateValueKey);
|
|
1928
|
-
return;
|
|
1929
|
-
}
|
|
1930
|
-
evaluatedValue = {
|
|
1931
|
-
type: ValueType.string,
|
|
1932
|
-
value: format(/* @__PURE__ */ new Date(evaluatedValue.value * 1e3), templateValue.templateDef.dateFormat)
|
|
1933
|
-
};
|
|
1934
|
-
break;
|
|
1935
|
-
default: break;
|
|
1936
|
-
}
|
|
1937
|
-
this.cache.templateValues[templateValueKey].value = evaluatedValue;
|
|
1938
|
-
});
|
|
1939
|
-
}
|
|
1940
|
-
evalDisplayConditions(evalEngine) {
|
|
1941
|
-
Object.keys(this.cache.displayConditions).forEach((itemKey) => {
|
|
1942
|
-
const displayCondition = this.cache.displayConditions[itemKey];
|
|
1943
|
-
if (displayCondition.root) {
|
|
1944
|
-
const resolvedValue = evalEngine.eval(displayCondition.root.expression);
|
|
1945
|
-
if (resolvedValue === void 0 || resolvedValue.type !== ValueType.boolean) {
|
|
1946
|
-
console.warn("evalDisplayConditions: display condition expression returned undefined: " + itemKey);
|
|
1947
|
-
return;
|
|
1948
|
-
}
|
|
1949
|
-
this.cache.displayConditions[itemKey].root.result = resolvedValue.value;
|
|
1950
|
-
}
|
|
1951
|
-
if (displayCondition.components) Object.keys(displayCondition.components).forEach((componentKey) => {
|
|
1952
|
-
const resolvedValue = evalEngine.eval(displayCondition.components[componentKey].expression);
|
|
1953
|
-
if (resolvedValue === void 0 || resolvedValue.type !== ValueType.boolean) {
|
|
1954
|
-
console.warn("evalDisplayConditions: display condition component expression returned undefined: " + itemKey + "." + componentKey);
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
this.cache.displayConditions[itemKey].components[componentKey].result = resolvedValue.value;
|
|
1958
|
-
});
|
|
1959
|
-
});
|
|
1960
|
-
}
|
|
1961
|
-
evalDisableConditions(evalEngine) {
|
|
1962
|
-
Object.keys(this.cache.disabledConditions).forEach((itemKey) => {
|
|
1963
|
-
const disableCondition = this.cache.disabledConditions[itemKey];
|
|
1964
|
-
if (disableCondition.components) Object.keys(disableCondition.components).forEach((componentKey) => {
|
|
1965
|
-
const resolvedValue = evalEngine.eval(disableCondition.components[componentKey].expression);
|
|
1966
|
-
if (resolvedValue === void 0 || resolvedValue.type !== ValueType.boolean) {
|
|
1967
|
-
console.warn("evalDisableConditions: disable condition component expression returned undefined: " + itemKey + "." + componentKey);
|
|
1968
|
-
return;
|
|
1969
|
-
}
|
|
1970
|
-
this.cache.disabledConditions[itemKey].components[componentKey].result = resolvedValue.value;
|
|
1971
|
-
});
|
|
1972
|
-
});
|
|
1973
|
-
}
|
|
1974
|
-
evalValidations(evalEngine) {
|
|
1975
|
-
Object.keys(this.cache.validations).forEach((itemId) => {
|
|
1976
|
-
const validation = this.cache.validations[itemId];
|
|
1977
|
-
Object.keys(validation).forEach((validationKey) => {
|
|
1978
|
-
const resolvedValue = evalEngine.eval(validation[validationKey].expression);
|
|
1979
|
-
if (resolvedValue === void 0 || resolvedValue.type !== ValueType.boolean) {
|
|
1980
|
-
console.warn("evalValidations: validation expression returned undefined: " + itemId + "." + validationKey);
|
|
1981
|
-
return;
|
|
1982
|
-
}
|
|
1983
|
-
this.cache.validations[itemId][validationKey].result = resolvedValue.value;
|
|
1984
|
-
});
|
|
1985
|
-
});
|
|
1986
|
-
}
|
|
1987
|
-
};
|
|
1988
|
-
const flattenTree = (itemTree) => {
|
|
1989
|
-
const flatTree = new Array();
|
|
1990
|
-
itemTree.items?.forEach((item) => {
|
|
1991
|
-
if (item.type === "group") flatTree.push(...flattenTree(item));
|
|
1992
|
-
else flatTree.push({ ...item });
|
|
1993
|
-
});
|
|
1994
|
-
return flatTree;
|
|
1995
|
-
};
|
|
1044
|
+
}
|
|
1996
1045
|
//#endregion
|
|
1997
|
-
//#region src/
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
this.path = path;
|
|
2003
|
-
this.operationIndex = operationIndex;
|
|
2004
|
-
this.name = "JsonPatchError";
|
|
2005
|
-
}
|
|
2006
|
-
};
|
|
2007
|
-
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
2008
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2009
|
-
const assertValidPointerSegment = (segment, pointer) => {
|
|
2010
|
-
if (/~(?![01])/.test(segment)) throw new JsonPatchError(`Invalid JSON pointer segment "${segment}" in "${pointer}". Only "~0" and "~1" escapes are allowed.`, "invalid-json-pointer", pointer);
|
|
2011
|
-
};
|
|
2012
|
-
const decodeJsonPointerSegment = (segment) => {
|
|
2013
|
-
assertValidPointerSegment(segment, segment);
|
|
2014
|
-
return segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
2015
|
-
};
|
|
2016
|
-
const encodeJsonPointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2017
|
-
const parseJsonPointer = (pointer) => {
|
|
2018
|
-
if (pointer === "") return [];
|
|
2019
|
-
if (!pointer.startsWith("/")) throw new JsonPatchError(`Invalid JSON pointer "${pointer}". It must be empty or start with "/".`, "invalid-json-pointer", pointer);
|
|
2020
|
-
return pointer.slice(1).split("/").map((segment) => {
|
|
2021
|
-
assertValidPointerSegment(segment, pointer);
|
|
2022
|
-
return segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
1046
|
+
//#region src/engine/validation.ts
|
|
1047
|
+
function validateSurveyAnswers(plan, answers, context, options) {
|
|
1048
|
+
const snapshot = plan.evaluate({
|
|
1049
|
+
answers,
|
|
1050
|
+
context
|
|
2023
1051
|
});
|
|
2024
|
-
|
|
2025
|
-
const
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
};
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
1052
|
+
const evaluationDiagnostics = [];
|
|
1053
|
+
const eligible = (slotId) => {
|
|
1054
|
+
const binding = plan.bindings.get(slotId);
|
|
1055
|
+
return !!binding && hasPermittedChange(binding, snapshot);
|
|
1056
|
+
};
|
|
1057
|
+
const evaluate = (expression, path) => {
|
|
1058
|
+
try {
|
|
1059
|
+
return evaluateExpression(expression, {
|
|
1060
|
+
...context,
|
|
1061
|
+
answer: snapshot.answer,
|
|
1062
|
+
disabled: snapshot.disabled
|
|
1063
|
+
});
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
if (!(error instanceof ExpressionEvaluationError)) throw error;
|
|
1066
|
+
evaluationDiagnostics.push({
|
|
1067
|
+
code: "validation-evaluation",
|
|
1068
|
+
severity: "warning",
|
|
1069
|
+
path,
|
|
1070
|
+
message: error.message
|
|
1071
|
+
});
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
const issues = [];
|
|
1076
|
+
const authorWarnings = [];
|
|
1077
|
+
const active = (slotId) => {
|
|
1078
|
+
const binding = plan.bindings.get(slotId);
|
|
1079
|
+
return !!binding && [binding.controlId, ...binding.visibilityIds ?? []].every(snapshot.visible);
|
|
1080
|
+
};
|
|
1081
|
+
const included = (itemId) => !options.itemIds || options.itemIds.includes(itemId);
|
|
1082
|
+
for (const element of plan.document.elements.values()) {
|
|
1083
|
+
if (!included(element.itemId)) continue;
|
|
1084
|
+
for (const binding of element.facts.bindings) {
|
|
1085
|
+
if (!binding.selection || !active(binding.slotId) || [binding.controlId, ...binding.visibilityIds ?? []].some(snapshot.disabled)) continue;
|
|
1086
|
+
if (!hasPermittedChange(binding, snapshot)) authorWarnings.push({
|
|
1087
|
+
ownerId: binding.controlId,
|
|
1088
|
+
code: "non-interactive-choice",
|
|
1089
|
+
message: "Option locks, visibility, or the selection limit leave this choice with no permitted participant change. Its constraints will not block progress; its active answer is still submitted."
|
|
1090
|
+
});
|
|
1091
|
+
if (binding.selection.multiple) {
|
|
1092
|
+
const answer = snapshot.answer(binding.slotId);
|
|
1093
|
+
if (answer?.type === "reference[]") for (const optionId of answer.value.filter(snapshot.disabled)) authorWarnings.push({
|
|
1094
|
+
ownerId: optionId,
|
|
1095
|
+
code: "locked-checkbox-selection",
|
|
1096
|
+
message: "This selected checkbox is now locked and cannot be unticked. It remains an active, submitted answer. Use visibility if the answer should stop applying."
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
for (const issue of plan.document.registry.get(element.node.elementType).validate(element.node, snapshot.answer, (expression) => evaluate(expression, element.path))) if (eligible(issue.slotId)) issues.push(issue);
|
|
1101
|
+
else if (active(issue.slotId)) authorWarnings.push({
|
|
1102
|
+
ownerId: issue.messageOwnerId ?? plan.bindings.get(issue.slotId).controlId,
|
|
1103
|
+
code: "locked-input-constraint",
|
|
1104
|
+
message: `The ${issue.code} constraint is not satisfied, but this input currently permits no participant change. It will not block progress; its active answer is still submitted.`
|
|
1105
|
+
});
|
|
2063
1106
|
}
|
|
2064
|
-
|
|
2065
|
-
if (
|
|
2066
|
-
const
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
1107
|
+
for (const item of plan.document.data.surveyItems) {
|
|
1108
|
+
if (item.itemType !== "content" || !included(item.id)) continue;
|
|
1109
|
+
for (const rule of item.config.validationRules ?? []) {
|
|
1110
|
+
if (rule.phase === "submit" && options.phase !== "submit") continue;
|
|
1111
|
+
const targets = rule.targets.filter(eligible);
|
|
1112
|
+
if (!rule.targets.filter(active).length) continue;
|
|
1113
|
+
if (rule.when) {
|
|
1114
|
+
const result = evaluate(rule.when, [...plan.document.owners.get(rule.id).path, "when"]);
|
|
1115
|
+
if (result?.type !== "boolean" || !result.value) continue;
|
|
1116
|
+
}
|
|
1117
|
+
const result = evaluate(rule.expression, [...plan.document.owners.get(rule.id).path, "expression"]);
|
|
1118
|
+
if (result?.type !== "boolean" || !result.value) {
|
|
1119
|
+
issues.push(...targets.map((slotId) => ({
|
|
1120
|
+
slotId,
|
|
1121
|
+
code: "rule",
|
|
1122
|
+
messageOwnerId: rule.id
|
|
1123
|
+
})));
|
|
1124
|
+
if (!targets.length) authorWarnings.push({
|
|
1125
|
+
ownerId: rule.id,
|
|
1126
|
+
code: "locked-rule-targets",
|
|
1127
|
+
message: "This applicable rule fails, but none of its active targets permits a participant change. It will not block progress. Check the rule and disabling conditions."
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
2070
1131
|
}
|
|
2071
|
-
return
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
1132
|
+
return {
|
|
1133
|
+
issues,
|
|
1134
|
+
diagnostics: [...snapshot.diagnostics, ...evaluationDiagnostics],
|
|
1135
|
+
authorWarnings
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
const authoringDependencyPlans = /* @__PURE__ */ new WeakMap();
|
|
1139
|
+
const staticWarnings = /* @__PURE__ */ new WeakMap();
|
|
1140
|
+
/** Conservative proofs only: dynamic conditions and prefills are checked in a real preview. */
|
|
1141
|
+
function staticConstraintWarnings(survey, plan) {
|
|
1142
|
+
const literal = (expression, value) => expression?.type === "const" && expression.value?.type === "boolean" && expression.value.value === value;
|
|
1143
|
+
const parents = (id) => {
|
|
1144
|
+
const owner = survey.owners.get(id);
|
|
1145
|
+
return [...owner.parentId ? [owner.parentId] : [], ...owner.elementId ? ownValue(survey.elements.get(owner.elementId)?.facts.componentParents, id) ?? [] : []];
|
|
1146
|
+
};
|
|
1147
|
+
const visible = (id) => {
|
|
1148
|
+
const owner = survey.owners.get(id);
|
|
1149
|
+
return !owner.followUpTriggerId && (!owner.visibility || literal(owner.visibility, true)) && parents(id).every(visible);
|
|
1150
|
+
};
|
|
1151
|
+
const hidden = (id) => literal(survey.owners.get(id).visibility, false) || parents(id).some(hidden);
|
|
1152
|
+
const disabled = (id) => literal(survey.owners.get(id).disabled, true) || parents(id).some(disabled);
|
|
1153
|
+
const locked = (slotId) => {
|
|
1154
|
+
const binding = plan.bindings.get(slotId);
|
|
1155
|
+
if (!binding) return false;
|
|
1156
|
+
const controls = [binding.controlId, ...binding.visibilityIds ?? []];
|
|
1157
|
+
return controls.every(visible) && (controls.some(disabled) || !!binding.selection?.options.every((id) => hidden(id) || disabled(id)));
|
|
1158
|
+
};
|
|
1159
|
+
const warnings = [];
|
|
1160
|
+
for (const element of survey.elements.values()) {
|
|
1161
|
+
if (!element.facts.bindings.some((binding) => locked(binding.slotId))) continue;
|
|
1162
|
+
for (const issue of survey.registry.get(element.node.elementType).validate(element.node, () => void 0, () => void 0)) if ((issue.code === "required" || issue.code === "mustBeTrue") && locked(issue.slotId) && !ownValue(element.node.prefills, issue.slotId)?.length) warnings.push({
|
|
1163
|
+
ownerId: issue.messageOwnerId ?? plan.bindings.get(issue.slotId).controlId,
|
|
1164
|
+
code: "locked-input-constraint",
|
|
1165
|
+
message: `The ${issue.code} constraint starts unsatisfied on an input with no permitted participant change and no prefill. It will not block progress. Check the requirement and disabling conditions.`
|
|
2086
1166
|
});
|
|
2087
|
-
parent.splice(index, 0, structuredCloneMethod(value));
|
|
2088
|
-
return document;
|
|
2089
1167
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
1168
|
+
for (const item of survey.data.surveyItems) {
|
|
1169
|
+
if (item.itemType !== "content") continue;
|
|
1170
|
+
for (const rule of item.config.validationRules ?? []) {
|
|
1171
|
+
if (!literal(rule.expression, false) || rule.when && !literal(rule.when, true)) continue;
|
|
1172
|
+
if (rule.targets.length && rule.targets.every(locked)) warnings.push({
|
|
1173
|
+
ownerId: rule.id,
|
|
1174
|
+
code: "locked-rule-targets",
|
|
1175
|
+
message: "This rule always fails, but all its targets are locked. It will not block progress. Check the rule and disabling conditions."
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return warnings;
|
|
1180
|
+
}
|
|
1181
|
+
function getAuthoringWarnings(survey, locale) {
|
|
1182
|
+
const warnings = [];
|
|
1183
|
+
const itemsWithElements = new Set([...survey.elements.values()].map((element) => element.itemId));
|
|
1184
|
+
for (const item of survey.data.surveyItems) if (item.itemType === "content" && !itemsWithElements.has(item.id)) warnings.push({
|
|
1185
|
+
ownerId: item.id,
|
|
1186
|
+
code: "empty-content-item",
|
|
1187
|
+
message: "Add an element to this empty item, or remove the item."
|
|
2096
1188
|
});
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
1189
|
+
for (const owner of survey.owners.values()) for (const role of owner.roles.filter((role) => role.recommended)) if (!hasLocalizationText(readLocalizationContent(survey, {
|
|
1190
|
+
scope: "content",
|
|
1191
|
+
ownerId: owner.id,
|
|
1192
|
+
role: role.role
|
|
1193
|
+
}, locale))) warnings.push({
|
|
1194
|
+
ownerId: owner.id,
|
|
1195
|
+
code: "missing-content",
|
|
1196
|
+
message: `Add ${role.role} to make this structure understandable`
|
|
1197
|
+
});
|
|
1198
|
+
let plan = authoringDependencyPlans.get(survey.elements);
|
|
1199
|
+
if (!plan) {
|
|
1200
|
+
plan = new SurveyStateGraph(survey, { admission: "draft" });
|
|
1201
|
+
authoringDependencyPlans.set(survey.elements, plan);
|
|
1202
|
+
}
|
|
1203
|
+
let conflicts = staticWarnings.get(survey.elements);
|
|
1204
|
+
if (!conflicts) {
|
|
1205
|
+
conflicts = staticConstraintWarnings(survey, plan);
|
|
1206
|
+
staticWarnings.set(survey.elements, conflicts);
|
|
1207
|
+
}
|
|
1208
|
+
warnings.push(...conflicts);
|
|
1209
|
+
for (const item of survey.data.surveyItems) {
|
|
1210
|
+
if (item.itemType !== "content") continue;
|
|
1211
|
+
for (const rule of item.config.validationRules ?? []) {
|
|
1212
|
+
if (rule.phase === "submit") continue;
|
|
1213
|
+
if ([rule.expression, ...rule.when ? [rule.when] : []].flatMap((expression) => [...plan.storedDependencies(expression)]).some((id) => {
|
|
1214
|
+
const dependencyItem = survey.slots.get(id)?.itemId;
|
|
1215
|
+
return dependencyItem && dependencyItem !== item.id && !isStaticallyBefore(survey, dependencyItem, item.id);
|
|
1216
|
+
})) warnings.push({
|
|
1217
|
+
ownerId: rule.id,
|
|
1218
|
+
code: "cross-item-page-rule",
|
|
1219
|
+
message: "This rule runs before leaving the page and reads answers from another item. If those answers are on a later page, the participant may be unable to continue. Consider submit-only validation or an explicit condition for unanswered dependencies."
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
for (const element of survey.elements.values()) for (const binding of element.facts.bindings) if (binding.labelRefs) {
|
|
1224
|
+
for (const ref of binding.labelRefs) if (!hasLocalizationText(readLocalizationContent(survey, {
|
|
1225
|
+
scope: "content",
|
|
1226
|
+
...ref
|
|
1227
|
+
}, locale)) && !warnings.some((warning) => warning.ownerId === ref.ownerId && warning.code === "missing-content")) warnings.push({
|
|
1228
|
+
ownerId: ref.ownerId,
|
|
1229
|
+
code: "missing-accessible-name",
|
|
1230
|
+
message: `Add ${ref.role} to name this input in the selected language`
|
|
2112
1231
|
});
|
|
2113
|
-
|
|
1232
|
+
} else if (!resolveElementName(survey, element.node.id, [locale]) && !warnings.some((warning) => warning.ownerId === element.node.id && warning.code === "missing-accessible-name")) warnings.push({
|
|
1233
|
+
ownerId: element.node.id,
|
|
1234
|
+
code: "missing-accessible-name",
|
|
1235
|
+
message: "Add a label, or an unambiguous enclosing title, in the selected language"
|
|
1236
|
+
});
|
|
1237
|
+
for (const element of survey.elements.values()) for (const binding of element.facts.bindings) {
|
|
1238
|
+
if (!binding.selection?.multiple) continue;
|
|
1239
|
+
for (const id of binding.selection.options) {
|
|
1240
|
+
const condition = survey.owners.get(id)?.disabled;
|
|
1241
|
+
if (condition && [...plan.storedDependencies(condition)].some((slotId) => slotId !== binding.slotId)) warnings.push({
|
|
1242
|
+
ownerId: id,
|
|
1243
|
+
code: "external-checkbox-lock",
|
|
1244
|
+
message: "This condition depends on another answer. An already selected checkbox can become locked and will still be submitted. Use visibility if the selection should stop applying."
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return warnings;
|
|
1249
|
+
}
|
|
1250
|
+
/** A shared shuffled ancestor only changes order between its distinct children. */
|
|
1251
|
+
function isStaticallyBefore(survey, earlierId, laterId) {
|
|
1252
|
+
const earlier = [...survey.getItemPath(earlierId), earlierId];
|
|
1253
|
+
const later = [...survey.getItemPath(laterId), laterId];
|
|
1254
|
+
const branch = earlier.findIndex((id, index) => id !== later[index]);
|
|
1255
|
+
if (branch < 1) return false;
|
|
1256
|
+
const parent = survey.getItem(earlier[branch - 1]);
|
|
1257
|
+
return parent?.itemType === "group" && !parent.config.shuffleItems && parent.config.items.indexOf(earlier[branch]) < parent.config.items.indexOf(later[branch]);
|
|
1258
|
+
}
|
|
1259
|
+
//#endregion
|
|
1260
|
+
//#region src/survey/composition/diagnostics.ts
|
|
1261
|
+
/** Observed author-only conflicts use the same validation result as the player and host. */
|
|
1262
|
+
function getSessionDiagnostics(session) {
|
|
1263
|
+
const result = session.validate("submit");
|
|
1264
|
+
return [...result.authorWarnings.map((warning) => {
|
|
1265
|
+
const owner = session.document.owners.get(warning.ownerId);
|
|
2114
1266
|
return {
|
|
2115
|
-
|
|
2116
|
-
|
|
1267
|
+
...warning,
|
|
1268
|
+
severity: "warning",
|
|
1269
|
+
path: owner?.path ?? [],
|
|
1270
|
+
ownerItemId: owner?.itemId
|
|
2117
1271
|
};
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
};
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
}
|
|
2138
|
-
const
|
|
2139
|
-
|
|
2140
|
-
|
|
1272
|
+
}), ...result.diagnostics.map((diagnostic) => {
|
|
1273
|
+
const owner = ownerAtPath(session.document, diagnostic.path);
|
|
1274
|
+
return {
|
|
1275
|
+
...diagnostic,
|
|
1276
|
+
ownerId: owner?.id,
|
|
1277
|
+
ownerItemId: owner?.itemId
|
|
1278
|
+
};
|
|
1279
|
+
})];
|
|
1280
|
+
}
|
|
1281
|
+
function ownerAtPath(survey, path) {
|
|
1282
|
+
return [...survey.owners.values()].filter((owner) => owner.path.length <= path.length && owner.path.every((part, index) => part === path[index])).sort((a, b) => b.path.length - a.path.length)[0];
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Every dangling reference an editor draft admits: missing answers, state targets, options,
|
|
1286
|
+
* owners and non-flow items from the document index, plus comparisons with removed option
|
|
1287
|
+
* identities reported by the compiled graph. Always empty for a runnable survey.
|
|
1288
|
+
*/
|
|
1289
|
+
function getBrokenReferences(survey) {
|
|
1290
|
+
return getDefinitionDiagnostics(survey).filter((diagnostic) => diagnostic.severity === "error" && diagnostic.kind === "reference");
|
|
1291
|
+
}
|
|
1292
|
+
const compiledDiagnostics = /* @__PURE__ */ new WeakMap();
|
|
1293
|
+
/** Use the authoritative definition/compiler diagnostics, including state reads and custom definitions. */
|
|
1294
|
+
function getDefinitionDiagnostics(survey, locale) {
|
|
1295
|
+
let compiled = compiledDiagnostics.get(survey.elements);
|
|
1296
|
+
if (!compiled) {
|
|
2141
1297
|
try {
|
|
2142
|
-
|
|
2143
|
-
case "add":
|
|
2144
|
-
patchedDocument = applyAdd(patchedDocument, operation.path, operation.value);
|
|
2145
|
-
break;
|
|
2146
|
-
case "remove":
|
|
2147
|
-
patchedDocument = applyRemove(patchedDocument, operation.path).document;
|
|
2148
|
-
break;
|
|
2149
|
-
case "replace":
|
|
2150
|
-
patchedDocument = applyReplace(patchedDocument, operation.path, operation.value);
|
|
2151
|
-
break;
|
|
2152
|
-
case "move": {
|
|
2153
|
-
assertMoveTargetIsNotDescendant(operation.from, operation.path);
|
|
2154
|
-
if (operation.from === operation.path) break;
|
|
2155
|
-
const { document: withoutValue, removedValue } = applyRemove(patchedDocument, operation.from);
|
|
2156
|
-
patchedDocument = applyAdd(withoutValue, operation.path, removedValue);
|
|
2157
|
-
break;
|
|
2158
|
-
}
|
|
2159
|
-
case "copy": {
|
|
2160
|
-
const value = getValueAtJsonPointer(patchedDocument, operation.from);
|
|
2161
|
-
patchedDocument = applyAdd(patchedDocument, operation.path, value);
|
|
2162
|
-
break;
|
|
2163
|
-
}
|
|
2164
|
-
case "test":
|
|
2165
|
-
if (!deepEqualJson(getValueAtJsonPointer(patchedDocument, operation.path), operation.value)) throw new JsonPatchError(`JSON Patch test failed at "${operation.path}".`, "test-failed", operation.path);
|
|
2166
|
-
break;
|
|
2167
|
-
}
|
|
1298
|
+
compiled = new SurveyStateGraph(survey, { admission: "draft" }).admittedDiagnostics;
|
|
2168
1299
|
} catch (error) {
|
|
2169
|
-
if (error instanceof
|
|
2170
|
-
throw error;
|
|
1300
|
+
if (error instanceof DefinitionError) compiled = error.diagnostics;
|
|
1301
|
+
else throw error;
|
|
2171
1302
|
}
|
|
1303
|
+
compiledDiagnostics.set(survey.elements, compiled);
|
|
1304
|
+
}
|
|
1305
|
+
const located = [...survey.diagnostics, ...compiled].map((diagnostic) => {
|
|
1306
|
+
const owner = ownerAtPath(survey, diagnostic.path) ?? (diagnostic.path[0] === "translations" && diagnostic.path[2] === "content" ? survey.owners.get(String(diagnostic.path[3])) : void 0);
|
|
1307
|
+
return {
|
|
1308
|
+
...diagnostic,
|
|
1309
|
+
ownerItemId: owner?.itemId,
|
|
1310
|
+
ownerId: owner?.id
|
|
1311
|
+
};
|
|
2172
1312
|
});
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
/** Source of an export column. */
|
|
2193
|
-
const ExportColumnSource = {
|
|
2194
|
-
meta: "meta",
|
|
2195
|
-
context: "context",
|
|
2196
|
-
response: "response"
|
|
2197
|
-
};
|
|
1313
|
+
if (locale) try {
|
|
1314
|
+
for (const warning of getAuthoringWarnings(survey, locale)) {
|
|
1315
|
+
const owner = survey.owners.get(warning.ownerId);
|
|
1316
|
+
located.push({
|
|
1317
|
+
...warning,
|
|
1318
|
+
severity: "warning",
|
|
1319
|
+
path: owner?.path ?? [],
|
|
1320
|
+
ownerItemId: owner?.itemId
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
if (!(error instanceof DefinitionError)) throw error;
|
|
1325
|
+
}
|
|
1326
|
+
return [...new Map(located.map((issue) => [JSON.stringify([
|
|
1327
|
+
issue.code,
|
|
1328
|
+
issue.path,
|
|
1329
|
+
issue.message
|
|
1330
|
+
]), issue])).values()];
|
|
1331
|
+
}
|
|
2198
1332
|
//#endregion
|
|
2199
|
-
//#region src/
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
1333
|
+
//#region src/engine/session.ts
|
|
1334
|
+
const sessionSnapshotSchema = z.strictObject({
|
|
1335
|
+
schemaVersion: z.literal(1),
|
|
1336
|
+
binding: z.strictObject({
|
|
1337
|
+
sessionId: z.string().min(1),
|
|
1338
|
+
definitionId: z.string().min(1)
|
|
1339
|
+
}),
|
|
1340
|
+
answers: stringRecord(z.string().min(1), responseValueSchema),
|
|
1341
|
+
initialization: z.literal("complete"),
|
|
1342
|
+
navigation: z.strictObject({
|
|
1343
|
+
pageIndex: z.number().int().nonnegative(),
|
|
1344
|
+
viewport: z.enum(["small", "large"])
|
|
1345
|
+
}),
|
|
1346
|
+
randomization: z.strictObject({ seed: z.string().min(1) }),
|
|
1347
|
+
context: z.strictObject({
|
|
1348
|
+
locale: z.string().min(1),
|
|
1349
|
+
/** Defaults to the participant-local day when a new session starts; kept on resume. */
|
|
1350
|
+
currentDate: calendarDaySchema.optional(),
|
|
1351
|
+
participantFlags: stringRecord(z.string(), z.string()).optional(),
|
|
1352
|
+
customValues: stringRecord(z.string(), responseValueSchema).optional()
|
|
1353
|
+
}),
|
|
1354
|
+
events: z.array(surveyEventSchema)
|
|
1355
|
+
});
|
|
1356
|
+
const eventTimestamp = () => Math.floor(Date.now() / 1e3);
|
|
1357
|
+
const comparableAnswer = (value) => JSON.stringify(value, Object.keys(value).sort());
|
|
1358
|
+
function freezeAnswer(value) {
|
|
1359
|
+
if (Array.isArray(value.value)) Object.freeze(value.value);
|
|
1360
|
+
return Object.freeze(value);
|
|
1361
|
+
}
|
|
1362
|
+
function sameRecord(left, right, equal) {
|
|
1363
|
+
const entries = Object.entries(left ?? {});
|
|
1364
|
+
return entries.length === Object.keys(right ?? {}).length && entries.every(([key, value]) => {
|
|
1365
|
+
const other = ownValue(right, key);
|
|
1366
|
+
return other !== void 0 && equal(value, other);
|
|
1367
|
+
});
|
|
2213
1368
|
}
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
1369
|
+
/** Owns retained answers; participant adapters receive no privileged initialization write path. */
|
|
1370
|
+
var SurveySession = class SurveySession {
|
|
1371
|
+
currentDocument;
|
|
1372
|
+
state;
|
|
1373
|
+
stateGraph;
|
|
1374
|
+
revision = 0;
|
|
1375
|
+
presentationRevision = 0;
|
|
1376
|
+
itemPresentationRevisions = /* @__PURE__ */ new Map();
|
|
1377
|
+
initializationDiagnostics = [];
|
|
1378
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1379
|
+
subscribe = (listener) => {
|
|
1380
|
+
this.listeners.add(listener);
|
|
1381
|
+
return () => {
|
|
1382
|
+
this.listeners.delete(listener);
|
|
1383
|
+
};
|
|
1384
|
+
};
|
|
1385
|
+
getRevision = () => this.revision;
|
|
1386
|
+
/** State/context changes notify every item; wording changes only notify their owning item. */
|
|
1387
|
+
getItemRevision = (itemId) => this.revision - this.presentationRevision + (this.itemPresentationRevisions.get(itemId) ?? 0);
|
|
1388
|
+
/** Prefer the current page, then the next surviving item, then the previous one. */
|
|
1389
|
+
pageAnchors(writtenItems = /* @__PURE__ */ new Set()) {
|
|
1390
|
+
const pages = this.pages();
|
|
1391
|
+
return [
|
|
1392
|
+
...(pages[this.pageIndex] ?? []).filter((id) => writtenItems.has(id)),
|
|
1393
|
+
...pages.slice(this.pageIndex).flat(),
|
|
1394
|
+
...pages.slice(0, this.pageIndex).flat().reverse()
|
|
1395
|
+
];
|
|
1396
|
+
}
|
|
1397
|
+
reconcilePageAndNotify(anchors) {
|
|
1398
|
+
const pages = this.pages();
|
|
1399
|
+
const pageByItem = new Map(pages.flatMap((items, index) => items.map((id) => [id, index])));
|
|
1400
|
+
const anchoredPage = anchors?.map((id) => pageByItem.get(id)).find((index) => index !== void 0);
|
|
1401
|
+
this.state.navigation.pageIndex = anchoredPage ?? Math.min(this.pageIndex, Math.max(0, pages.length - 1));
|
|
1402
|
+
this.revision++;
|
|
1403
|
+
this.listeners.forEach((listener) => listener());
|
|
1404
|
+
}
|
|
1405
|
+
constructor(currentDocument, state, admission = "strict") {
|
|
1406
|
+
this.currentDocument = currentDocument;
|
|
1407
|
+
this.state = state;
|
|
1408
|
+
this.stateGraph = new SurveyStateGraph(currentDocument, { admission });
|
|
1409
|
+
this.validateAnswers(state.answers);
|
|
1410
|
+
for (const value of Object.values(state.answers)) freezeAnswer(value);
|
|
1411
|
+
for (const value of Object.values(state.context.customValues ?? {})) freezeAnswer(value);
|
|
1412
|
+
}
|
|
1413
|
+
get document() {
|
|
1414
|
+
return this.currentDocument;
|
|
1415
|
+
}
|
|
1416
|
+
/** Wording-only authoring revisions share the compiler indexes and cannot change execution.
|
|
1417
|
+
* Keep the compiled graph, answers, navigation and initialization when updating a live preview.
|
|
1418
|
+
* A structural/configuration revision must create a new session instead.
|
|
1419
|
+
*/
|
|
1420
|
+
updatePresentation(document) {
|
|
1421
|
+
if (document === this.currentDocument) return;
|
|
1422
|
+
if (document.elements !== this.currentDocument.elements) throw new Error("Only wording revisions can update a session presentation");
|
|
1423
|
+
const affected = /* @__PURE__ */ new Set();
|
|
1424
|
+
let sharedContentChanged = false;
|
|
1425
|
+
for (const locale of /* @__PURE__ */ new Set([...document.locales, ...this.currentDocument.locales])) {
|
|
1426
|
+
const before = this.currentDocument.data.translations?.[locale];
|
|
1427
|
+
const after = document.data.translations?.[locale];
|
|
1428
|
+
if (before === after) continue;
|
|
1429
|
+
sharedContentChanged ||= before?.navigationContent !== after?.navigationContent || before?.validationMessages !== after?.validationMessages;
|
|
1430
|
+
for (const owner of document.owners.values()) if (before?.content[owner.id] !== after?.content[owner.id]) affected.add(owner.itemId);
|
|
2228
1431
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
const arr = value.value;
|
|
2233
|
-
const prec = value.precision;
|
|
2234
|
-
const fmt = (n) => prec === NumberPrecision.int ? Math.floor(n) : n;
|
|
2235
|
-
return arr.map(fmt).join("; ");
|
|
1432
|
+
if (!sharedContentChanged) {
|
|
1433
|
+
this.presentationRevision++;
|
|
1434
|
+
for (const id of affected) this.itemPresentationRevisions.set(id, (this.itemPresentationRevisions.get(id) ?? 0) + 1);
|
|
2236
1435
|
}
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
1436
|
+
this.currentDocument = document;
|
|
1437
|
+
this.reconcilePageAndNotify();
|
|
1438
|
+
}
|
|
1439
|
+
static async create(document, input, sources = {}) {
|
|
1440
|
+
if (input.mode === "resumed") {
|
|
1441
|
+
const snapshot = sessionSnapshotSchema.parse(input.snapshot);
|
|
1442
|
+
if (snapshot.binding.sessionId !== input.binding.sessionId || snapshot.binding.definitionId !== input.binding.definitionId) throw new Error("Resume snapshot does not match the session and exact survey definition");
|
|
1443
|
+
const session = new SurveySession(document, snapshot);
|
|
1444
|
+
if (session.pageIndex >= Math.max(1, session.pages().length)) throw new Error("Resume page index is outside the survey");
|
|
1445
|
+
return session;
|
|
2243
1446
|
}
|
|
2244
|
-
|
|
1447
|
+
const session = new SurveySession(document, sessionSnapshotSchema.parse({
|
|
1448
|
+
schemaVersion: 1,
|
|
1449
|
+
binding: input.binding,
|
|
1450
|
+
answers: input.answers ?? {},
|
|
1451
|
+
initialization: "complete",
|
|
1452
|
+
navigation: {
|
|
1453
|
+
pageIndex: 0,
|
|
1454
|
+
viewport: "large"
|
|
1455
|
+
},
|
|
1456
|
+
randomization: { seed: input.seed ?? crypto.randomUUID() },
|
|
1457
|
+
context: {
|
|
1458
|
+
...input.context,
|
|
1459
|
+
currentDate: input.context.currentDate ?? calendarDateFromLocalDate(/* @__PURE__ */ new Date())
|
|
1460
|
+
},
|
|
1461
|
+
events: [{
|
|
1462
|
+
type: "openSurvey",
|
|
1463
|
+
timestamp: eventTimestamp(),
|
|
1464
|
+
value: input.context.locale
|
|
1465
|
+
}]
|
|
1466
|
+
}), input.admission);
|
|
1467
|
+
await session.initialize(sources);
|
|
1468
|
+
return session;
|
|
1469
|
+
}
|
|
1470
|
+
snapshot() {
|
|
1471
|
+
return structuredClone(this.state);
|
|
1472
|
+
}
|
|
1473
|
+
itemMetadata() {
|
|
1474
|
+
return Object.fromEntries(this.pages().flat().map((itemId, position) => [itemId, { position }]));
|
|
1475
|
+
}
|
|
1476
|
+
active() {
|
|
1477
|
+
return this.stateGraph.evaluate({
|
|
1478
|
+
answers: this.state.answers,
|
|
1479
|
+
context: this.state.context
|
|
1480
|
+
});
|
|
2245
1481
|
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
if (value.type === ValueType.date) return format(/* @__PURE__ */ new Date(value.value * 1e3), pattern);
|
|
2249
|
-
if (value.type === ValueType.dateArray) return value.value.map((ts) => format(/* @__PURE__ */ new Date(ts * 1e3), pattern)).join("; ");
|
|
2250
|
-
return formatDefault(value);
|
|
2251
|
-
}
|
|
2252
|
-
function maskValue(value, maskChar) {
|
|
2253
|
-
return valueToString(value) ? maskChar : "";
|
|
2254
|
-
}
|
|
2255
|
-
function valueToString(value) {
|
|
2256
|
-
switch (value.type) {
|
|
2257
|
-
case ValueType.string:
|
|
2258
|
-
case ValueType.reference: return String(value.value);
|
|
2259
|
-
case ValueType.stringArray:
|
|
2260
|
-
case ValueType.referenceArray: return value.value.join(" ");
|
|
2261
|
-
default: return String(value.value ?? "");
|
|
1482
|
+
get locale() {
|
|
1483
|
+
return this.state.context.locale;
|
|
2262
1484
|
}
|
|
2263
|
-
|
|
1485
|
+
get pageIndex() {
|
|
1486
|
+
return this.state.navigation.pageIndex;
|
|
1487
|
+
}
|
|
1488
|
+
setLocale(locale) {
|
|
1489
|
+
this.setContext({
|
|
1490
|
+
...this.state.context,
|
|
1491
|
+
locale
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
/** Replace host context without restarting initialization or losing retained answers. */
|
|
1495
|
+
setContext(context) {
|
|
1496
|
+
const parsed = sessionSnapshotSchema.shape.context.parse(context);
|
|
1497
|
+
const previous = this.state.context;
|
|
1498
|
+
const next = {
|
|
1499
|
+
...parsed,
|
|
1500
|
+
currentDate: parsed.currentDate ?? previous.currentDate
|
|
1501
|
+
};
|
|
1502
|
+
if (!next.locale.trim()) throw new Error("A locale is required");
|
|
1503
|
+
const localeChanged = next.locale !== previous.locale;
|
|
1504
|
+
if (!localeChanged && next.currentDate === previous.currentDate && sameRecord(previous.participantFlags, next.participantFlags, (a, b) => a === b) && sameRecord(previous.customValues, next.customValues, (a, b) => JSON.stringify(a) === JSON.stringify(b))) return;
|
|
1505
|
+
for (const value of Object.values(next.customValues ?? {})) freezeAnswer(value);
|
|
1506
|
+
const anchors = this.pageAnchors();
|
|
1507
|
+
this.state = {
|
|
1508
|
+
...this.state,
|
|
1509
|
+
context: next,
|
|
1510
|
+
events: localeChanged ? [...this.state.events, {
|
|
1511
|
+
type: "languageChanged",
|
|
1512
|
+
timestamp: eventTimestamp(),
|
|
1513
|
+
value: next.locale
|
|
1514
|
+
}] : this.state.events
|
|
1515
|
+
};
|
|
1516
|
+
this.reconcilePageAndNotify(anchors);
|
|
1517
|
+
}
|
|
1518
|
+
orderedIds(ids, ownerId) {
|
|
1519
|
+
return shuffleArray(ids, createSeededRandom(`${this.state.randomization.seed}:${ownerId}`));
|
|
1520
|
+
}
|
|
1521
|
+
setViewport(viewport) {
|
|
1522
|
+
if (viewport === this.state.navigation.viewport) return;
|
|
1523
|
+
const anchors = this.pageAnchors();
|
|
1524
|
+
this.state.navigation.viewport = viewport;
|
|
1525
|
+
this.reconcilePageAndNotify(anchors);
|
|
1526
|
+
}
|
|
1527
|
+
pages(viewport = this.state.navigation.viewport) {
|
|
1528
|
+
const active = this.active(), byId = new Map(this.document.data.surveyItems.map((item) => [item.id, item]));
|
|
1529
|
+
const pages = [[]];
|
|
1530
|
+
const limit = this.document.data.maxItemsPerPage?.[viewport] ?? Infinity;
|
|
1531
|
+
const next = () => {
|
|
1532
|
+
if (pages.at(-1).length) pages.push([]);
|
|
1533
|
+
};
|
|
1534
|
+
const visit = (id) => {
|
|
1535
|
+
const item = byId.get(id);
|
|
1536
|
+
if (!item || !active.visible(id)) return;
|
|
1537
|
+
if (item.itemType === "group") (item.config.shuffleItems ? this.orderedIds(item.config.items, item.id) : item.config.items).forEach(visit);
|
|
1538
|
+
else if (item.itemType === "page-break") next();
|
|
1539
|
+
else {
|
|
1540
|
+
if (pages.at(-1).length >= limit) next();
|
|
1541
|
+
pages.at(-1).push(id);
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
visit(this.document.rootId);
|
|
1545
|
+
return pages.filter((page) => page.length);
|
|
1546
|
+
}
|
|
1547
|
+
validate(phase, itemIds) {
|
|
1548
|
+
const result = validateSurveyAnswers(this.stateGraph, this.state.answers, this.state.context, {
|
|
1549
|
+
phase,
|
|
1550
|
+
itemIds
|
|
1551
|
+
});
|
|
1552
|
+
return {
|
|
1553
|
+
...result,
|
|
1554
|
+
diagnostics: [...this.initializationDiagnostics, ...result.diagnostics]
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
goToPage(index, viewport = this.state.navigation.viewport) {
|
|
1558
|
+
const pages = this.pages(viewport);
|
|
1559
|
+
if (!Number.isInteger(index) || index < 0 || index >= pages.length) throw new Error("Invalid page index");
|
|
1560
|
+
if (index > this.pageIndex) {
|
|
1561
|
+
if (this.validate("page", pages[this.pageIndex]).issues.length) throw new Error("Current page has blocking validation errors");
|
|
1562
|
+
}
|
|
1563
|
+
this.state = {
|
|
1564
|
+
...this.state,
|
|
1565
|
+
navigation: {
|
|
1566
|
+
pageIndex: index,
|
|
1567
|
+
viewport
|
|
1568
|
+
},
|
|
1569
|
+
events: [...this.state.events, {
|
|
1570
|
+
type: "pageChanged",
|
|
1571
|
+
timestamp: eventTimestamp(),
|
|
1572
|
+
value: index
|
|
1573
|
+
}]
|
|
1574
|
+
};
|
|
1575
|
+
this.reconcilePageAndNotify();
|
|
1576
|
+
}
|
|
1577
|
+
submission(responseId) {
|
|
1578
|
+
if (this.validate("submit").issues.length) throw new Error("Survey has blocking validation errors");
|
|
1579
|
+
return structuredClone({
|
|
1580
|
+
schemaVersion: 2,
|
|
1581
|
+
responseId,
|
|
1582
|
+
definitionId: this.state.binding.definitionId,
|
|
1583
|
+
answers: this.active().activeAnswers(),
|
|
1584
|
+
events: this.state.events,
|
|
1585
|
+
itemMetadata: this.itemMetadata(),
|
|
1586
|
+
customValues: this.state.context.customValues,
|
|
1587
|
+
evaluationContext: {
|
|
1588
|
+
locale: this.state.context.locale,
|
|
1589
|
+
currentDate: this.state.context.currentDate,
|
|
1590
|
+
participantFlags: this.state.context.participantFlags
|
|
1591
|
+
},
|
|
1592
|
+
randomization: this.state.randomization
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
validateAnswers(answers) {
|
|
1596
|
+
for (const [slotId, value] of Object.entries(answers)) {
|
|
1597
|
+
const slot = this.document.slots.get(slotId);
|
|
1598
|
+
if (!slot || slot.computed) throw new Error(`Unknown or computed answer target '${slotId}'`);
|
|
1599
|
+
const errors = validateResponseSlotValue(slot, value);
|
|
1600
|
+
if (errors.length) throw new Error(errors.join("; "));
|
|
1601
|
+
if (value.type === "reference[]" && new Set(value.value).size !== value.value.length) throw new Error(`Duplicate selected options in '${slotId}'`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
write(writes) {
|
|
1605
|
+
if (new Set(writes.map((write) => write.slotId)).size !== writes.length) throw new Error("One action cannot write a slot twice");
|
|
1606
|
+
const before = this.active();
|
|
1607
|
+
const changed = writes.filter(({ slotId, value }) => {
|
|
1608
|
+
const previous = ownValue(this.state.answers, slotId);
|
|
1609
|
+
return previous === void 0 || value === void 0 ? previous !== value : comparableAnswer(previous) !== comparableAnswer(value);
|
|
1610
|
+
});
|
|
1611
|
+
for (const { slotId, value } of writes) {
|
|
1612
|
+
const slot = this.document.slots.get(slotId);
|
|
1613
|
+
const binding = this.stateGraph.bindings.get(slotId);
|
|
1614
|
+
if (!slot || slot.computed || !binding) throw new Error(`Slot '${slotId}' is not a writable input`);
|
|
1615
|
+
if (value !== void 0) this.validateAnswers({ [slotId]: value });
|
|
1616
|
+
}
|
|
1617
|
+
for (const { slotId, value } of changed) {
|
|
1618
|
+
const binding = this.stateGraph.bindings.get(slotId);
|
|
1619
|
+
if (!canWriteSelection(binding, ownValue(this.state.answers, slotId), value, before)) throw new Error(`Participant write to '${slotId}' is not permitted in the current state`);
|
|
1620
|
+
}
|
|
1621
|
+
if (!changed.length) return;
|
|
1622
|
+
const anchors = this.pageAnchors(new Set(changed.map(({ slotId }) => this.document.slots.get(slotId).itemId)));
|
|
1623
|
+
const answers = { ...this.state.answers };
|
|
1624
|
+
const timestamp = eventTimestamp();
|
|
1625
|
+
for (const { slotId, value } of changed) if (value === void 0) delete answers[slotId];
|
|
1626
|
+
else setOwnValue(answers, slotId, freezeAnswer(structuredClone(value)));
|
|
1627
|
+
const events = [...this.state.events];
|
|
1628
|
+
for (const { slotId, value } of changed) {
|
|
1629
|
+
const event = {
|
|
1630
|
+
type: "responseChanged",
|
|
1631
|
+
slotId,
|
|
1632
|
+
operation: value === void 0 ? "clear" : "set",
|
|
1633
|
+
timestamp
|
|
1634
|
+
};
|
|
1635
|
+
const previous = events.at(-1);
|
|
1636
|
+
if (previous?.type === event.type && previous.slotId === slotId && previous.operation === event.operation) events[events.length - 1] = event;
|
|
1637
|
+
else events.push(event);
|
|
1638
|
+
}
|
|
1639
|
+
this.state = {
|
|
1640
|
+
...this.state,
|
|
1641
|
+
answers,
|
|
1642
|
+
events
|
|
1643
|
+
};
|
|
1644
|
+
this.reconcilePageAndNotify(anchors);
|
|
1645
|
+
}
|
|
1646
|
+
evaluate(expression) {
|
|
1647
|
+
const active = this.active();
|
|
1648
|
+
return evaluateExpression(expression, {
|
|
1649
|
+
...this.state.context,
|
|
1650
|
+
answer: active.answer,
|
|
1651
|
+
disabled: active.disabled
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
async initialize(sources) {
|
|
1655
|
+
const prefills = new Map([...this.document.elements.values()].flatMap((element) => Object.entries(element.node.prefills ?? {})));
|
|
1656
|
+
for (const slotId of this.stateGraph.initializationOrder) {
|
|
1657
|
+
if (Object.hasOwn(this.state.answers, slotId)) continue;
|
|
1658
|
+
for (const [index, candidate] of (prefills.get(slotId) ?? []).entries()) {
|
|
1659
|
+
const path = [
|
|
1660
|
+
...this.document.elements.get(this.document.slots.get(slotId).elementId).path,
|
|
1661
|
+
"prefills",
|
|
1662
|
+
slotId,
|
|
1663
|
+
index
|
|
1664
|
+
];
|
|
1665
|
+
const warn = (message) => this.initializationDiagnostics.push({
|
|
1666
|
+
code: "prefill-value",
|
|
1667
|
+
severity: "warning",
|
|
1668
|
+
path,
|
|
1669
|
+
message
|
|
1670
|
+
});
|
|
1671
|
+
const evaluate = (expression) => {
|
|
1672
|
+
try {
|
|
1673
|
+
return this.evaluate(expression);
|
|
1674
|
+
} catch (error) {
|
|
1675
|
+
if (!(error instanceof ExpressionEvaluationError)) throw error;
|
|
1676
|
+
warn(`Prefill '${candidate.id}' was skipped: ${error.message}`);
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
if (candidate.when) {
|
|
1681
|
+
const result = evaluate(candidate.when);
|
|
1682
|
+
if (result?.type !== "boolean" || !result.value) continue;
|
|
1683
|
+
}
|
|
1684
|
+
let value;
|
|
1685
|
+
switch (candidate.source.type) {
|
|
1686
|
+
case "static":
|
|
1687
|
+
value = candidate.source.value;
|
|
1688
|
+
break;
|
|
1689
|
+
case "expression":
|
|
1690
|
+
value = evaluate(candidate.source.expression);
|
|
1691
|
+
break;
|
|
1692
|
+
case "templateValue": {
|
|
1693
|
+
const template = ownValue(this.document.data.templateValues, candidate.source.key);
|
|
1694
|
+
if (template?.expression) {
|
|
1695
|
+
value = evaluate(template.expression);
|
|
1696
|
+
if (template.type === "formatDate" && value?.type === "date") value = {
|
|
1697
|
+
type: "string",
|
|
1698
|
+
value: formatCalendarDate(value.value, template.dateFormat)
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
break;
|
|
1702
|
+
}
|
|
1703
|
+
case "previousResponse":
|
|
1704
|
+
if (!sources.previousResponse && candidate.source.ifUnsupported !== "skip") throw new Error("This host does not supply previous responses for prefills");
|
|
1705
|
+
value = await sources.previousResponse?.(candidate.source.ref);
|
|
1706
|
+
}
|
|
1707
|
+
if (value === void 0) continue;
|
|
1708
|
+
if (candidate.source.type === "previousResponse") {
|
|
1709
|
+
const parsed = responseValueSchema.safeParse(value);
|
|
1710
|
+
const slot = this.document.slots.get(slotId);
|
|
1711
|
+
if (parsed.success && parsed.data.type === "reference[]" && slot.valueType === "reference[]" && slot.allowedValues) {
|
|
1712
|
+
const allowed = new Set(slot.allowedValues.flatMap(responseValueElements).flatMap((option) => option.type === "reference" ? [option.value] : []));
|
|
1713
|
+
const retained = parsed.data.value.filter((id) => allowed.has(id));
|
|
1714
|
+
if (retained.length !== parsed.data.value.length) {
|
|
1715
|
+
warn(`Prefill '${candidate.id}' discarded options outside the current response domain`);
|
|
1716
|
+
if (!retained.length) continue;
|
|
1717
|
+
value = {
|
|
1718
|
+
type: "reference[]",
|
|
1719
|
+
value: retained
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
const issues = this.document.slots.validateAnswers({ [slotId]: value });
|
|
1725
|
+
if (issues.length) {
|
|
1726
|
+
warn(`Prefill '${candidate.id}' was skipped: ${issues.map((issue) => issue.message).join("; ")}`);
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
setOwnValue(this.state.answers, slotId, freezeAnswer(structuredClone(value)));
|
|
1730
|
+
break;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
2264
1735
|
//#endregion
|
|
2265
1736
|
//#region src/response-exporter/csv-serializer.ts
|
|
2266
1737
|
const DEFAULT_OPTIONS = {
|
|
@@ -2296,182 +1767,229 @@ function serializeToCsv(columns, rows, options) {
|
|
|
2296
1767
|
return lines.join(opts.lineEnding);
|
|
2297
1768
|
}
|
|
2298
1769
|
//#endregion
|
|
2299
|
-
//#region src/response-exporter/
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
return `${itemId}::${slotId}`;
|
|
2305
|
-
}
|
|
2306
|
-
function metaColumnId(metaField) {
|
|
2307
|
-
return `meta:${metaField}`;
|
|
2308
|
-
}
|
|
2309
|
-
function contextColumnId(contextKey) {
|
|
2310
|
-
return `context:${contextKey}`;
|
|
2311
|
-
}
|
|
2312
|
-
function formatMetaValue(response, metaField) {
|
|
2313
|
-
switch (metaField) {
|
|
2314
|
-
case "submittedAt":
|
|
2315
|
-
if (response.submittedAt == null || response.submittedAt === 0) return "";
|
|
2316
|
-
return format(/* @__PURE__ */ new Date(response.submittedAt * 1e3), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx");
|
|
2317
|
-
case "participantId": return response.participantId ?? "";
|
|
2318
|
-
case "responseId": return response.key;
|
|
2319
|
-
case "versionId": return response.versionId;
|
|
2320
|
-
default: return "";
|
|
2321
|
-
}
|
|
1770
|
+
//#region src/response-exporter/validate-configuration.ts
|
|
1771
|
+
/** Export requires an executable definition, including complete expression operands. */
|
|
1772
|
+
function validateExportConfiguration(survey, registry = survey.slots) {
|
|
1773
|
+
registry.assertValid();
|
|
1774
|
+
new SurveyStateGraph(survey);
|
|
2322
1775
|
}
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
const
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
});
|
|
1776
|
+
//#endregion
|
|
1777
|
+
//#region src/response-exporter/exporter.ts
|
|
1778
|
+
const canonical = (value) => JSON.stringify(value, (_key, entry) => entry && typeof entry === "object" && !Array.isArray(entry) ? Object.fromEntries(Object.entries(entry).sort(([a], [b]) => a.localeCompare(b))) : entry);
|
|
1779
|
+
/** Export only against schemas selected by the host, never against response version tags. */
|
|
1780
|
+
var SurveyResponseExporter = class SurveyResponseExporter {
|
|
1781
|
+
batches;
|
|
1782
|
+
options;
|
|
1783
|
+
constructor(batches, options = {}) {
|
|
1784
|
+
this.batches = batches;
|
|
1785
|
+
this.options = options;
|
|
1786
|
+
if (!batches.length) throw new Error("Supply at least one schema-bound export batch");
|
|
1787
|
+
if (batches.some((batch) => !batch.schema || !Array.isArray(batch.responses))) throw new Error("Each export batch requires a schema and responses array");
|
|
1788
|
+
if (batches.length > 1 && !options.codingPolicy) throw new Error("Combined export requires an explicit original or harmonized coding policy");
|
|
1789
|
+
}
|
|
1790
|
+
batch(index) {
|
|
1791
|
+
const batch = this.batches[index];
|
|
1792
|
+
if (!batch) throw new Error(`Unknown export batch: ${index}`);
|
|
1793
|
+
return batch;
|
|
1794
|
+
}
|
|
1795
|
+
profile(batch) {
|
|
1796
|
+
if (!this.options.profile) return batch.profile;
|
|
1797
|
+
return {
|
|
1798
|
+
...batch.profile,
|
|
1799
|
+
...this.options.profile,
|
|
1800
|
+
slotOverrides: {
|
|
1801
|
+
...batch.profile?.slotOverrides,
|
|
1802
|
+
...this.options.profile.slotOverrides
|
|
1803
|
+
},
|
|
1804
|
+
columnHeaders: {
|
|
1805
|
+
...batch.profile?.columnHeaders,
|
|
1806
|
+
...this.options.profile.columnHeaders
|
|
1807
|
+
},
|
|
1808
|
+
contextOverrides: {
|
|
1809
|
+
...batch.profile?.contextOverrides,
|
|
1810
|
+
...this.options.profile.contextOverrides
|
|
2359
1811
|
}
|
|
2360
|
-
}
|
|
2361
|
-
return columns;
|
|
1812
|
+
};
|
|
2362
1813
|
}
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
id: contextColumnId(contextKey),
|
|
2394
|
-
header: this.resolveColumnHeader(profile, contextColumnId(contextKey), `context.${contextKey}`),
|
|
2395
|
-
source: ExportColumnSource.context,
|
|
2396
|
-
contextKey
|
|
1814
|
+
compileContexts() {
|
|
1815
|
+
return new Map(this.batches.map((batch) => {
|
|
1816
|
+
const registry = batch.schema.slots;
|
|
1817
|
+
validateExportConfiguration(batch.schema, registry);
|
|
1818
|
+
const slots = new Map(localizedExportSlots(batch.schema).map((slot) => [slot.slotId, slot]));
|
|
1819
|
+
const profile = this.profile(batch);
|
|
1820
|
+
const transforms = new Map([...slots.values()].map((slot) => [slot.slotId, {
|
|
1821
|
+
...DEFAULT_TRANSFORM,
|
|
1822
|
+
...slot.exportSettings,
|
|
1823
|
+
...ownValue(profile?.slotOverrides, slot.slotId)
|
|
1824
|
+
}]));
|
|
1825
|
+
const columns = [...slots.values()].flatMap((slot) => projectSlot(slot, transforms.get(slot.slotId)).map((column) => ({
|
|
1826
|
+
...column,
|
|
1827
|
+
header: ownValue(profile?.columnHeaders, column.id)?.trim() ?? column.header
|
|
1828
|
+
})));
|
|
1829
|
+
const encodings = /* @__PURE__ */ new Map();
|
|
1830
|
+
for (const slotId of new Set(columns.map((column) => column.slotId))) {
|
|
1831
|
+
const { include: _include, ...encoding } = resolvedSlotEncoding(slots.get(slotId), transforms.get(slotId));
|
|
1832
|
+
encodings.set(slotId, canonical(encoding));
|
|
1833
|
+
}
|
|
1834
|
+
return [batch, {
|
|
1835
|
+
registry,
|
|
1836
|
+
slots,
|
|
1837
|
+
profile,
|
|
1838
|
+
transforms,
|
|
1839
|
+
encodings,
|
|
1840
|
+
columns,
|
|
1841
|
+
included: new Set(columns.map((column) => column.id)),
|
|
1842
|
+
stateGraph: new SurveyStateGraph(batch.schema)
|
|
1843
|
+
}];
|
|
2397
1844
|
}));
|
|
2398
|
-
const respOrder = profile?.responseColumnOrder ?? [];
|
|
2399
|
-
const byOrder = respOrder.filter((id) => responseCols.some((c) => c.id === id)).map((id) => responseCols.find((c) => c.id === id));
|
|
2400
|
-
const remaining = responseCols.filter((c) => !respOrder.includes(c.id)).sort((a, b) => a.header.localeCompare(b.header));
|
|
2401
|
-
const respCols = [...byOrder, ...remaining];
|
|
2402
|
-
const columns = [
|
|
2403
|
-
...metaColumns,
|
|
2404
|
-
...contextCols,
|
|
2405
|
-
...respCols
|
|
2406
|
-
];
|
|
2407
|
-
this.assertUniqueHeaders(columns);
|
|
2408
|
-
return columns;
|
|
2409
|
-
}
|
|
2410
|
-
resolveProfileForVersion(versionId) {
|
|
2411
|
-
return this.profiles.find((p) => !p.versionId || p.versionId === versionId);
|
|
2412
|
-
}
|
|
2413
|
-
resolveColumnHeader(profile, columnId, defaultHeader) {
|
|
2414
|
-
const override = profile?.columnHeaders?.[columnId]?.trim();
|
|
2415
|
-
return override && override.length > 0 ? override : defaultHeader;
|
|
2416
|
-
}
|
|
2417
|
-
assertUniqueHeaders(columns) {
|
|
2418
|
-
const seen = /* @__PURE__ */ new Map();
|
|
2419
|
-
for (const column of columns) {
|
|
2420
|
-
const existingId = seen.get(column.header);
|
|
2421
|
-
if (existingId && existingId !== column.id) throw new Error(`Duplicate export column header '${column.header}' for columns '${existingId}' and '${column.id}'`);
|
|
2422
|
-
seen.set(column.header, column.id);
|
|
2423
|
-
}
|
|
2424
1845
|
}
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
for (const
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
1846
|
+
validateProfile(profile, batches, contexts, extraResponses = []) {
|
|
1847
|
+
if (!profile) return;
|
|
1848
|
+
if (profile.schemaVersion !== 1) throw new Error(`Unsupported exporter profile schema version: ${String(profile.schemaVersion)}`);
|
|
1849
|
+
const slots = new Map(batches.flatMap((batch) => [...contexts.get(batch).slots]));
|
|
1850
|
+
const contextKeys = new Set([...batches.flatMap((batch) => batch.responses), ...extraResponses].flatMap((response) => Object.keys(response.customValues ?? {})));
|
|
1851
|
+
const columns = /* @__PURE__ */ new Set([
|
|
1852
|
+
...[...slots.values()].flatMap((slot) => projectSlot(slot, { mode: "default" }, true).map((column) => column.id)),
|
|
1853
|
+
...META_COLUMN_ORDER.map((field) => encodeExportColumnId(["meta", field])),
|
|
1854
|
+
...[...contextKeys].map((key) => encodeExportColumnId(["context", key]))
|
|
1855
|
+
]);
|
|
1856
|
+
for (const id of Object.keys(profile.slotOverrides ?? {})) if (!slots.has(id)) throw new Error(`Profile references unknown response slot "${id}"`);
|
|
1857
|
+
for (const id of [...Object.keys(profile.columnHeaders ?? {}), ...profile.responseColumnOrder ?? []]) if (!columns.has(id)) throw new Error(`Profile references unknown export column "${id}"`);
|
|
1858
|
+
for (const id of profile.responseColumnOrder ?? []) if (![...slots.values()].some((slot) => projectSlot(slot, { mode: "default" }, true).some((column) => column.id === id))) throw new Error(`Response column order contains a non-response column: ${id}`);
|
|
1859
|
+
for (const key of [...Object.keys(profile.contextOverrides ?? {}), ...profile.contextColumnOrder ?? []]) if (!contextKeys.has(key)) throw new Error(`Profile references unknown context key "${key}"`);
|
|
1860
|
+
for (const slot of slots.values()) if ((slot.naming?.kind === "matrix" || slot.rowId || slot.columnId) && projectSlot(slot, { mode: "default" }, true).some((column) => Object.hasOwn(profile.columnHeaders ?? {}, column.id))) throw new Error(`Matrix variable "${slot.slotId}" cannot have an individual header override; edit its matrix coding components`);
|
|
1861
|
+
for (const [id, header] of Object.entries(profile.columnHeaders ?? {})) if (typeof header !== "string" || !header.trim()) throw new Error(`Export header for "${id}" cannot be empty`);
|
|
1862
|
+
if (new Set(profile.responseColumnOrder).size !== (profile.responseColumnOrder?.length ?? 0)) throw new Error("Response column order contains duplicate identities");
|
|
1863
|
+
}
|
|
1864
|
+
computeResponseColumns(batchIndex = 0, extraResponses = []) {
|
|
1865
|
+
const batch = this.batch(batchIndex);
|
|
1866
|
+
const contexts = this.compileContexts();
|
|
1867
|
+
this.validateProfile(batch.profile, [batch], contexts, extraResponses);
|
|
1868
|
+
this.validateProfile(this.options.profile, this.batches, contexts, extraResponses);
|
|
1869
|
+
return contexts.get(batch).columns;
|
|
1870
|
+
}
|
|
1871
|
+
computeColumns(extraResponses = []) {
|
|
1872
|
+
return this.columns(this.compileContexts(), extraResponses);
|
|
1873
|
+
}
|
|
1874
|
+
columns(contexts, extraResponses = []) {
|
|
1875
|
+
this.validateProfile(this.options.profile, this.batches, contexts, extraResponses);
|
|
1876
|
+
const columns = /* @__PURE__ */ new Map();
|
|
1877
|
+
const signatures = /* @__PURE__ */ new Map();
|
|
1878
|
+
const encodings = /* @__PURE__ */ new Map();
|
|
1879
|
+
this.batches.forEach((batch) => {
|
|
1880
|
+
const context = contexts.get(batch);
|
|
1881
|
+
this.validateProfile(batch.profile, [batch], contexts, extraResponses);
|
|
1882
|
+
for (const slot of context.slots.values()) {
|
|
1883
|
+
const signature = canonical({
|
|
1884
|
+
type: slot.valueType,
|
|
1885
|
+
allowedValues: slot.allowedValues?.map((value) => canonical(value)).sort(),
|
|
1886
|
+
codes: this.options.codingPolicy === "harmonized" ? ownValue(context.profile?.slotOverrides, slot.slotId)?.optionCodes ?? slot.referenceValueKeys : slot.referenceValueKeys,
|
|
1887
|
+
computed: slot.computed,
|
|
1888
|
+
scores: slot.referenceValueScores
|
|
2448
1889
|
});
|
|
2449
|
-
|
|
1890
|
+
const previous = signatures.get(slot.slotId);
|
|
1891
|
+
if (previous && previous !== signature) throw new Error(`Incompatible schema definitions for slot "${slot.slotId}"; supply a compatible explicit harmonization mapping`);
|
|
1892
|
+
signatures.set(slot.slotId, signature);
|
|
2450
1893
|
}
|
|
2451
|
-
|
|
2452
|
-
const
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
1894
|
+
for (const column of context.columns) {
|
|
1895
|
+
const signature = context.encodings.get(column.slotId);
|
|
1896
|
+
if (encodings.has(column.id) && encodings.get(column.id) !== signature) throw new Error(`Incompatible export encodings for column "${column.id}"; supply one harmonized profile`);
|
|
1897
|
+
encodings.set(column.id, signature);
|
|
1898
|
+
const existing = columns.get(column.id);
|
|
1899
|
+
if (existing && existing.header !== column.header) throw new Error(`Conflicting names for column "${column.id}"; select an explicit harmonized output header`);
|
|
1900
|
+
columns.set(column.id, column);
|
|
1901
|
+
}
|
|
1902
|
+
});
|
|
1903
|
+
const profile = this.options.profile ?? (this.batches.length === 1 ? this.batches[0].profile : void 0);
|
|
1904
|
+
const allResponses = [...this.batches.flatMap((batch) => batch.responses), ...extraResponses];
|
|
1905
|
+
const contextKeys = [...new Set(allResponses.flatMap((response) => Object.keys(response.customValues ?? {})))];
|
|
1906
|
+
const ordered = (values, order, id, name) => values.sort((a, b) => {
|
|
1907
|
+
const left = order.indexOf(id(a)), right = order.indexOf(id(b));
|
|
1908
|
+
if (left >= 0 || right >= 0) return (left < 0 ? Infinity : left) - (right < 0 ? Infinity : right);
|
|
1909
|
+
return name(a).localeCompare(name(b));
|
|
1910
|
+
});
|
|
1911
|
+
const result = [
|
|
1912
|
+
...META_COLUMN_ORDER.map((field) => ({
|
|
1913
|
+
id: encodeExportColumnId(["meta", field]),
|
|
1914
|
+
header: profile?.columnHeaders?.[encodeExportColumnId(["meta", field])]?.trim() ?? field,
|
|
1915
|
+
source: ExportColumnSource.meta,
|
|
1916
|
+
metaField: field
|
|
1917
|
+
})),
|
|
1918
|
+
...ordered(contextKeys, profile?.contextColumnOrder ?? [], (value) => value, (value) => value).map((key) => ({
|
|
1919
|
+
id: encodeExportColumnId(["context", key]),
|
|
1920
|
+
header: profile?.columnHeaders?.[encodeExportColumnId(["context", key])]?.trim() ?? `context.${key}`,
|
|
1921
|
+
source: ExportColumnSource.context,
|
|
1922
|
+
contextKey: key
|
|
1923
|
+
})),
|
|
1924
|
+
...ordered([...columns.values()], profile?.responseColumnOrder ?? [], (value) => value.id, (value) => value.header)
|
|
1925
|
+
];
|
|
1926
|
+
const headers = /* @__PURE__ */ new Set();
|
|
1927
|
+
for (const column of result) {
|
|
1928
|
+
if (headers.has(column.header)) throw new Error(`Duplicate export column header '${column.header}'`);
|
|
1929
|
+
headers.add(column.header);
|
|
1930
|
+
}
|
|
1931
|
+
return result;
|
|
1932
|
+
}
|
|
1933
|
+
row(response, batch, columns, context) {
|
|
1934
|
+
const { registry, slots, profile, included, transforms } = context;
|
|
1935
|
+
const diagnostics = registry.validateAnswers(response.answers);
|
|
1936
|
+
if (diagnostics.length) throw new Error(diagnostics.map((issue) => issue.message).join("\n"));
|
|
1937
|
+
const row = Object.create(null);
|
|
1938
|
+
const active = context.stateGraph.evaluate({
|
|
1939
|
+
answers: response.answers,
|
|
1940
|
+
context: {
|
|
1941
|
+
locale: response.evaluationContext?.locale ?? batch.schema.locales[0] ?? "en",
|
|
1942
|
+
currentDate: response.evaluationContext?.currentDate,
|
|
1943
|
+
participantFlags: response.evaluationContext?.participantFlags,
|
|
1944
|
+
customValues: response.customValues
|
|
2459
1945
|
}
|
|
1946
|
+
});
|
|
1947
|
+
for (const column of columns) if (column.source === ExportColumnSource.meta) switch (column.metaField) {
|
|
1948
|
+
case "responseId":
|
|
1949
|
+
row[column.header] = response.responseId;
|
|
1950
|
+
break;
|
|
1951
|
+
case "participantId":
|
|
1952
|
+
row[column.header] = response.participantId ?? "";
|
|
1953
|
+
break;
|
|
1954
|
+
case "sourceId":
|
|
1955
|
+
row[column.header] = batch.sourceId ?? "";
|
|
1956
|
+
break;
|
|
1957
|
+
case "submittedAt": row[column.header] = response.submittedAt ? format(/* @__PURE__ */ new Date(response.submittedAt * 1e3), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx") : "";
|
|
1958
|
+
}
|
|
1959
|
+
else if (column.source === ExportColumnSource.context) {
|
|
1960
|
+
const value = ownValue(response.customValues, column.contextKey);
|
|
1961
|
+
row[column.header] = exportSingleResponseSlot({
|
|
1962
|
+
value,
|
|
1963
|
+
valueType: value?.type ?? ValueType.string,
|
|
1964
|
+
transformConfig: ownValue(profile?.contextOverrides, column.contextKey) ?? {
|
|
1965
|
+
...DEFAULT_TRANSFORM,
|
|
1966
|
+
categorical: "ids"
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
} else {
|
|
1970
|
+
const slot = slots.get(column.slotId);
|
|
1971
|
+
row[column.header] = slot && included.has(column.id) ? formatProjectedValue(slot, column, active.answer(slot.slotId), transforms.get(slot.slotId)) : "";
|
|
2460
1972
|
}
|
|
2461
1973
|
return row;
|
|
2462
1974
|
}
|
|
2463
|
-
/**
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
1975
|
+
/** Explicitly associate one response with a supplied batch schema. */
|
|
1976
|
+
exportResponse(response, batchIndex = 0) {
|
|
1977
|
+
return new SurveyResponseExporter([{
|
|
1978
|
+
...this.batch(batchIndex),
|
|
1979
|
+
responses: [response]
|
|
1980
|
+
}], { profile: this.options.profile }).exportRows()[0];
|
|
1981
|
+
}
|
|
1982
|
+
exportRows() {
|
|
1983
|
+
const contexts = this.compileContexts();
|
|
1984
|
+
return this.rows(this.columns(contexts), contexts);
|
|
1985
|
+
}
|
|
1986
|
+
rows(columns, contexts) {
|
|
1987
|
+
return this.batches.flatMap((batch) => batch.responses.map((response) => this.row(response, batch, columns, contexts.get(batch))));
|
|
2471
1988
|
}
|
|
2472
|
-
exportResponsesToCsv(
|
|
2473
|
-
|
|
2474
|
-
|
|
1989
|
+
exportResponsesToCsv(options) {
|
|
1990
|
+
const contexts = this.compileContexts();
|
|
1991
|
+
const columns = this.columns(contexts);
|
|
1992
|
+
return serializeToCsv(columns, this.rows(columns, contexts), options);
|
|
2475
1993
|
}
|
|
2476
1994
|
};
|
|
2477
1995
|
//#endregion
|
|
@@ -2480,51 +1998,123 @@ var SurveyResponseExporter = class {
|
|
|
2480
1998
|
* Generate a structured codebook from a survey definition.
|
|
2481
1999
|
*/
|
|
2482
2000
|
function generateCodebook(survey, options) {
|
|
2001
|
+
const registry = survey.slots;
|
|
2002
|
+
validateExportConfiguration(survey, registry);
|
|
2003
|
+
const slots = localizedExportSlots(survey);
|
|
2483
2004
|
const locales = options?.locales ?? survey.locales;
|
|
2484
|
-
const
|
|
2485
|
-
const
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2005
|
+
const slotDefsByItemId = /* @__PURE__ */ new Map();
|
|
2006
|
+
for (const slot of slots) {
|
|
2007
|
+
const entries = slotDefsByItemId.get(slot.itemId) ?? [];
|
|
2008
|
+
entries.push({
|
|
2009
|
+
slotId: slot.slotId,
|
|
2010
|
+
elementId: slot.elementId,
|
|
2011
|
+
...slot.rowId ? { rowId: slot.rowId } : {},
|
|
2012
|
+
...slot.columnId ? { columnId: slot.columnId } : {},
|
|
2013
|
+
variableName: slot.variableName,
|
|
2014
|
+
valueType: slot.valueType,
|
|
2015
|
+
encoding: resolvedSlotEncoding(slot, {
|
|
2016
|
+
mode: "default",
|
|
2017
|
+
...slot.exportSettings,
|
|
2018
|
+
...ownValue(options?.profile?.slotOverrides, slot.slotId)
|
|
2019
|
+
}),
|
|
2020
|
+
optionLabels: slot.referenceValueLabels,
|
|
2021
|
+
...slot.allowedValues ? { allowedValues: slot.allowedValues.map((value) => structuredClone(value)) } : {},
|
|
2022
|
+
...slot.computed ? { computed: {
|
|
2023
|
+
...slot.computed,
|
|
2024
|
+
sourceSlotIds: [...slot.computed.sourceSlotIds]
|
|
2025
|
+
} } : {},
|
|
2026
|
+
...slot.referenceValueKeys ? { referenceValueKeys: { ...slot.referenceValueKeys } } : {},
|
|
2027
|
+
...slot.referenceValueScores ? { referenceValueScores: { ...slot.referenceValueScores } } : {}
|
|
2028
|
+
});
|
|
2029
|
+
slotDefsByItemId.set(slot.itemId, entries);
|
|
2030
|
+
}
|
|
2492
2031
|
const items = [];
|
|
2493
|
-
for (const item of survey.surveyItems
|
|
2494
|
-
const keyInfo = keyByItemId.get(item.id);
|
|
2495
|
-
const key = keyInfo?.key ?? item.id;
|
|
2496
|
-
const keyPath = keyInfo?.keyPath ?? [];
|
|
2032
|
+
for (const item of survey.data.surveyItems) {
|
|
2497
2033
|
const slotList = slotDefsByItemId.get(item.id) ?? [];
|
|
2498
2034
|
slotList.sort((a, b) => a.slotId.localeCompare(b.slotId));
|
|
2499
2035
|
let contentByLocale;
|
|
2500
2036
|
if (locales.length > 0) {
|
|
2501
2037
|
contentByLocale = {};
|
|
2502
|
-
const
|
|
2503
|
-
|
|
2504
|
-
const
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
for (const [contentKey, content] of Object.entries(all)) flat[contentKey] = getContentPlainText(content);
|
|
2508
|
-
contentByLocale[locale] = flat;
|
|
2038
|
+
for (const locale of locales) {
|
|
2039
|
+
const values = {};
|
|
2040
|
+
for (const owner of survey.owners.values()) if (owner.itemId === item.id) {
|
|
2041
|
+
const roles = ownValue(ownValue(survey.data.translations, locale)?.content, owner.id);
|
|
2042
|
+
if (roles) setOwnValue(values, owner.id, Object.fromEntries(Object.entries(roles).map(([role, content]) => [role, getContentPlainText(content)])));
|
|
2509
2043
|
}
|
|
2044
|
+
setOwnValue(contentByLocale, locale, values);
|
|
2510
2045
|
}
|
|
2511
2046
|
}
|
|
2512
2047
|
items.push({
|
|
2513
2048
|
id: item.id,
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
type: item.
|
|
2517
|
-
interactive:
|
|
2049
|
+
editorName: survey.resolveItemEditorName(item.id, locales[0]),
|
|
2050
|
+
breadcrumb: survey.getItemDisplayPath(item.id, locales[0]).breadcrumb,
|
|
2051
|
+
type: item.itemType,
|
|
2052
|
+
interactive: slotList.some((slot) => !slot.computed),
|
|
2518
2053
|
slots: slotList,
|
|
2519
2054
|
contentByLocale
|
|
2520
2055
|
});
|
|
2521
2056
|
}
|
|
2522
2057
|
return {
|
|
2523
|
-
surveyKey: survey.surveyKey,
|
|
2524
|
-
items
|
|
2058
|
+
surveyKey: survey.data.metadata?.surveyKey,
|
|
2059
|
+
items,
|
|
2060
|
+
projection: new SurveyResponseExporter([{
|
|
2061
|
+
schema: survey,
|
|
2062
|
+
responses: [],
|
|
2063
|
+
profile: options?.profile
|
|
2064
|
+
}]).computeColumns(),
|
|
2065
|
+
profile: options?.profile && structuredClone(options.profile),
|
|
2066
|
+
missingValue: ""
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
/** Describe precisely the combined projection and each host-selected source definition. */
|
|
2070
|
+
function generateExportCodebook(batches, options = {}) {
|
|
2071
|
+
const exporter = new SurveyResponseExporter(batches, options);
|
|
2072
|
+
return {
|
|
2073
|
+
schemaVersion: 1,
|
|
2074
|
+
codingPolicy: options.codingPolicy ?? "original",
|
|
2075
|
+
columns: exporter.computeColumns(),
|
|
2076
|
+
missingValue: "",
|
|
2077
|
+
profile: options.profile && structuredClone(options.profile),
|
|
2078
|
+
sources: batches.map((batch) => ({
|
|
2079
|
+
sourceId: batch.sourceId,
|
|
2080
|
+
surveySchemaVersion: batch.schema.serialize().schemaVersion,
|
|
2081
|
+
profile: batch.profile && structuredClone(batch.profile),
|
|
2082
|
+
variables: localizedExportSlots(batch.schema).map((slot) => ({
|
|
2083
|
+
...slot,
|
|
2084
|
+
encoding: resolvedSlotEncoding(slot, {
|
|
2085
|
+
mode: "default",
|
|
2086
|
+
...slot.exportSettings,
|
|
2087
|
+
...ownValue(options.profile?.slotOverrides, slot.slotId) ?? ownValue(batch.profile?.slotOverrides, slot.slotId)
|
|
2088
|
+
})
|
|
2089
|
+
}))
|
|
2090
|
+
}))
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
//#endregion
|
|
2094
|
+
//#region src/engine/submission.ts
|
|
2095
|
+
/** The host chooses the exact definition and trusted context before calling this boundary. */
|
|
2096
|
+
function validateSurveySubmission(survey, input, context, binding) {
|
|
2097
|
+
const response = SurveyResponse.deserialize(input);
|
|
2098
|
+
if (response.definitionId !== binding.definitionId) throw new Error(`Response was given against definition '${response.definitionId}', not '${binding.definitionId}'`);
|
|
2099
|
+
const issues = survey.slots.validateAnswers(response.answers);
|
|
2100
|
+
if (issues.length) throw new Error(issues.map((issue) => issue.message).join("\n"));
|
|
2101
|
+
const plan = new SurveyStateGraph(survey);
|
|
2102
|
+
const applicable = plan.evaluate({
|
|
2103
|
+
answers: response.answers,
|
|
2104
|
+
context
|
|
2105
|
+
}).activeAnswers();
|
|
2106
|
+
const validation = validateSurveyAnswers(plan, applicable, context, { phase: "submit" });
|
|
2107
|
+
if (validation.issues.length) throw new Error(validation.issues.map((issue) => `${issue.slotId}: ${issue.code}`).join("\n"));
|
|
2108
|
+
response.answers = structuredClone(applicable);
|
|
2109
|
+
response.evaluationContext = {
|
|
2110
|
+
locale: context.locale,
|
|
2111
|
+
currentDate: context.currentDate,
|
|
2112
|
+
participantFlags: context.participantFlags && { ...context.participantFlags }
|
|
2525
2113
|
};
|
|
2114
|
+
response.customValues = context.customValues && structuredClone(context.customValues);
|
|
2115
|
+
return response;
|
|
2526
2116
|
}
|
|
2527
2117
|
//#endregion
|
|
2528
|
-
export { AndExpressionEditor, CURRENT_SURVEY_SCHEMA, ConstBooleanEditor, ConstDateArrayEditor, ConstDateEditor,
|
|
2118
|
+
export { AndExpressionEditor, BooleanExportValues, CALENDAR_DATE_UNITS, CURRENT_SURVEY_SCHEMA, CURRENT_SURVEY_SCHEMA_VERSION, ConstBooleanEditor, ConstDateArrayEditor, ConstDateEditor, ConstNumberArrayEditor, ConstNumberEditor, ConstStringArrayEditor, ConstStringEditor, ContentType, CtxCurrentDateEditor, CtxCustomValueEditor, CtxLocaleEditor, CtxPFlagDateEditor, CtxPFlagIsDefinedEditor, CtxPFlagNumEditor, CtxPFlagStringEditor, DEFAULT_TRANSFORM, DefinitionError, DisabledStateEditor, DurationUnits, EXPORTER_PROFILE_SCHEMA_VERSION, EqExpressionEditor, ExportColumnSource, ExpressionEditor, ExpressionEvaluationError, FUNCTION_SIGNATURES, GtExpressionEditor, GteExpressionEditor, InRangeExpressionEditor, JsonPatchError, LtExpressionEditor, LteExpressionEditor, META_COLUMN_ORDER, MaxExpressionEditor, MinExpressionEditor, NumberPrecision, OrExpressionEditor, RESPONSE_VARIABLE_NAME_MAX_LENGTH, ResponseSlotRegistry, ResponseVariableEditor, SURVEY_LOCALIZATION_SURFACES, SURVEY_RESPONSE_SCHEMA_VERSION, SlotTransformMode, StrEqExpressionEditor, StrListContainsExpressionEditor, SumExpressionEditor, Survey, SurveyEventTypes, SurveyResponse, SurveyResponseExporter, SurveySession, SurveyStateGraph, ValueType, addCalendarDate, allocateResponseVariableName, and, annotated, annotatedReferences, applyJsonPatch, assertResponseValue, boundField, calendarDateBounds, calendarDateFromLocalDate, calendarDatePrecision, calendarDateToLocalDate, calendarDaySchema, canWriteSelection, checkDocumentBudget, collectExpressionDependencies, compareCalendarDates, conditionsShape, const_boolean, const_date, const_date_array, const_number, const_number_array, const_string, const_string_array, contentOwner, contentOwnerIds, createCompositionSchema, createElementRegistry, createFlowItemSchema, createFunctionExpression, createRichTextContent, createSeededRandom, createSurveySchema, ctx_current_date, ctx_custom_value, ctx_locale, ctx_pflag_date, ctx_pflag_is_defined, ctx_pflag_num, ctx_pflag_string, date_add_days, date_add_months, date_add_years, date_diff, date_eq, date_gt, date_gte, date_lt, date_lte, date_max, date_min, decodeExportColumnId, decodeJsonPointerSegment, defineElement, differenceCalendarDates, elementShape, encodeExportColumnId, encodeJsonPointerSegment, eq, escapeCsvCell, evaluateExpression, exportSingleResponseSlot, expressionArguments, expressionEditorConfigSchema, expressionField, expressionIssues, expressionSchema, expressionTypeIssues, extendObject, followUpBody, formatCalendarDate, formatProjectedValue, functionNameSchema, functionTypeIssues, generateCodebook, generateCodingKey, generateExportCodebook, generateId, getAssetUsagesFromContent, getAuthoringWarnings, getBrokenReferences, getCompositionLabel, getContentPlainText, getDefaultProjectionDiagnostics, getDefaultProjectionIssues, getDefinitionDiagnostics, getElementSemanticContext, getLocalizationCellKey, getLocalizationCoverage, getLocalizationEntryKey, getLocalizationItems, getPlainTextFromRichTextContent, getSessionDiagnostics, getValueAtJsonPointer, gt, gte, hasLocalizationText, hasPermittedChange, hasRenderableRichTextBlock, hasRenderableRichTextContent, if_else, in_range, inferExpressionReturnType, inspectAnnotated, invalidResponseDomainValues, isAdmissibleInDraft, isCalendarDate, isCalendarDateUnit, isContentEmpty, isEmptySurveyLocalizationEntry, isResponseValue, isValidResponseVariableName, is_disabled, itemEditorMetadataSchema, itemResponseMetadataSchema, jsonValueSchema, layoutPolicySchema, listLocalizationEntries, list_count, localizedExportSlots, lt, lte, max, min, moveArrayElement, operand, optionIds, optionMapping, or, ownValue, ownedId, ownerAtPath, parseJsonPointer, projectSlot, readLocalizationContent, referenceId, remapAnnotated, remapExpression, resolveElementName, resolveParticipantContent, resolveUniqueItemKey, resolvedSlotEncoding, responseValueElements, responseValueSchema, responseValuesEqual, response_boolean, response_date, response_date_array, response_number, response_number_array, response_string, response_string_array, richTextImageDimensionSchema, richTextImageSourceSchema, schemaAnnotation, selectedAndEnabled, serializeToCsv, sessionSnapshotSchema, setOwnValue, shuffleArray, shuffleIndices, slotPrefillSchema, slotTransformSchema, str_eq, str_list_contains, strictKeyedObject, stringRecord, structuredCloneMethod, suggestResponseVariableName, sum, surveyContentSchema, surveyEventSchema, surveyLocales, surveyResponseSchema, swapArrayElements, templateValueSchema, transformAnnotated, transformExpression, unreachable, validateLocalizationRef, validateResponseSlotValue, validateSurveyAnswers, validateSurveySubmission, validationRuleSchema, valueTypeSchema, visitExpression };
|
|
2529
2119
|
|
|
2530
2120
|
//# sourceMappingURL=index.mjs.map
|