sdc-qrf 1.0.0-beta.2 → 1.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/converter/extensions.d.ts +9 -2
- package/dist/converter/extensions.d.ts.map +1 -1
- package/dist/converter/extensions.js +66 -2
- package/dist/converter/extensions.js.map +1 -1
- package/dist/converter/fhirToFce/questionnaire/processItems.d.ts.map +1 -1
- package/dist/converter/fhirToFce/questionnaire/processItems.js +33 -23
- package/dist/converter/fhirToFce/questionnaire/processItems.js.map +1 -1
- package/dist/converter/index.d.ts.map +1 -1
- package/dist/converter/index.js +2 -1
- package/dist/converter/index.js.map +1 -1
- package/dist/converter/utils.d.ts +1 -2
- package/dist/converter/utils.d.ts.map +1 -1
- package/dist/converter/utils.js +0 -5
- package/dist/converter/utils.js.map +1 -1
- package/dist/fce.types.d.ts +13 -1
- package/dist/fce.types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +16 -6
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +42 -26
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/converter/__tests__/fce.test.ts +88 -54
- package/src/converter/__tests__/resources/questionnaire_fce/answerOptionsToggleExpression.json +84 -0
- package/src/converter/__tests__/resources/questionnaire_fce/mixed-fce-with-extensions.json +26 -0
- package/src/converter/__tests__/resources/questionnaire_fce/occurs.json +16 -0
- package/src/converter/__tests__/resources/questionnaire_fce/sub-questionnaire.json +22 -0
- package/src/converter/__tests__/resources/questionnaire_fce/unknown-extensions.json +58 -0
- package/src/converter/__tests__/resources/questionnaire_fce/with-attachment-question.json +17 -0
- package/src/converter/__tests__/resources/questionnaire_fhir/answerOptionsToggleExpression.json +96 -0
- package/src/converter/__tests__/resources/questionnaire_fhir/mixed-fce-with-extensions.json +27 -0
- package/src/converter/__tests__/resources/questionnaire_fhir/occurs.json +24 -0
- package/src/converter/__tests__/resources/questionnaire_fhir/sub-questionnaire.json +32 -0
- package/src/converter/__tests__/resources/questionnaire_fhir/unknown-extensions.json +62 -0
- package/src/converter/__tests__/resources/questionnaire_fhir/with-attachment-question.json +26 -0
- package/src/converter/extensions.ts +78 -4
- package/src/converter/fhirToFce/questionnaire/processItems.ts +43 -34
- package/src/converter/index.ts +3 -1
- package/src/converter/utils.ts +1 -5
- package/src/fce.types.ts +18 -0
- package/src/index.ts +5 -1
- package/src/types.ts +2 -1
- package/src/utils.ts +79 -43
package/src/fce.types.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Questionnaire,
|
|
10
10
|
QuestionnaireItem,
|
|
11
11
|
Reference,
|
|
12
|
+
QuestionnaireItemAnswerOption,
|
|
12
13
|
} from 'fhir/r4b';
|
|
13
14
|
|
|
14
15
|
export interface FCEQuestionnaire extends Questionnaire {
|
|
@@ -59,6 +60,9 @@ export interface FCEQuestionnaireItem extends QuestionnaireItem {
|
|
|
59
60
|
/** NOTE: from extension http://hl7.org/fhir/StructureDefinition/questionnaire-constraint */
|
|
60
61
|
/** An invariant that must be satisfied before responses to the questionnaire can be considered "complete". */
|
|
61
62
|
itemConstraint?: FCEQuestionnaireItemConstraint[];
|
|
63
|
+
/** NOTE: from extension http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerOptionsToggleExpression */
|
|
64
|
+
/** A complex expression that provides a list of the allowed options that should be enabled or disabled based on the evaluation of a provided expression. */
|
|
65
|
+
answerOptionsToggleExpression?: FCEQuestionnaireItemAnswerOptionsToggleExpression[];
|
|
62
66
|
/** NOTE: from extension http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression */
|
|
63
67
|
/** An expression that returns a boolean value for whether to enable the item. */
|
|
64
68
|
enableWhenExpression?: Expression;
|
|
@@ -103,6 +107,7 @@ export interface FCEQuestionnaireItem extends QuestionnaireItem {
|
|
|
103
107
|
helpText?: string;
|
|
104
108
|
stopLabel?: string;
|
|
105
109
|
rowsNumber?: number;
|
|
110
|
+
colsNumber?: number;
|
|
106
111
|
unitOption?: Coding[];
|
|
107
112
|
columnSize?: number;
|
|
108
113
|
itemMedia?: Attachment;
|
|
@@ -113,6 +118,8 @@ export interface FCEQuestionnaireItem extends QuestionnaireItem {
|
|
|
113
118
|
maxValue?: Extension;
|
|
114
119
|
minQuantity?: Extension;
|
|
115
120
|
maxQuantity?: Extension;
|
|
121
|
+
minOccurs?: number;
|
|
122
|
+
maxOccurs?: number;
|
|
116
123
|
showOrdinalValue?: boolean;
|
|
117
124
|
preferredTerminologyServer?: string;
|
|
118
125
|
openLabel?: string;
|
|
@@ -121,6 +128,7 @@ export interface FCEQuestionnaireItem extends QuestionnaireItem {
|
|
|
121
128
|
choiceOrientation?: 'horizontal' | 'vertical';
|
|
122
129
|
choiceColumns?: number;
|
|
123
130
|
ordinalValue?: number;
|
|
131
|
+
mimeType?: string[];
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
export interface FCEQuestionnaireItemText {
|
|
@@ -169,6 +177,16 @@ export interface FCEQuestionnaireItemConstraint {
|
|
|
169
177
|
severity: string;
|
|
170
178
|
}
|
|
171
179
|
|
|
180
|
+
export type FCEQuestionnaireItemAnswerOptionsToggleExpressionOption = Omit<
|
|
181
|
+
QuestionnaireItemAnswerOption,
|
|
182
|
+
'initialSelected' | '_initialSelected'
|
|
183
|
+
>;
|
|
184
|
+
|
|
185
|
+
export interface FCEQuestionnaireItemAnswerOptionsToggleExpression {
|
|
186
|
+
expression: Expression;
|
|
187
|
+
option: Array<FCEQuestionnaireItemAnswerOptionsToggleExpressionOption>;
|
|
188
|
+
}
|
|
189
|
+
|
|
172
190
|
export interface FCEQuestionnaireLaunchContext {
|
|
173
191
|
/** NOTE: from extension description */
|
|
174
192
|
description?: string;
|
package/src/index.ts
CHANGED
|
@@ -2,13 +2,17 @@ export * from './types';
|
|
|
2
2
|
export {
|
|
3
3
|
mapFormToResponse,
|
|
4
4
|
mapResponseToForm,
|
|
5
|
-
findAnswersForQuestionsRecursive,
|
|
6
5
|
removeDisabledAnswers,
|
|
7
6
|
getEnabledQuestions,
|
|
8
7
|
calcInitialContext,
|
|
9
8
|
parseFhirQueryExpression,
|
|
9
|
+
findAnswersForQuestion,
|
|
10
10
|
getChecker,
|
|
11
|
+
cleanFormAnswerItems,
|
|
11
12
|
toAnswerValue,
|
|
13
|
+
getAnswerValues,
|
|
14
|
+
getAnswerValueType,
|
|
15
|
+
isAnswerValueEmpty,
|
|
12
16
|
populateItemKey,
|
|
13
17
|
removeItemKey,
|
|
14
18
|
getItemKey,
|
package/src/types.ts
CHANGED
|
@@ -109,7 +109,8 @@ export interface FormAnswerItems {
|
|
|
109
109
|
items?: FormItems;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
// Form renderers might insert undefined for missing field values even into array
|
|
113
|
+
export type FormItems = Record<string, FormGroupItems | (FormAnswerItems | undefined)[] | undefined>;
|
|
113
114
|
|
|
114
115
|
export interface QuestionnaireResponseFormData {
|
|
115
116
|
formValues: FormItems;
|
package/src/utils.ts
CHANGED
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
Expression,
|
|
11
11
|
Questionnaire,
|
|
12
12
|
QuestionnaireItem,
|
|
13
|
+
QuestionnaireItemAnswerOption,
|
|
13
14
|
QuestionnaireItemEnableWhen,
|
|
15
|
+
QuestionnaireItemInitial,
|
|
14
16
|
QuestionnaireResponse,
|
|
15
17
|
QuestionnaireResponseItem,
|
|
16
18
|
QuestionnaireResponseItemAnswer,
|
|
@@ -193,7 +195,7 @@ function isGroup(question: QuestionnaireItem) {
|
|
|
193
195
|
|
|
194
196
|
function isFormGroupItems(
|
|
195
197
|
question: QuestionnaireItem,
|
|
196
|
-
answers: FormGroupItems | FormAnswerItems[],
|
|
198
|
+
answers: FormGroupItems | (FormAnswerItems | undefined)[],
|
|
197
199
|
): answers is FormGroupItems {
|
|
198
200
|
return isGroup(question) && _.isPlainObject(answers);
|
|
199
201
|
}
|
|
@@ -249,25 +251,29 @@ function mapFormToResponseRecursive(
|
|
|
249
251
|
}, acc);
|
|
250
252
|
}
|
|
251
253
|
|
|
254
|
+
const qrItemAnswers = cleanFormAnswerItems(answers).reduce((answersAcc, answer) => {
|
|
255
|
+
const items = hasSubAnswerItems(answer.items)
|
|
256
|
+
? mapFormToResponseRecursive(answer.items, question.item ?? [])
|
|
257
|
+
: [];
|
|
258
|
+
|
|
259
|
+
return [
|
|
260
|
+
...answersAcc,
|
|
261
|
+
{
|
|
262
|
+
...toFHIRAnswerValue(answer.value!, 'value'),
|
|
263
|
+
...(items.length ? { item: items } : {}),
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
}, [] as QuestionnaireResponseItemAnswer[]);
|
|
267
|
+
|
|
268
|
+
if (!qrItemAnswers.length) {
|
|
269
|
+
return acc;
|
|
270
|
+
}
|
|
271
|
+
|
|
252
272
|
return [
|
|
253
273
|
...acc,
|
|
254
274
|
{
|
|
255
275
|
linkId,
|
|
256
|
-
answer:
|
|
257
|
-
.filter((answer) => !isAnswerValueEmpty(answer.value))
|
|
258
|
-
.reduce((answersAcc, answer) => {
|
|
259
|
-
const items = hasSubAnswerItems(answer.items)
|
|
260
|
-
? mapFormToResponseRecursive(answer.items, question.item ?? [])
|
|
261
|
-
: [];
|
|
262
|
-
|
|
263
|
-
return [
|
|
264
|
-
...answersAcc,
|
|
265
|
-
{
|
|
266
|
-
...toFHIRAnswerValue(answer.value!, 'value'),
|
|
267
|
-
...(items.length ? { item: items } : {}),
|
|
268
|
-
},
|
|
269
|
-
];
|
|
270
|
-
}, [] as QuestionnaireResponseItemAnswer[]),
|
|
276
|
+
answer: qrItemAnswers,
|
|
271
277
|
},
|
|
272
278
|
];
|
|
273
279
|
}, [] as QuestionnaireResponseItem[]);
|
|
@@ -282,7 +288,7 @@ export function mapFormToResponse(
|
|
|
282
288
|
};
|
|
283
289
|
}
|
|
284
290
|
|
|
285
|
-
const ITEM_KEY = '_itemKey';
|
|
291
|
+
export const ITEM_KEY = '_itemKey';
|
|
286
292
|
export function getItemKey(items: FormGroupItems | FormAnswerItems) {
|
|
287
293
|
return (items as any)[ITEM_KEY];
|
|
288
294
|
}
|
|
@@ -380,7 +386,9 @@ export function mapResponseToForm(resource: QuestionnaireResponse, questionnaire
|
|
|
380
386
|
return mapResponseToFormRecursive(resource.item ?? [], questionnaire.item ?? []);
|
|
381
387
|
}
|
|
382
388
|
|
|
383
|
-
|
|
389
|
+
function findAnswersForQuestionsRecursive(linkId: string, values?: FormItems): any | null {
|
|
390
|
+
// TODO: specify types for returning value
|
|
391
|
+
// TODO: pass Questionnaire structure to make code robust
|
|
384
392
|
if (values && _.has(values, linkId)) {
|
|
385
393
|
return values[linkId];
|
|
386
394
|
}
|
|
@@ -404,7 +412,7 @@ export function findAnswersForQuestionsRecursive(linkId: string, values?: FormIt
|
|
|
404
412
|
return acc2;
|
|
405
413
|
}
|
|
406
414
|
|
|
407
|
-
return findAnswersForQuestionsRecursive(linkId, v2
|
|
415
|
+
return findAnswersForQuestionsRecursive(linkId, v2?.items);
|
|
408
416
|
},
|
|
409
417
|
null,
|
|
410
418
|
);
|
|
@@ -428,7 +436,13 @@ export function findAnswersForQuestionsRecursive(linkId: string, values?: FormIt
|
|
|
428
436
|
);
|
|
429
437
|
}
|
|
430
438
|
|
|
431
|
-
|
|
439
|
+
// TODO: deprecate usage of this function because it relies on internals
|
|
440
|
+
// TODO: pass Questionnaire structure to make code robust, currently it uses isNaN that might work falsy when linkId is number
|
|
441
|
+
export function findAnswersForQuestion(linkId: string, parentPath: string[], values: FormItems): FormAnswerItems[] {
|
|
442
|
+
if (linkId === ITEM_KEY) {
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
|
|
432
446
|
const p = _.cloneDeep(parentPath);
|
|
433
447
|
|
|
434
448
|
// Go up
|
|
@@ -437,11 +451,13 @@ function findAnswersForQuestion(linkId: string, parentPath: string[], values: Fo
|
|
|
437
451
|
|
|
438
452
|
// Find answers in parent groups (including repeatable)
|
|
439
453
|
// They might have either 'items' of the group or number of the repeatable group in path
|
|
454
|
+
// TODO: using isNaN might return invalid value for linkId like '0'
|
|
440
455
|
if (part === 'items' || !isNaN(part as any)) {
|
|
456
|
+
// TODO: specify type FormItems, and handle group's linkId
|
|
441
457
|
const parentGroup = _.get(values, [...p, part]);
|
|
442
458
|
|
|
443
459
|
if (typeof parentGroup === 'object' && linkId in parentGroup) {
|
|
444
|
-
return parentGroup[linkId];
|
|
460
|
+
return cleanFormAnswerItems(parentGroup[linkId]);
|
|
445
461
|
}
|
|
446
462
|
}
|
|
447
463
|
}
|
|
@@ -449,7 +465,7 @@ function findAnswersForQuestion(linkId: string, parentPath: string[], values: Fo
|
|
|
449
465
|
// Go down
|
|
450
466
|
const answers = findAnswersForQuestionsRecursive(linkId, values);
|
|
451
467
|
|
|
452
|
-
return answers ? answers : [];
|
|
468
|
+
return answers ? cleanFormAnswerItems(answers) : [];
|
|
453
469
|
}
|
|
454
470
|
|
|
455
471
|
export function compareValue(firstAnswerValue: AnswerValue, secondAnswerValue: AnswerValue) {
|
|
@@ -457,7 +473,9 @@ export function compareValue(firstAnswerValue: AnswerValue, secondAnswerValue: A
|
|
|
457
473
|
const secondValueType = getAnswerValueType(secondAnswerValue);
|
|
458
474
|
|
|
459
475
|
if (firstValueType !== secondValueType) {
|
|
460
|
-
throw new Error(
|
|
476
|
+
throw new Error(
|
|
477
|
+
`Enable when must be used for the same type, first type is ${firstValueType}, second type is ${secondValueType}`,
|
|
478
|
+
);
|
|
461
479
|
}
|
|
462
480
|
if (!_.includes(FHIRPrimitiveTypes, firstValueType)) {
|
|
463
481
|
throw new Error('Impossible to compare non-primitive type');
|
|
@@ -482,7 +500,9 @@ export function isValueEqual(firstValue: AnswerValue, secondValue: AnswerValue)
|
|
|
482
500
|
const secondValueType = getAnswerValueType(secondValue);
|
|
483
501
|
|
|
484
502
|
if (firstValueType !== secondValueType) {
|
|
485
|
-
console.error(
|
|
503
|
+
console.error(
|
|
504
|
+
`Enable when must be used for the same type, first type is ${firstValueType}, second type is ${secondValueType}`,
|
|
505
|
+
);
|
|
486
506
|
|
|
487
507
|
return false;
|
|
488
508
|
}
|
|
@@ -686,21 +706,19 @@ function removeDisabledAnswersRecursive(args: RemoveDisabledAnswersRecursiveArgs
|
|
|
686
706
|
|
|
687
707
|
return {
|
|
688
708
|
...acc,
|
|
689
|
-
[linkId!]: answers
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
return [...answersAcc, { ...answer, items }];
|
|
703
|
-
}, [] as any),
|
|
709
|
+
[linkId!]: cleanFormAnswerItems(answers).reduce((answersAcc, answer, index) => {
|
|
710
|
+
const items = hasSubAnswerItems(answer.items)
|
|
711
|
+
? removeDisabledAnswersRecursive({
|
|
712
|
+
questionnaireItems: questionnaireItem.item ?? [],
|
|
713
|
+
parentPath: [...args.parentPath, linkId!, index.toString(), 'items'],
|
|
714
|
+
answersItems: answer.items,
|
|
715
|
+
initialValues: { ...values, [linkId!]: [...answersAcc, { ...answer, items: [] }] },
|
|
716
|
+
context: args.context,
|
|
717
|
+
})
|
|
718
|
+
: {};
|
|
719
|
+
|
|
720
|
+
return [...answersAcc, { ...answer, items }];
|
|
721
|
+
}, [] as any),
|
|
704
722
|
};
|
|
705
723
|
}, {} as any);
|
|
706
724
|
}
|
|
@@ -825,9 +843,21 @@ export function getChoiceTypeValue(obj: Record<any, any>, prefix: string): any |
|
|
|
825
843
|
return prefixKey ? obj[prefixKey] : undefined;
|
|
826
844
|
}
|
|
827
845
|
|
|
828
|
-
|
|
846
|
+
type ExtractAnswerProps<T> = {
|
|
847
|
+
[K in keyof T as K extends `answer${string}` ? K : never]: T[K];
|
|
848
|
+
};
|
|
849
|
+
type ExtractValueProps<T> = {
|
|
850
|
+
[K in keyof T as K extends `value${string}` ? K : never]: T[K];
|
|
851
|
+
};
|
|
852
|
+
type GenericValue = ExtractValueProps<
|
|
853
|
+
QuestionnaireResponseItemAnswer | QuestionnaireItemInitial | QuestionnaireItemAnswerOption
|
|
854
|
+
>;
|
|
855
|
+
type GenericAnswer = ExtractAnswerProps<QuestionnaireItemEnableWhen>;
|
|
856
|
+
export function toAnswerValue(obj: GenericValue, prefix: 'value'): AnswerValue | undefined;
|
|
857
|
+
|
|
858
|
+
export function toAnswerValue(obj: GenericAnswer, prefix: 'answer'): AnswerValue | undefined;
|
|
859
|
+
export function toAnswerValue(obj: GenericAnswer | GenericValue, prefix: 'value' | 'answer'): AnswerValue | undefined {
|
|
829
860
|
const prefixKey = Object.keys(obj).filter((key: string) => key.startsWith(prefix))[0];
|
|
830
|
-
|
|
831
861
|
if (!prefixKey) {
|
|
832
862
|
return undefined;
|
|
833
863
|
}
|
|
@@ -835,7 +865,7 @@ export function toAnswerValue(obj: Record<any, any>, prefix: string): AnswerValu
|
|
|
835
865
|
const answerKey = FHIRPrimitiveTypes.includes(key) ? key : upperFirst(key);
|
|
836
866
|
|
|
837
867
|
return {
|
|
838
|
-
[answerKey]: obj[prefixKey],
|
|
868
|
+
[answerKey]: (obj as any)[prefixKey],
|
|
839
869
|
};
|
|
840
870
|
}
|
|
841
871
|
|
|
@@ -857,10 +887,16 @@ export function getAnswerValues(answers: FormAnswerItems[]) {
|
|
|
857
887
|
return _.reject(answers, ({ value }) => isAnswerValueEmpty(value)).map(({ value }) => value!);
|
|
858
888
|
}
|
|
859
889
|
|
|
860
|
-
export function isAnswerValueEmpty(value:
|
|
890
|
+
export function isAnswerValueEmpty(value: AnswerValue | undefined | null) {
|
|
861
891
|
return isValueEmpty(value) || _.every(_.mapValues(value, isValueEmpty));
|
|
862
892
|
}
|
|
863
893
|
|
|
894
|
+
export function cleanFormAnswerItems(answerItems: (FormAnswerItems | undefined)[] | undefined): FormAnswerItems[] {
|
|
895
|
+
// TODO: answerItems might be any here, we use _.filter because it handles object and nulls.
|
|
896
|
+
// TODO: get rid of it, once we have right types for all other functions
|
|
897
|
+
return _.filter(answerItems, (answer) => !!answer).filter((answer) => !isAnswerValueEmpty(answer.value));
|
|
898
|
+
}
|
|
899
|
+
|
|
864
900
|
export function isValueEmpty(value: any) {
|
|
865
901
|
if (_.isNaN(value)) {
|
|
866
902
|
console.warn(
|