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.
Files changed (48) hide show
  1. package/dist/converter/extensions.d.ts +9 -2
  2. package/dist/converter/extensions.d.ts.map +1 -1
  3. package/dist/converter/extensions.js +66 -2
  4. package/dist/converter/extensions.js.map +1 -1
  5. package/dist/converter/fhirToFce/questionnaire/processItems.d.ts.map +1 -1
  6. package/dist/converter/fhirToFce/questionnaire/processItems.js +33 -23
  7. package/dist/converter/fhirToFce/questionnaire/processItems.js.map +1 -1
  8. package/dist/converter/index.d.ts.map +1 -1
  9. package/dist/converter/index.js +2 -1
  10. package/dist/converter/index.js.map +1 -1
  11. package/dist/converter/utils.d.ts +1 -2
  12. package/dist/converter/utils.d.ts.map +1 -1
  13. package/dist/converter/utils.js +0 -5
  14. package/dist/converter/utils.js.map +1 -1
  15. package/dist/fce.types.d.ts +13 -1
  16. package/dist/fce.types.d.ts.map +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +6 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/types.d.ts +1 -1
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/utils.d.ts +16 -6
  24. package/dist/utils.d.ts.map +1 -1
  25. package/dist/utils.js +42 -26
  26. package/dist/utils.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/converter/__tests__/fce.test.ts +88 -54
  29. package/src/converter/__tests__/resources/questionnaire_fce/answerOptionsToggleExpression.json +84 -0
  30. package/src/converter/__tests__/resources/questionnaire_fce/mixed-fce-with-extensions.json +26 -0
  31. package/src/converter/__tests__/resources/questionnaire_fce/occurs.json +16 -0
  32. package/src/converter/__tests__/resources/questionnaire_fce/sub-questionnaire.json +22 -0
  33. package/src/converter/__tests__/resources/questionnaire_fce/unknown-extensions.json +58 -0
  34. package/src/converter/__tests__/resources/questionnaire_fce/with-attachment-question.json +17 -0
  35. package/src/converter/__tests__/resources/questionnaire_fhir/answerOptionsToggleExpression.json +96 -0
  36. package/src/converter/__tests__/resources/questionnaire_fhir/mixed-fce-with-extensions.json +27 -0
  37. package/src/converter/__tests__/resources/questionnaire_fhir/occurs.json +24 -0
  38. package/src/converter/__tests__/resources/questionnaire_fhir/sub-questionnaire.json +32 -0
  39. package/src/converter/__tests__/resources/questionnaire_fhir/unknown-extensions.json +62 -0
  40. package/src/converter/__tests__/resources/questionnaire_fhir/with-attachment-question.json +26 -0
  41. package/src/converter/extensions.ts +78 -4
  42. package/src/converter/fhirToFce/questionnaire/processItems.ts +43 -34
  43. package/src/converter/index.ts +3 -1
  44. package/src/converter/utils.ts +1 -5
  45. package/src/fce.types.ts +18 -0
  46. package/src/index.ts +5 -1
  47. package/src/types.ts +2 -1
  48. 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
- export type FormItems = Record<string, FormGroupItems | FormAnswerItems[] | undefined>;
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: answers
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
- export function findAnswersForQuestionsRecursive(linkId: string, values?: FormItems): any | null {
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.items);
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
- function findAnswersForQuestion(linkId: string, parentPath: string[], values: FormItems): Array<FormAnswerItems> {
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('Enable when must be used for the same type');
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('Enable when must be used for the same type');
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
- .filter((answer) => !isAnswerValueEmpty(answer.value))
691
- .reduce((answersAcc, answer, index) => {
692
- const items = hasSubAnswerItems(answer.items)
693
- ? removeDisabledAnswersRecursive({
694
- questionnaireItems: questionnaireItem.item ?? [],
695
- parentPath: [...args.parentPath, linkId!, index.toString(), 'items'],
696
- answersItems: answer.items,
697
- initialValues: { ...values, [linkId!]: [...answersAcc, { ...answer, items: [] }] },
698
- context: args.context,
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
- export function toAnswerValue(obj: Record<any, any>, prefix: string): AnswerValue | undefined {
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: { [x: string]: any } | undefined | null) {
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(