@aehrc/smart-forms-renderer 0.21.2 → 0.22.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/lib/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.js +0 -1
- package/lib/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.js.map +1 -1
- package/lib/components/FormComponents/DecimalItem/DecimalField.js +1 -1
- package/lib/components/FormComponents/DecimalItem/DecimalField.js.map +1 -1
- package/lib/components/FormComponents/DecimalItem/DecimalItem.js +2 -2
- package/lib/components/FormComponents/DecimalItem/DecimalItem.js.map +1 -1
- package/lib/hooks/useRenderingExtensions.d.ts +4 -3
- package/lib/hooks/useRenderingExtensions.js +4 -3
- package/lib/hooks/useRenderingExtensions.js.map +1 -1
- package/lib/hooks/useValidationFeedback.d.ts +1 -1
- package/lib/hooks/useValidationFeedback.js +10 -6
- package/lib/hooks/useValidationFeedback.js.map +1 -1
- package/lib/stores/questionnaireResponseStore.d.ts +3 -4
- package/lib/stores/questionnaireResponseStore.js +3 -5
- package/lib/stores/questionnaireResponseStore.js.map +1 -1
- package/lib/utils/debounce.d.ts +1 -1
- package/lib/utils/debounce.js +1 -1
- package/lib/utils/itemControl.d.ts +3 -3
- package/lib/utils/itemControl.js +14 -8
- package/lib/utils/itemControl.js.map +1 -1
- package/lib/utils/validateQuestionnaire.d.ts +44 -5
- package/lib/utils/validateQuestionnaire.js +278 -42
- package/lib/utils/validateQuestionnaire.js.map +1 -1
- package/package.json +1 -1
- package/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.tsx +0 -1
- package/src/components/FormComponents/DecimalItem/DecimalField.tsx +1 -0
- package/src/components/FormComponents/DecimalItem/DecimalItem.tsx +16 -3
- package/src/hooks/useRenderingExtensions.ts +7 -5
- package/src/hooks/useValidationFeedback.ts +20 -8
- package/src/stores/questionnaireResponseStore.ts +5 -9
- package/src/utils/debounce.ts +1 -1
- package/src/utils/itemControl.ts +18 -10
- package/src/utils/validateQuestionnaire.ts +352 -51
package/src/utils/debounce.ts
CHANGED
package/src/utils/itemControl.ts
CHANGED
|
@@ -312,17 +312,30 @@ export function getTextDisplayFlyover(qItem: QuestionnaireItem): string {
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
/**
|
|
315
|
-
* Get
|
|
316
|
-
* i.e. DD-MM-YYYY for dates, HH:MM for times etc.
|
|
315
|
+
* Get regex validation for items with regex extensions
|
|
317
316
|
*
|
|
318
317
|
* @author Sean Fong
|
|
319
318
|
*/
|
|
320
|
-
export function getRegexValidation(qItem: QuestionnaireItem): RegexValidation |
|
|
319
|
+
export function getRegexValidation(qItem: QuestionnaireItem): RegexValidation | undefined {
|
|
320
|
+
// Get regex expression from extension
|
|
321
|
+
const regexString = getRegexString(qItem);
|
|
322
|
+
if (regexString) {
|
|
323
|
+
return { expression: new RegExp(regexString), feedback: null };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Get regex expression from item types if regex extensions not present
|
|
327
|
+
if (qItem.type === 'url') {
|
|
328
|
+
return { expression: new RegExp(/^\S*$/), feedback: 'URLs should not contain any whitespaces' };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function getRegexString(qItem: QuestionnaireItem): string | null {
|
|
321
335
|
const itemControl = qItem.extension?.find(
|
|
322
336
|
(extension: Extension) => extension.url === 'http://hl7.org/fhir/StructureDefinition/regex'
|
|
323
337
|
);
|
|
324
338
|
|
|
325
|
-
// Get regex expression from extension
|
|
326
339
|
if (itemControl) {
|
|
327
340
|
const extensionString = itemControl.valueString;
|
|
328
341
|
if (extensionString) {
|
|
@@ -336,14 +349,9 @@ export function getRegexValidation(qItem: QuestionnaireItem): RegexValidation |
|
|
|
336
349
|
regexString = extensionString;
|
|
337
350
|
}
|
|
338
351
|
|
|
339
|
-
return
|
|
352
|
+
return regexString;
|
|
340
353
|
}
|
|
341
354
|
}
|
|
342
355
|
|
|
343
|
-
// Get regex expression from item types if regex extensions not present
|
|
344
|
-
if (qItem.type === 'url') {
|
|
345
|
-
return { expression: new RegExp(/^\S*$/), feedback: 'URLs should not contain any whitespaces' };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
356
|
return null;
|
|
349
357
|
}
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type {
|
|
19
|
+
OperationOutcome,
|
|
20
|
+
OperationOutcomeIssue,
|
|
19
21
|
Questionnaire,
|
|
20
22
|
QuestionnaireItem,
|
|
21
23
|
QuestionnaireResponse,
|
|
@@ -25,16 +27,55 @@ import type {
|
|
|
25
27
|
import { getQrItemsIndex, mapQItemsIndex } from './mapItem';
|
|
26
28
|
import type { EnableWhenExpressions, EnableWhenItems } from '../interfaces';
|
|
27
29
|
import { isHiddenByEnableWhen } from './qItem';
|
|
28
|
-
import { getRegexValidation } from './itemControl';
|
|
30
|
+
import { getRegexString, getRegexValidation, getShortText } from './itemControl';
|
|
29
31
|
import { structuredDataCapture } from 'fhir-sdc-helpers';
|
|
30
32
|
import type { RegexValidation } from '../interfaces/regex.interface';
|
|
31
33
|
|
|
32
|
-
export
|
|
34
|
+
export enum ValidationResult {
|
|
35
|
+
unknown = 'unknown', // Unknown validation result
|
|
36
|
+
questionnaireNotFound = 'questionnaireNotFound', // The Questionnaire referenced by the QR was not found
|
|
37
|
+
questionnaireInactive = 'questionnaireInactive', // The QuestionnaireResponse.authored is outside the defined Questionnaire.effectivePeriod
|
|
38
|
+
questionnaireDraft = 'questionnaireDraft', // The Questionnaire.status was not active (draft)
|
|
39
|
+
questionnaireRetired = 'questionnaireRetired', // The Questionnaire.status was not active (retired)
|
|
40
|
+
invalidLinkId = 'invalidLinkId', // LinkId was not found in the questionnaire
|
|
41
|
+
invalidType = 'invalidType', // The Question Type should not be included in Response (or Definition) data
|
|
42
|
+
invalidAnswerType = 'invalidAnswerType', // The Answer does not conform to the Item.Type value required (also taking into consideration the fhir-type extension)
|
|
43
|
+
invalidAnswerOption = 'invalidAnswerOption', // A set of valid AnswerOptions were provided in the definition, but the value entered was not in the set
|
|
44
|
+
exclusiveAnswerOption = 'exclusiveAnswerOption', // The selected answer cannot be used in conjunction with other answers in this multi-select choice option
|
|
45
|
+
invalidUrlValue = 'invalidUrlValue', // URL value not formatted correctly as a UUID, or relative/absolute URL
|
|
46
|
+
groupShouldNotHaveAnswers = 'groupShouldNotHaveAnswers', // A Group Item should not use the `answer` child, it should use the `item` child
|
|
47
|
+
required = 'required', // There was no answer provided for a mandatory field
|
|
48
|
+
invariant = 'invariant', // A FHIRPATH validation expression did not pass
|
|
49
|
+
invariantExecution = 'invariantExecution', // A FHIRPATH validation expression failed to execute
|
|
50
|
+
repeats = 'repeats', // There was more than one answer provided for an item with repeats = false (which is the default)
|
|
51
|
+
minCount = 'minCount', // Minimum number of answers required for the item was not provided
|
|
52
|
+
maxCount = 'maxCount', // Number of answers provided exceeded the maximum permitted
|
|
53
|
+
minValue = 'minValue', // Minimum value constraint violated
|
|
54
|
+
maxValue = 'maxValue', // Maximum value constraint violated
|
|
55
|
+
maxDecimalPlaces = 'maxDecimalPlaces', // Maximum decimal places constraint violated
|
|
56
|
+
minLength = 'minLength', // Minimum length constraint violated
|
|
57
|
+
maxLength = 'maxLength', // Maximum length constraint violated
|
|
58
|
+
invalidNewLine = 'invalidNewLine', // 'string' type items cannot include newline characters, use a 'text' type for these
|
|
59
|
+
invalidCoding = 'invalidCoding', // Coding value not valid in the ValueSet
|
|
60
|
+
tsError = 'tsError', // Error accessing the Terminology Server
|
|
61
|
+
maxAttachmentSize = 'maxAttachmentSize', // Maximum attachment size constraint violated
|
|
62
|
+
attachmentSizeInconsistent = 'attachmentSizeInconsistent', // The Size of the attachment data and the reported size are different
|
|
63
|
+
invalidAttachmentType = 'invalidAttachmentType', // Attachment type constraint violated
|
|
64
|
+
displayAnswer = 'displayAnswer', // Display Items should not have an answer provided
|
|
65
|
+
regex = 'regex', // The answer does not match the provided regex expression
|
|
66
|
+
regexTimeout = 'regexTimeout', // Evaluating the regex expression timed out
|
|
67
|
+
invalidRefValue = 'invalidRefValue', // The Reference value was not a valid URL value (relative or absolute)
|
|
68
|
+
invalidRefResourceType = 'invalidRefResourceType', // The Reference value did not refer to a valid FHIR resource type
|
|
69
|
+
invalidRefResourceTypeRestriction = 'invalidRefResourceTypeRestriction', // The Reference value was not
|
|
70
|
+
minValueIncompatUnits = 'minValueIncompatUnits', // The units provided in the Quantity cannot be converted to the min Quantity units
|
|
71
|
+
maxValueIncompatUnits = 'maxValueIncompatUnits', // The units provided in the Quantity cannot be converted to the max Quantity units
|
|
72
|
+
invalidUnit = 'invalidUnit', // The unit provided was not among the list selected (or did not have all the properties defined in the unit coding)
|
|
73
|
+
invalidUnitValueSet = 'invalidUnitValueSet' // The unit provided was not in the provided valueset
|
|
74
|
+
}
|
|
33
75
|
|
|
34
76
|
interface ValidateQuestionnaireParams {
|
|
35
77
|
questionnaire: Questionnaire;
|
|
36
78
|
questionnaireResponse: QuestionnaireResponse;
|
|
37
|
-
invalidItems: Record<string, InvalidType>;
|
|
38
79
|
enableWhenIsActivated: boolean;
|
|
39
80
|
enableWhenItems: EnableWhenItems;
|
|
40
81
|
enableWhenExpressions: EnableWhenExpressions;
|
|
@@ -48,11 +89,10 @@ interface ValidateQuestionnaireParams {
|
|
|
48
89
|
*/
|
|
49
90
|
export function validateQuestionnaire(
|
|
50
91
|
params: ValidateQuestionnaireParams
|
|
51
|
-
): Record<string,
|
|
92
|
+
): Record<string, OperationOutcome> {
|
|
52
93
|
const {
|
|
53
94
|
questionnaire,
|
|
54
95
|
questionnaireResponse,
|
|
55
|
-
invalidItems,
|
|
56
96
|
enableWhenIsActivated,
|
|
57
97
|
enableWhenItems,
|
|
58
98
|
enableWhenExpressions
|
|
@@ -64,7 +104,7 @@ export function validateQuestionnaire(
|
|
|
64
104
|
!questionnaireResponse.item ||
|
|
65
105
|
questionnaireResponse.item.length === 0
|
|
66
106
|
) {
|
|
67
|
-
return
|
|
107
|
+
return {};
|
|
68
108
|
}
|
|
69
109
|
|
|
70
110
|
const qItemsIndexMap = mapQItemsIndex(questionnaire);
|
|
@@ -74,13 +114,19 @@ export function validateQuestionnaire(
|
|
|
74
114
|
qItemsIndexMap
|
|
75
115
|
);
|
|
76
116
|
|
|
117
|
+
const invalidItems: Record<string, OperationOutcome> = {};
|
|
118
|
+
let qrItemIndex = 0;
|
|
77
119
|
for (const [index, topLevelQItem] of questionnaire.item.entries()) {
|
|
120
|
+
let repeatGroupInstances: number | null = null;
|
|
78
121
|
let topLevelQRItem = topLevelQRItemsByIndex[index] ?? {
|
|
79
122
|
linkId: topLevelQItem.linkId,
|
|
80
123
|
text: topLevelQItem.text
|
|
81
124
|
};
|
|
82
125
|
|
|
126
|
+
// topLevelQRItem being an array means this item is a repeat group
|
|
127
|
+
const isRepeatGroup = Array.isArray(topLevelQRItem);
|
|
83
128
|
if (Array.isArray(topLevelQRItem)) {
|
|
129
|
+
repeatGroupInstances = topLevelQRItem.length;
|
|
84
130
|
topLevelQRItem = {
|
|
85
131
|
linkId: topLevelQItem.linkId,
|
|
86
132
|
text: topLevelQItem.text,
|
|
@@ -88,14 +134,26 @@ export function validateQuestionnaire(
|
|
|
88
134
|
};
|
|
89
135
|
}
|
|
90
136
|
|
|
137
|
+
const locationExpression = `QuestionnaireResponse.item`;
|
|
91
138
|
validateItemRecursive({
|
|
92
139
|
qItem: topLevelQItem,
|
|
93
140
|
qrItem: topLevelQRItem,
|
|
94
|
-
|
|
141
|
+
qrItemIndex,
|
|
142
|
+
locationExpression,
|
|
143
|
+
invalidItems,
|
|
95
144
|
enableWhenIsActivated,
|
|
96
145
|
enableWhenItems,
|
|
97
|
-
enableWhenExpressions
|
|
146
|
+
enableWhenExpressions,
|
|
147
|
+
isRepeatGroupInstance: false
|
|
98
148
|
});
|
|
149
|
+
|
|
150
|
+
// Increment qrItemIndex
|
|
151
|
+
// If its a repeat group, increment by the number of instances so qrItemIndex is correct once we reach the next item
|
|
152
|
+
if (isRepeatGroup && typeof repeatGroupInstances === 'number') {
|
|
153
|
+
qrItemIndex += repeatGroupInstances;
|
|
154
|
+
} else {
|
|
155
|
+
qrItemIndex++;
|
|
156
|
+
}
|
|
99
157
|
}
|
|
100
158
|
|
|
101
159
|
return invalidItems;
|
|
@@ -104,22 +162,29 @@ export function validateQuestionnaire(
|
|
|
104
162
|
interface ValidateItemRecursiveParams {
|
|
105
163
|
qItem: QuestionnaireItem;
|
|
106
164
|
qrItem: QuestionnaireResponseItem;
|
|
107
|
-
|
|
165
|
+
qrItemIndex: number;
|
|
166
|
+
locationExpression: string;
|
|
167
|
+
invalidItems: Record<string, OperationOutcome>;
|
|
108
168
|
enableWhenIsActivated: boolean;
|
|
109
169
|
enableWhenItems: EnableWhenItems;
|
|
110
170
|
enableWhenExpressions: EnableWhenExpressions;
|
|
171
|
+
isRepeatGroupInstance: boolean;
|
|
111
172
|
}
|
|
112
173
|
|
|
113
174
|
function validateItemRecursive(params: ValidateItemRecursiveParams) {
|
|
114
175
|
const {
|
|
115
176
|
qItem,
|
|
116
177
|
qrItem,
|
|
178
|
+
qrItemIndex,
|
|
117
179
|
invalidItems,
|
|
118
180
|
enableWhenIsActivated,
|
|
119
181
|
enableWhenItems,
|
|
120
|
-
enableWhenExpressions
|
|
182
|
+
enableWhenExpressions,
|
|
183
|
+
isRepeatGroupInstance
|
|
121
184
|
} = params;
|
|
185
|
+
let { locationExpression } = params;
|
|
122
186
|
|
|
187
|
+
// If item is hidden by enableWhen, skip validation
|
|
123
188
|
if (
|
|
124
189
|
isHiddenByEnableWhen({
|
|
125
190
|
linkId: qItem.linkId,
|
|
@@ -131,11 +196,27 @@ function validateItemRecursive(params: ValidateItemRecursiveParams) {
|
|
|
131
196
|
return;
|
|
132
197
|
}
|
|
133
198
|
|
|
134
|
-
//
|
|
199
|
+
// Validate repeat groups
|
|
135
200
|
if (qItem.type === 'group' && qItem.repeats) {
|
|
136
|
-
|
|
201
|
+
if (!isRepeatGroupInstance) {
|
|
202
|
+
validateRepeatGroupRecursive({
|
|
203
|
+
qItem,
|
|
204
|
+
qrItem,
|
|
205
|
+
qrItemIndex,
|
|
206
|
+
locationExpression,
|
|
207
|
+
invalidItems,
|
|
208
|
+
enableWhenIsActivated,
|
|
209
|
+
enableWhenItems,
|
|
210
|
+
enableWhenExpressions,
|
|
211
|
+
isRepeatGroupInstance: false
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
137
215
|
}
|
|
138
216
|
|
|
217
|
+
locationExpression = `${locationExpression}[${qrItemIndex}]`;
|
|
218
|
+
|
|
219
|
+
// Recursively validate groups with child items
|
|
139
220
|
const childQItems = qItem.item;
|
|
140
221
|
if (childQItems && childQItems.length > 0) {
|
|
141
222
|
const childQrItems = qrItem?.item ?? [];
|
|
@@ -143,13 +224,23 @@ function validateItemRecursive(params: ValidateItemRecursiveParams) {
|
|
|
143
224
|
const indexMap = mapQItemsIndex(qItem);
|
|
144
225
|
const qrItemsByIndex = getQrItemsIndex(childQItems, childQrItems, indexMap);
|
|
145
226
|
|
|
227
|
+
// Check if group is required and has no answers
|
|
146
228
|
if (qItem.type === 'group' && qItem.required) {
|
|
147
229
|
if (!qrItem || qrItemsByIndex.length === 0) {
|
|
148
|
-
invalidItems[qItem.linkId] =
|
|
230
|
+
invalidItems[qItem.linkId] = createValidationOperationOutcome(
|
|
231
|
+
ValidationResult.required,
|
|
232
|
+
qItem,
|
|
233
|
+
qrItem,
|
|
234
|
+
null,
|
|
235
|
+
locationExpression,
|
|
236
|
+
invalidItems[qItem.linkId]?.issue
|
|
237
|
+
);
|
|
149
238
|
}
|
|
150
239
|
}
|
|
151
240
|
|
|
241
|
+
// Loop through child items
|
|
152
242
|
for (const [index, childQItem] of childQItems.entries()) {
|
|
243
|
+
const childLocationExpression = `${locationExpression}.item`;
|
|
153
244
|
let childQRItem = qrItemsByIndex[index] ?? {
|
|
154
245
|
linkId: childQItem.linkId,
|
|
155
246
|
text: childQItem.text
|
|
@@ -166,72 +257,114 @@ function validateItemRecursive(params: ValidateItemRecursiveParams) {
|
|
|
166
257
|
validateItemRecursive({
|
|
167
258
|
qItem: childQItem,
|
|
168
259
|
qrItem: childQRItem,
|
|
260
|
+
qrItemIndex: index,
|
|
261
|
+
locationExpression: childLocationExpression,
|
|
169
262
|
invalidItems: invalidItems,
|
|
170
263
|
enableWhenIsActivated,
|
|
171
264
|
enableWhenItems,
|
|
172
|
-
enableWhenExpressions
|
|
265
|
+
enableWhenExpressions,
|
|
266
|
+
isRepeatGroupInstance: false
|
|
173
267
|
});
|
|
174
268
|
}
|
|
175
269
|
}
|
|
176
270
|
|
|
177
|
-
|
|
271
|
+
// Validate the item, note that this can be either a group or a non-group
|
|
272
|
+
validateSingleItem(qItem, qrItem, invalidItems, locationExpression);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function validateRepeatGroupRecursive(params: ValidateItemRecursiveParams) {
|
|
276
|
+
const {
|
|
277
|
+
qItem,
|
|
278
|
+
qrItem,
|
|
279
|
+
qrItemIndex,
|
|
280
|
+
locationExpression,
|
|
281
|
+
invalidItems,
|
|
282
|
+
enableWhenIsActivated,
|
|
283
|
+
enableWhenItems,
|
|
284
|
+
enableWhenExpressions
|
|
285
|
+
} = params;
|
|
286
|
+
|
|
287
|
+
if (!qItem.item || qItem.item.length === 0 || !qrItem.item || qrItem.item.length === 0) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Get repeat group answers
|
|
292
|
+
const repeatGroupAnswers = qrItem.item ?? [];
|
|
293
|
+
for (const [index, repeatGroupAnswer] of repeatGroupAnswers.entries()) {
|
|
294
|
+
// Because the item is a repeat group and might have multiple answer instances, we need to increment the qItemIndex by the instanceIndex
|
|
295
|
+
const updatedQrItemIndex = qrItemIndex + index;
|
|
296
|
+
|
|
297
|
+
validateItemRecursive({
|
|
298
|
+
qItem: qItem,
|
|
299
|
+
qrItem: repeatGroupAnswer,
|
|
300
|
+
qrItemIndex: updatedQrItemIndex,
|
|
301
|
+
locationExpression: locationExpression,
|
|
302
|
+
invalidItems: invalidItems,
|
|
303
|
+
enableWhenIsActivated,
|
|
304
|
+
enableWhenItems,
|
|
305
|
+
enableWhenExpressions,
|
|
306
|
+
isRepeatGroupInstance: true
|
|
307
|
+
});
|
|
308
|
+
}
|
|
178
309
|
}
|
|
179
310
|
|
|
180
311
|
function validateSingleItem(
|
|
181
312
|
qItem: QuestionnaireItem,
|
|
182
313
|
qrItem: QuestionnaireResponseItem,
|
|
183
|
-
invalidItems: Record<string,
|
|
314
|
+
invalidItems: Record<string, OperationOutcome>,
|
|
315
|
+
locationExpression: string
|
|
184
316
|
) {
|
|
185
|
-
// Validate item.required
|
|
317
|
+
// Validate item.required first before every other validation check
|
|
186
318
|
if (qItem.type !== 'display') {
|
|
187
319
|
if (qItem.required && !qrItem.answer) {
|
|
188
|
-
invalidItems[qItem.linkId] =
|
|
320
|
+
invalidItems[qItem.linkId] = createValidationOperationOutcome(
|
|
321
|
+
ValidationResult.required,
|
|
322
|
+
qItem,
|
|
323
|
+
qrItem,
|
|
324
|
+
null,
|
|
325
|
+
locationExpression,
|
|
326
|
+
invalidItems[qItem.linkId]?.issue
|
|
327
|
+
);
|
|
328
|
+
|
|
189
329
|
return invalidItems;
|
|
190
330
|
}
|
|
191
331
|
}
|
|
192
332
|
|
|
193
333
|
// Validate regex, maxLength and minLength
|
|
194
334
|
if (qrItem.answer) {
|
|
195
|
-
for (const answer of qrItem.answer) {
|
|
335
|
+
for (const [i, answer] of qrItem.answer.entries()) {
|
|
336
|
+
// Your code here, you can use 'index' and 'answer' as needed
|
|
196
337
|
if (answer.valueString || answer.valueInteger || answer.valueDecimal || answer.valueUri) {
|
|
197
338
|
const invalidInputType = getInputInvalidType(
|
|
198
339
|
getInputInString(answer),
|
|
199
340
|
getRegexValidation(qItem),
|
|
200
|
-
structuredDataCapture.getMinLength(qItem)
|
|
201
|
-
qItem.maxLength
|
|
341
|
+
structuredDataCapture.getMinLength(qItem),
|
|
342
|
+
qItem.maxLength,
|
|
343
|
+
structuredDataCapture.getMaxDecimalPlaces(qItem)
|
|
202
344
|
);
|
|
203
345
|
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
invalidItems[qItem.linkId]
|
|
212
|
-
|
|
213
|
-
case 'minLength':
|
|
214
|
-
invalidItems[qItem.linkId] = 'minLength';
|
|
215
|
-
break;
|
|
216
|
-
case 'maxLength':
|
|
217
|
-
invalidItems[qItem.linkId] = 'maxLength';
|
|
218
|
-
break;
|
|
346
|
+
if (invalidInputType) {
|
|
347
|
+
invalidItems[qItem.linkId] = createValidationOperationOutcome(
|
|
348
|
+
invalidInputType,
|
|
349
|
+
qItem,
|
|
350
|
+
qrItem,
|
|
351
|
+
i,
|
|
352
|
+
locationExpression,
|
|
353
|
+
invalidItems[qItem.linkId]?.issue
|
|
354
|
+
);
|
|
219
355
|
}
|
|
220
|
-
break;
|
|
221
356
|
}
|
|
222
357
|
}
|
|
223
|
-
|
|
224
|
-
// Reached the end of the loop and no invalid input type found
|
|
225
|
-
// If a required item is filled, remove the required invalid type
|
|
226
|
-
if (qItem.required && invalidItems[qItem.linkId] && invalidItems[qItem.linkId] === 'required') {
|
|
227
|
-
delete invalidItems[qItem.linkId];
|
|
228
|
-
}
|
|
229
358
|
}
|
|
230
359
|
|
|
231
360
|
return invalidItems;
|
|
232
361
|
}
|
|
233
362
|
|
|
234
|
-
function getInputInString(answer
|
|
363
|
+
function getInputInString(answer?: QuestionnaireResponseItemAnswer) {
|
|
364
|
+
if (!answer) {
|
|
365
|
+
return '';
|
|
366
|
+
}
|
|
367
|
+
|
|
235
368
|
if (answer.valueString) {
|
|
236
369
|
return answer.valueString;
|
|
237
370
|
} else if (answer.valueInteger) {
|
|
@@ -247,23 +380,191 @@ function getInputInString(answer: QuestionnaireResponseItemAnswer) {
|
|
|
247
380
|
|
|
248
381
|
export function getInputInvalidType(
|
|
249
382
|
input: string,
|
|
250
|
-
regexValidation
|
|
251
|
-
minLength
|
|
252
|
-
maxLength
|
|
253
|
-
|
|
383
|
+
regexValidation?: RegexValidation,
|
|
384
|
+
minLength?: number,
|
|
385
|
+
maxLength?: number,
|
|
386
|
+
maxDecimalPlaces?: number
|
|
387
|
+
): ValidationResult | null {
|
|
254
388
|
if (input) {
|
|
255
389
|
if (regexValidation && !regexValidation.expression.test(input)) {
|
|
256
|
-
return
|
|
390
|
+
return ValidationResult.regex;
|
|
257
391
|
}
|
|
258
392
|
|
|
259
393
|
if (minLength && input.length < minLength) {
|
|
260
|
-
return
|
|
394
|
+
return ValidationResult.minLength;
|
|
261
395
|
}
|
|
262
396
|
|
|
263
397
|
if (maxLength && input.length > maxLength) {
|
|
264
|
-
return
|
|
398
|
+
return ValidationResult.maxLength;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (maxDecimalPlaces) {
|
|
402
|
+
const decimalPlaces = input.split('.')[1]?.length ?? 0;
|
|
403
|
+
if (decimalPlaces > maxDecimalPlaces) {
|
|
404
|
+
return ValidationResult.maxDecimalPlaces;
|
|
405
|
+
}
|
|
265
406
|
}
|
|
266
407
|
}
|
|
267
408
|
|
|
268
409
|
return null;
|
|
269
410
|
}
|
|
411
|
+
|
|
412
|
+
function createValidationOperationOutcome(
|
|
413
|
+
error: ValidationResult,
|
|
414
|
+
qItem: QuestionnaireItem,
|
|
415
|
+
qrItem: QuestionnaireResponseItem,
|
|
416
|
+
answerIndex: number | null,
|
|
417
|
+
locationExpression: string,
|
|
418
|
+
existingOperationOutcomeIssues: OperationOutcomeIssue[] = []
|
|
419
|
+
): OperationOutcome {
|
|
420
|
+
return {
|
|
421
|
+
resourceType: 'OperationOutcome',
|
|
422
|
+
issue: existingOperationOutcomeIssues.concat(
|
|
423
|
+
createValidationOperationOutcomeIssue(error, qItem, qrItem, answerIndex, locationExpression)
|
|
424
|
+
)
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function createValidationOperationOutcomeIssue(
|
|
429
|
+
error: ValidationResult,
|
|
430
|
+
qItem: QuestionnaireItem,
|
|
431
|
+
qrItem: QuestionnaireResponseItem,
|
|
432
|
+
answerIndex: number | null,
|
|
433
|
+
locationExpression: string
|
|
434
|
+
): OperationOutcomeIssue {
|
|
435
|
+
const errorCodeSystem = 'http://fhir.forms-lab.com/CodeSystem/errors';
|
|
436
|
+
let detailsText = '';
|
|
437
|
+
let fieldDisplayText =
|
|
438
|
+
qrItem?.text ?? getShortText(qItem) ?? qItem?.text ?? qItem.linkId ?? qrItem.linkId;
|
|
439
|
+
if (!fieldDisplayText && fieldDisplayText.endsWith(':')) {
|
|
440
|
+
fieldDisplayText = fieldDisplayText.substring(0, fieldDisplayText.length - 1);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
answerIndex = answerIndex ?? 0;
|
|
444
|
+
|
|
445
|
+
// create operationOutcomeIssue based on error
|
|
446
|
+
switch (error) {
|
|
447
|
+
case ValidationResult.required: {
|
|
448
|
+
if (qItem.type === 'group') {
|
|
449
|
+
detailsText = `${fieldDisplayText}: Mandatory group does not have answer(s)`;
|
|
450
|
+
} else {
|
|
451
|
+
detailsText = `${fieldDisplayText}: Mandatory field does not have an answer`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
severity: 'error',
|
|
456
|
+
code: 'required',
|
|
457
|
+
expression: [locationExpression],
|
|
458
|
+
details: {
|
|
459
|
+
coding: [
|
|
460
|
+
{
|
|
461
|
+
system: errorCodeSystem,
|
|
462
|
+
code: error,
|
|
463
|
+
display: 'Required'
|
|
464
|
+
}
|
|
465
|
+
],
|
|
466
|
+
text: detailsText
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
case ValidationResult.regex: {
|
|
472
|
+
detailsText = `${fieldDisplayText}: The value '${getInputInString(qrItem.answer?.[answerIndex])}' does not match the defined format.`;
|
|
473
|
+
if (structuredDataCapture.getEntryFormat(qItem)) {
|
|
474
|
+
detailsText += ` ${structuredDataCapture.getEntryFormat(qItem)}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
severity: 'error',
|
|
479
|
+
code: 'invalid',
|
|
480
|
+
expression: [locationExpression],
|
|
481
|
+
details: {
|
|
482
|
+
coding: [
|
|
483
|
+
{
|
|
484
|
+
system: errorCodeSystem,
|
|
485
|
+
code: error,
|
|
486
|
+
display: 'Invalid format'
|
|
487
|
+
}
|
|
488
|
+
],
|
|
489
|
+
text: detailsText
|
|
490
|
+
},
|
|
491
|
+
diagnostics: getRegexString(qItem) ?? undefined
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
case ValidationResult.minLength: {
|
|
496
|
+
detailsText = `${fieldDisplayText}: Expected the minimum value ${structuredDataCapture.getMinLength(qItem)} characters, received '${getInputInString(qrItem.answer?.[answerIndex])}'`;
|
|
497
|
+
return {
|
|
498
|
+
severity: 'error',
|
|
499
|
+
code: 'business-rule',
|
|
500
|
+
expression: [locationExpression],
|
|
501
|
+
details: {
|
|
502
|
+
coding: [
|
|
503
|
+
{
|
|
504
|
+
system: errorCodeSystem,
|
|
505
|
+
code: error,
|
|
506
|
+
display: 'Too short'
|
|
507
|
+
}
|
|
508
|
+
],
|
|
509
|
+
text: detailsText
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
case ValidationResult.maxLength: {
|
|
515
|
+
detailsText = `${fieldDisplayText}: Exceeded maximum of ${qItem.maxLength} characters, received '${getInputInString(qrItem.answer?.[answerIndex])}'`;
|
|
516
|
+
return {
|
|
517
|
+
severity: 'error',
|
|
518
|
+
code: 'business-rule',
|
|
519
|
+
expression: [locationExpression],
|
|
520
|
+
details: {
|
|
521
|
+
coding: [
|
|
522
|
+
{
|
|
523
|
+
system: errorCodeSystem,
|
|
524
|
+
code: error,
|
|
525
|
+
display: 'Too long'
|
|
526
|
+
}
|
|
527
|
+
],
|
|
528
|
+
text: detailsText
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
case ValidationResult.maxDecimalPlaces: {
|
|
534
|
+
detailsText = `${fieldDisplayText}: Exceeded maximum decimal places ${structuredDataCapture.getMaxDecimalPlaces(qItem)}, received '${getInputInString(qrItem.answer?.[answerIndex])}'`;
|
|
535
|
+
return {
|
|
536
|
+
severity: 'error',
|
|
537
|
+
code: 'business-rule',
|
|
538
|
+
expression: [locationExpression],
|
|
539
|
+
details: {
|
|
540
|
+
coding: [
|
|
541
|
+
{
|
|
542
|
+
system: errorCodeSystem,
|
|
543
|
+
code: error,
|
|
544
|
+
display: 'Too precise'
|
|
545
|
+
}
|
|
546
|
+
],
|
|
547
|
+
text: detailsText
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// mark unknown issues as fatal
|
|
553
|
+
default: {
|
|
554
|
+
return {
|
|
555
|
+
severity: 'error',
|
|
556
|
+
code: 'unknown',
|
|
557
|
+
expression: [locationExpression],
|
|
558
|
+
details: {
|
|
559
|
+
coding: [
|
|
560
|
+
{
|
|
561
|
+
system: errorCodeSystem,
|
|
562
|
+
code: 'unknown',
|
|
563
|
+
display: 'Unknown'
|
|
564
|
+
}
|
|
565
|
+
]
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|