/* * Copyright 2025 Commonwealth Scientific and Industrial Research * Organisation (CSIRO) ABN 41 687 119 230. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import type { Coding, QuestionnaireItemAnswerOption, QuestionnaireResponseItem, QuestionnaireResponseItemAnswer, ValueSet } from 'fhir/r4'; import type { ValueSetPromise } from '../interfaces/expressions.interface'; import { getRelevantCodingProperties } from './codingProperties'; export async function resolveValueSetPromises( valueSetPromises: Record ): Promise> { const newValueSetPromises: Record = {}; const valueSetPromiseKeys = Object.keys(valueSetPromises); const valueSetPromiseValues = Object.values(valueSetPromises); const promises = valueSetPromiseValues.map((valueSetPromise) => valueSetPromise.promise); const settledPromises = await Promise.allSettled(promises); for (const [i, settledPromise] of settledPromises.entries()) { if (settledPromise.status === 'rejected') { continue; } const key = valueSetPromiseKeys[i]; const valueSetPromise = valueSetPromiseValues[i]; if (key && valueSetPromise) { // Promises can be generated by a fhirClient (internally) or fetch/axios (via callback function), so we need to handle both scenarios const response = settledPromise.value; // Get valueSet from response (fhirClient and fetch scenario) if (responseIsValueSet(response)) { valueSetPromise.valueSet = response; } // Fallback to get valueSet from response.data (axios scenario) if (!valueSetPromise.valueSet && response.data && responseIsValueSet(response.data)) { valueSetPromise.valueSet = response.data; } newValueSetPromises[key] = valueSetPromise; } } return newValueSetPromises; } function responseIsValueSet(response: any): response is ValueSet { return response && response.resourceType === 'ValueSet'; } /** * Read a questionnaire response item recursively and retrieve valueSet answers if present * * @author Sean Fong */ export function filterValueSetAnswersRecursive( qrItem: QuestionnaireResponseItem, valueSetPromises: Record, answerOptions: Record, containedResources: Record ): QuestionnaireResponseItem | null { const items = qrItem.item; if (items && items.length > 0) { // iterate through items of item recursively const qrItems: QuestionnaireResponseItem[] = items .map((item) => filterValueSetAnswersRecursive(item, valueSetPromises, answerOptions, containedResources) ) .filter((item): item is QuestionnaireResponseItem => item !== null); return { ...qrItem, item: qrItems }; } const linkId = qrItem.linkId; const valueSetOptionCodings = valueSetPromises[linkId]?.valueSet?.expansion?.contains; if (qrItem.answer && valueSetOptionCodings) { return { ...qrItem, answer: filterAndNormaliseAnswers(qrItem.answer, valueSetOptionCodings) }; } const answerOptionCodings = answerOptions[linkId]?.map((option) => option.valueCoding); if (qrItem.answer && answerOptionCodings) { return { ...qrItem, answer: filterAndNormaliseAnswers(qrItem.answer, answerOptionCodings) }; } const containedValueSetOptionCodings = containedResources[linkId]?.expansion?.contains; if (qrItem.answer && containedValueSetOptionCodings) { const cleanedAnswers = filterAndNormaliseAnswers(qrItem.answer, containedValueSetOptionCodings); return cleanedAnswers.length > 0 ? { ...qrItem, answer: filterAndNormaliseAnswers(qrItem.answer, containedValueSetOptionCodings) } : null; } // If item does not have any valueSet nor answerOption return qrItem; } /** * Normalises a list of QuestionnaireResponse answers by: * - Filtering out valueCoding answers that are not present in the provided options * - Converting valueString answers to valueCoding when matching codes are found in options * - Preserving all other answers, including valueString answers that do not match any coding, * to support open-choice questions where arbitrary strings are allowed */ function filterAndNormaliseAnswers( answers: QuestionnaireResponseItemAnswer[], options: (Coding | undefined)[] ) { const newAnswers: QuestionnaireResponseItemAnswer[] = []; for (const answer of answers) { // answer is valueCoding, check if it is in options if (answer.valueCoding) { const valueCoding = codingIsInOptions(answer.valueCoding, options); if (valueCoding) { const newAnswer: QuestionnaireResponseItemAnswer = { valueCoding: valueCoding }; newAnswers.push(newAnswer); } // Add continue here to skip to the next iteration if answer coding is not in options continue; } // answer is valueString, attempt to parse it to valueCoding if (answer.valueString) { // attempt to obtain valueCodings in valueSet from valueString const newAnswer = parseStringToCoding(answer.valueString, options); newAnswers.push(newAnswer); continue; } // fallback to adding the answer as is newAnswers.push(answer); } return newAnswers; } function parseStringToCoding( value: string, options: (Coding | undefined)[] ): QuestionnaireResponseItemAnswer { if (!options) { return { valueString: value }; } const coding = options.find((coding) => coding?.code === value); // code is found in options, return as valueCoding if (coding) { return { valueCoding: getRelevantCodingProperties(coding) }; } // fallback to returning as valueString if no coding found return { valueString: value }; } function codingIsInOptions(answerCoding: Coding, options: (Coding | undefined)[]): Coding | null { if (!options) { return null; } const foundCoding = options.find((option) => option?.code === answerCoding.code); if (foundCoding) { return getRelevantCodingProperties(foundCoding); } return null; }