From e33c4f0a914fb1090a9e8c0cbaae477e97a55712 Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Fri, 31 May 2024 11:27:23 +0930 Subject: [PATCH 1/4] Make ontoserver $lookups tolerant of systems that can't be found --- .../utils/addDisplayToCodings.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/addDisplayToCodings.ts b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/addDisplayToCodings.ts index 4e30ff627..7024cd1e0 100644 --- a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/addDisplayToCodings.ts +++ b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/addDisplayToCodings.ts @@ -74,9 +74,14 @@ export async function resolveLookupPromises( const lookupPromiseValues = Object.values(codeSystemLookupPromises); const promises = lookupPromiseValues.map((lookupPromise) => lookupPromise.promise); - const lookupResults = await Promise.all(promises); + const settledPromises = await Promise.allSettled(promises); - for (const [i, lookupResult] of lookupResults.entries()) { + for (const [i, settledPromise] of settledPromises.entries()) { + if (settledPromise.status === 'rejected') { + continue; + } + + const lookupResult = settledPromise.value; if (!lookupResponseIsValid(lookupResult)) { continue; } From f91d56b025bf14379dcd8db0a1b7746bf691d34d Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Fri, 31 May 2024 12:56:08 +0930 Subject: [PATCH 2/4] Tweak default terminology url in renderer --- .../variables/terminologyServerStore.md | 2 +- .../variables/useTerminologyServerStore.md | 2 +- .../smart-forms-renderer/src/globals.ts | 4 +--- .../src/stores/terminologyServerStore.ts | 11 +++++------ 4 files changed, 8 insertions(+), 11 deletions(-) rename apps/smart-forms-app/src/utils/env.ts => packages/smart-forms-renderer/src/globals.ts (77%) diff --git a/documentation/docs/api/smart-forms-renderer/variables/terminologyServerStore.md b/documentation/docs/api/smart-forms-renderer/variables/terminologyServerStore.md index 224d20929..a6a6dcc45 100644 --- a/documentation/docs/api/smart-forms-renderer/variables/terminologyServerStore.md +++ b/documentation/docs/api/smart-forms-renderer/variables/terminologyServerStore.md @@ -3,7 +3,7 @@ > `const` **terminologyServerStore**: `StoreApi` \<[`TerminologyServerStoreType`](../interfaces/TerminologyServerStoreType.md)\> Terminology server state management store. This is used for resolving valueSets externally. -Defaults to use https://r4.ontoserver.csiro.au/fhir. +Defaults to use https://tx.ontoserver.csiro.au/fhir. This is the vanilla version of the store which can be used in non-React environments. ## See diff --git a/documentation/docs/api/smart-forms-renderer/variables/useTerminologyServerStore.md b/documentation/docs/api/smart-forms-renderer/variables/useTerminologyServerStore.md index 27dbb3cd9..f5f9bfc52 100644 --- a/documentation/docs/api/smart-forms-renderer/variables/useTerminologyServerStore.md +++ b/documentation/docs/api/smart-forms-renderer/variables/useTerminologyServerStore.md @@ -3,7 +3,7 @@ > `const` **useTerminologyServerStore**: `StoreApi` \<[`TerminologyServerStoreType`](../interfaces/TerminologyServerStoreType.md)\> & `object` Terminology server state management store. This is used for resolving valueSets externally. -Defaults to use https://r4.ontoserver.csiro.au/fhir. +Defaults to use https://tx.ontoserver.csiro.au/fhir. This is the React version of the store which can be used as React hooks in React functional components. ## See diff --git a/apps/smart-forms-app/src/utils/env.ts b/packages/smart-forms-renderer/src/globals.ts similarity index 77% rename from apps/smart-forms-app/src/utils/env.ts rename to packages/smart-forms-renderer/src/globals.ts index 0239377c8..36d067086 100644 --- a/apps/smart-forms-app/src/utils/env.ts +++ b/packages/smart-forms-renderer/src/globals.ts @@ -14,7 +14,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export const FORMS_SERVER_ENDPOINT = - import.meta.env.VITE_FORMS_SERVER_URL ?? 'https://smartforms.csiro.au/api/fhir'; -export const IS_IN_APP_POPULATE = import.meta.env.VITE_IN_APP_POPULATE ?? true; +export const TERMINOLOGY_SERVER_URL = 'https://tx.ontoserver.csiro.au/fhir'; diff --git a/packages/smart-forms-renderer/src/stores/terminologyServerStore.ts b/packages/smart-forms-renderer/src/stores/terminologyServerStore.ts index 77a257374..f16125a17 100644 --- a/packages/smart-forms-renderer/src/stores/terminologyServerStore.ts +++ b/packages/smart-forms-renderer/src/stores/terminologyServerStore.ts @@ -17,8 +17,7 @@ import { createStore } from 'zustand/vanilla'; import { createSelectors } from './selector'; - -const ONTOSERVER_R4 = 'https://r4.ontoserver.csiro.au/fhir'; +import { TERMINOLOGY_SERVER_URL } from '../globals'; /** * TerminologyServerStore properties and methods @@ -39,21 +38,21 @@ export interface TerminologyServerStoreType { /** * Terminology server state management store. This is used for resolving valueSets externally. - * Defaults to use https://r4.ontoserver.csiro.au/fhir. + * Defaults to use https://tx.ontoserver.csiro.au/fhir. * This is the vanilla version of the store which can be used in non-React environments. * @see TerminologyServerStoreType for available properties and methods. * * @author Sean Fong */ export const terminologyServerStore = createStore()((set) => ({ - url: ONTOSERVER_R4, + url: TERMINOLOGY_SERVER_URL, setUrl: (newUrl: string) => set(() => ({ url: newUrl })), - resetUrl: () => set(() => ({ url: ONTOSERVER_R4 })) + resetUrl: () => set(() => ({ url: TERMINOLOGY_SERVER_URL })) })); /** * Terminology server state management store. This is used for resolving valueSets externally. - * Defaults to use https://r4.ontoserver.csiro.au/fhir. + * Defaults to use https://tx.ontoserver.csiro.au/fhir. * This is the React version of the store which can be used as React hooks in React functional components. * @see TerminologyServerStoreType for available properties and methods. * From 1fe0ee903efefef179aab9fa1c6bf192c7d4ef8e Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Fri, 31 May 2024 12:58:16 +0930 Subject: [PATCH 3/4] Add optional terminology server callback to sdc-populate --- README.md | 2 +- apps/smart-forms-app/.env | 2 - apps/smart-forms-app/.env.production | 2 - .../DashboardDebugFooter.tsx | 9 +-- .../src/features/dashboard/utils/dashboard.ts | 9 +-- .../prepopulate/api/requestPopulate.ts | 22 ++++-- .../features/prepopulate/utils/callback.ts | 23 +++++- .../RendererDebugFooter/DebugPanel.tsx | 5 +- .../smartAppLaunch/components/Launch.tsx | 5 +- .../features/smartAppLaunch/utils/launch.ts | 5 +- .../components/StandaloneResourceViewer.tsx | 2 +- apps/smart-forms-app/src/globals.ts | 26 +++++++ apps/smart-forms-app/src/utils/assemble.ts | 7 +- .../api/expandValueset.ts | 23 ++++-- .../api/lookupCodeSystem.ts | 18 +++-- .../utils/addDisplayToCodings.ts | 12 ++- .../utils/constructResponse.ts | 74 +++++++++++++++---- .../utils/populate.ts | 20 +++-- packages/sdc-populate/src/globals.ts | 18 +++++ 19 files changed, 215 insertions(+), 69 deletions(-) create mode 100644 apps/smart-forms-app/src/globals.ts create mode 100644 packages/sdc-populate/src/globals.ts diff --git a/README.md b/README.md index 3a90640ab..6ff07d41d 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ The default configuration is set to: ``` # Ontoserver endpoint for $expand operations # To run your own Ontoserver instance, contact us at https://ontoserver.csiro.au/site/contact-us/ontoserver-contact-form/ -VITE_ONTOSERVER_URL=https://r4.ontoserver.csiro.au/fhir +VITE_ONTOSERVER_URL=https://tx.ontoserver.csiro.au/fhir # Questionnaire-hosting FHIR server VITE_FORMS_SERVER_URL=https://smartforms.csiro.au/api/fhir diff --git a/apps/smart-forms-app/.env b/apps/smart-forms-app/.env index 35b5a6487..fd0bbb6d1 100644 --- a/apps/smart-forms-app/.env +++ b/apps/smart-forms-app/.env @@ -5,5 +5,3 @@ VITE_LAUNCH_SCOPE=fhirUser online_access openid profile patient/Condition.rs pat VITE_LAUNCH_CLIENT_ID=a57d90e3-5f69-4b92-aa2e-2992180863c1 VITE_IN_APP_POPULATE=true - -VITE_SHOW_DEBUG_MODE=true diff --git a/apps/smart-forms-app/.env.production b/apps/smart-forms-app/.env.production index 363b4474d..fd0bbb6d1 100644 --- a/apps/smart-forms-app/.env.production +++ b/apps/smart-forms-app/.env.production @@ -5,5 +5,3 @@ VITE_LAUNCH_SCOPE=fhirUser online_access openid profile patient/Condition.rs pat VITE_LAUNCH_CLIENT_ID=a57d90e3-5f69-4b92-aa2e-2992180863c1 VITE_IN_APP_POPULATE=true - -VITE_SHOW_DEBUG_MODE=false diff --git a/apps/smart-forms-app/src/features/dashboard/components/DashboardDebugFooter/DashboardDebugFooter.tsx b/apps/smart-forms-app/src/features/dashboard/components/DashboardDebugFooter/DashboardDebugFooter.tsx index 0f7b8302a..6747ab28a 100644 --- a/apps/smart-forms-app/src/features/dashboard/components/DashboardDebugFooter/DashboardDebugFooter.tsx +++ b/apps/smart-forms-app/src/features/dashboard/components/DashboardDebugFooter/DashboardDebugFooter.tsx @@ -17,8 +17,8 @@ import { StyledRoot } from '../../../../components/DebugFooter/DebugFooter.styles.ts'; import { Box, Typography } from '@mui/material'; -import { FORMS_SERVER_ENDPOINT } from '../../../../utils/env.ts'; import useSmartClient from '../../../../hooks/useSmartClient.ts'; +import { FORMS_SERVER_URL, LAUNCH_CLIENT_ID } from '../../../../globals.ts'; function DashboardDebugFooter() { const { smartClient } = useSmartClient(); @@ -32,13 +32,10 @@ function DashboardDebugFooter() { {`Forms server: ${ - FORMS_SERVER_ENDPOINT ?? - 'Undefined. Defaulting to https://smartforms.csiro.au/api/fhir' + FORMS_SERVER_URL ?? 'Undefined. Defaulting to https://smartforms.csiro.au/api/fhir' }`} - - {`Client ID: ${import.meta.env.VITE_LAUNCH_CLIENT_ID}`} - + {`Client ID: ${LAUNCH_CLIENT_ID}`} diff --git a/apps/smart-forms-app/src/features/dashboard/utils/dashboard.ts b/apps/smart-forms-app/src/features/dashboard/utils/dashboard.ts index 9ce24ac01..94d78126d 100644 --- a/apps/smart-forms-app/src/features/dashboard/utils/dashboard.ts +++ b/apps/smart-forms-app/src/features/dashboard/utils/dashboard.ts @@ -30,13 +30,12 @@ import type Client from 'fhirclient/lib/Client'; import type { QuestionnaireListItem, ResponseListItem } from '../types/list.interface.ts'; import { HEADERS } from '../../../api/headers.ts'; import { nanoid } from 'nanoid'; - -const endpointUrl = import.meta.env.VITE_FORMS_SERVER_URL ?? 'https://smartforms.csiro.au/api/fhir'; +import { FORMS_SERVER_URL } from '../../../globals.ts'; export function getFormsServerBundlePromise(queryUrl: string): Promise { queryUrl = queryUrl.replace('|', '&version='); - return FHIR.client(endpointUrl).request({ + return FHIR.client(FORMS_SERVER_URL).request({ url: queryUrl, headers: HEADERS }); @@ -45,7 +44,7 @@ export function getFormsServerBundlePromise(queryUrl: string): Promise { export function getFormsServerAssembledBundlePromise(queryUrl: string): Promise { queryUrl = queryUrl.replace('|', '&version='); - return FHIR.client(endpointUrl).request({ + return FHIR.client(FORMS_SERVER_URL).request({ url: queryUrl, headers: HEADERS }); @@ -68,7 +67,7 @@ export function getFormsServerBundleOrQuestionnairePromise( queryUrl = queryUrl.substring(0, queryUrl.lastIndexOf('-SMARTcopy')) + ''; } - return FHIR.client(endpointUrl).request({ + return FHIR.client(FORMS_SERVER_URL).request({ url: queryUrl, headers: HEADERS }); diff --git a/apps/smart-forms-app/src/features/prepopulate/api/requestPopulate.ts b/apps/smart-forms-app/src/features/prepopulate/api/requestPopulate.ts index a8a8211b4..4ab912953 100644 --- a/apps/smart-forms-app/src/features/prepopulate/api/requestPopulate.ts +++ b/apps/smart-forms-app/src/features/prepopulate/api/requestPopulate.ts @@ -15,26 +15,36 @@ * limitations under the License. */ -import { IS_IN_APP_POPULATE } from '../../../utils/env.ts'; import type { InputParameters, OutputParameters } from '@aehrc/sdc-populate'; import { isOutputParameters, populate } from '@aehrc/sdc-populate'; import type { RequestConfig } from '../utils/callback.ts'; -import { fetchResourceCallback } from '../utils/callback.ts'; +import { fetchResourceCallback, terminologyCallback } from '../utils/callback.ts'; import { HEADERS } from '../../../api/headers.ts'; import type Client from 'fhirclient/lib/Client'; import type { OperationOutcome } from 'fhir/r4'; +import { IN_APP_POPULATE, TERMINOLOGY_SERVER_URL } from '../../../globals.ts'; export async function requestPopulate( fhirClient: Client, inputParameters: InputParameters ): Promise { - const requestConfig: RequestConfig = { + const fetchResourceRequestConfig: RequestConfig = { clientEndpoint: fhirClient.state.serverUrl, authToken: fhirClient.state.tokenResponse!.access_token! }; - const populatePromise: Promise = IS_IN_APP_POPULATE - ? populate(inputParameters, fetchResourceCallback, requestConfig) + const terminologyRequestConfig: RequestConfig = { + clientEndpoint: TERMINOLOGY_SERVER_URL + }; + + const populatePromise: Promise = IN_APP_POPULATE + ? populate( + inputParameters, + fetchResourceCallback, + fetchResourceRequestConfig, + terminologyCallback, + terminologyRequestConfig + ) : fhirClient.request({ url: 'Questionnaire/$populate', method: 'POST', @@ -42,7 +52,7 @@ export async function requestPopulate( headers: { ...HEADERS, 'Content-Type': 'application/json', - Authorization: `Bearer ${requestConfig.authToken}` + Authorization: `Bearer ${fetchResourceRequestConfig.authToken}` } }); diff --git a/apps/smart-forms-app/src/features/prepopulate/utils/callback.ts b/apps/smart-forms-app/src/features/prepopulate/utils/callback.ts index 8b6d50329..8629907e2 100644 --- a/apps/smart-forms-app/src/features/prepopulate/utils/callback.ts +++ b/apps/smart-forms-app/src/features/prepopulate/utils/callback.ts @@ -22,7 +22,7 @@ const ABSOLUTE_URL_REGEX = /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/; export interface RequestConfig { clientEndpoint: string; - authToken: string; + authToken?: string; } export const fetchResourceCallback: FetchResourceCallback = ( @@ -47,3 +47,24 @@ export const fetchResourceCallback: FetchResourceCallback = ( headers: headers }); }; + +export const terminologyCallback: FetchResourceCallback = ( + query: string, + requestConfig: RequestConfig +) => { + let { clientEndpoint } = requestConfig; + + const headers = { + Accept: 'application/json;charset=utf-8' + }; + + if (!clientEndpoint.endsWith('/')) { + clientEndpoint += '/'; + } + + const queryUrl = ABSOLUTE_URL_REGEX.test(query) ? query : clientEndpoint + query; + + return axios.get(queryUrl, { + headers: headers + }); +}; diff --git a/apps/smart-forms-app/src/features/renderer/components/RendererDebugFooter/DebugPanel.tsx b/apps/smart-forms-app/src/features/renderer/components/RendererDebugFooter/DebugPanel.tsx index 5ce7bd3c5..dae2cde40 100644 --- a/apps/smart-forms-app/src/features/renderer/components/RendererDebugFooter/DebugPanel.tsx +++ b/apps/smart-forms-app/src/features/renderer/components/RendererDebugFooter/DebugPanel.tsx @@ -25,8 +25,7 @@ import AccountTreeIcon from '@mui/icons-material/AccountTree'; import NotesIcon from '@mui/icons-material/Notes'; import DebugResponseView from './DebugResponseView.tsx'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; - -const endpointUrl = import.meta.env.VITE_FORMS_SERVER_URL ?? 'https://smartforms.csiro.au/api/fhir'; +import { FORMS_SERVER_URL } from '../../../../globals.ts'; interface Props { questionnaire: Questionnaire; @@ -96,7 +95,7 @@ function DebugPanel(props: Props) { {questionnaireSelected ? ( diff --git a/apps/smart-forms-app/src/features/smartAppLaunch/components/Launch.tsx b/apps/smart-forms-app/src/features/smartAppLaunch/components/Launch.tsx index 09dcf3388..2956cbaab 100644 --- a/apps/smart-forms-app/src/features/smartAppLaunch/components/Launch.tsx +++ b/apps/smart-forms-app/src/features/smartAppLaunch/components/Launch.tsx @@ -19,6 +19,7 @@ import { useSearchParams } from 'react-router-dom'; import { oauth2 } from 'fhirclient'; import { useState } from 'react'; import LaunchView from './LaunchView.tsx'; +import { LAUNCH_CLIENT_ID, LAUNCH_SCOPE } from '../../../globals.ts'; export type LaunchState = 'loading' | 'error' | 'success'; @@ -29,8 +30,8 @@ function Launch() { const iss = searchParams.get('iss'); const launch = searchParams.get('launch'); - const clientId = import.meta.env.VITE_LAUNCH_CLIENT_ID; - const scope = import.meta.env.VITE_LAUNCH_SCOPE; + const clientId = LAUNCH_CLIENT_ID; + const scope = LAUNCH_SCOPE; if (iss && launch) { // oauth2.authorize triggers a redirect to EHR diff --git a/apps/smart-forms-app/src/features/smartAppLaunch/utils/launch.ts b/apps/smart-forms-app/src/features/smartAppLaunch/utils/launch.ts index 0abdfd79a..3e4aa95f4 100644 --- a/apps/smart-forms-app/src/features/smartAppLaunch/utils/launch.ts +++ b/apps/smart-forms-app/src/features/smartAppLaunch/utils/launch.ts @@ -28,8 +28,7 @@ import type { import type { fhirclient } from 'fhirclient/lib/types'; import * as FHIR from 'fhirclient'; import { HEADERS } from '../../../api/headers.ts'; - -const endpointUrl = import.meta.env.VITE_FORMS_SERVER_URL ?? 'https://smartforms.csiro.au/api/fhir'; +import { FORMS_SERVER_URL } from '../../../globals.ts'; export async function readCommonLaunchContexts( client: Client @@ -117,7 +116,7 @@ export function readQuestionnaireContext( canonical = canonical.replace('|', '&version='); - return FHIR.client(endpointUrl).request({ + return FHIR.client(FORMS_SERVER_URL).request({ url: 'Questionnaire?url=' + canonical + '&_sort=_lastUpdated', method: 'GET', headers: HEADERS diff --git a/apps/smart-forms-app/src/features/standalone/components/StandaloneResourceViewer.tsx b/apps/smart-forms-app/src/features/standalone/components/StandaloneResourceViewer.tsx index f7998bb3d..2d747f9d5 100644 --- a/apps/smart-forms-app/src/features/standalone/components/StandaloneResourceViewer.tsx +++ b/apps/smart-forms-app/src/features/standalone/components/StandaloneResourceViewer.tsx @@ -99,7 +99,7 @@ function ResourceViewSwitcher(props: ResourceViewSwitcherProps) { return ( <> - Defaults to https://r4.ontoserver.csiro.au if null + Defaults to https://tx.ontoserver.csiro.au if null
{JSON.stringify(state.terminologyServerUrl, null, 2)}
diff --git a/apps/smart-forms-app/src/globals.ts b/apps/smart-forms-app/src/globals.ts new file mode 100644 index 000000000..ff69d6146 --- /dev/null +++ b/apps/smart-forms-app/src/globals.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2024 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. + */ + +export const TERMINOLOGY_SERVER_URL = + import.meta.env.VITE_ONTOSERVER_URL ?? 'https://tx.ontoserver.csiro.au/fhir'; +export const FORMS_SERVER_URL = + import.meta.env.VITE_FORMS_SERVER_URL ?? 'https://smartforms.csiro.au/api/fhir'; +export const LAUNCH_SCOPE = + import.meta.env.VITE_LAUNCH_SCOPE ?? + 'fhirUser online_access openid profile patient/Condition.rs patient/Observation.rs launch patient/Encounter.rs patient/QuestionnaireResponse.cruds patient/Patient.rs'; +export const LAUNCH_CLIENT_ID = import.meta.env.VITE_LAUNCH_CLIENT_ID ?? 'smart-forms-client-id'; +export const IN_APP_POPULATE = import.meta.env.VITE_IN_APP_POPULATE ?? true; diff --git a/apps/smart-forms-app/src/utils/assemble.ts b/apps/smart-forms-app/src/utils/assemble.ts index d1b22952f..2788c6eaf 100644 --- a/apps/smart-forms-app/src/utils/assemble.ts +++ b/apps/smart-forms-app/src/utils/assemble.ts @@ -20,8 +20,7 @@ import { isInputParameters } from '@aehrc/sdc-assemble'; import * as FHIR from 'fhirclient'; import { HEADERS } from '../api/headers.ts'; import { getFormsServerAssembledBundlePromise } from '../features/dashboard/utils/dashboard.ts'; - -const endpointUrl = import.meta.env.VITE_FORMS_SERVER_URL ?? 'https://smartforms.csiro.au/api/fhir'; +import { FORMS_SERVER_URL } from '../globals.ts'; export function assemblyIsRequired(questionnaire: Questionnaire): boolean { return !!questionnaire.extension?.find( @@ -49,7 +48,7 @@ export async function assembleQuestionnaire( ): Promise { const parameters = defineAssembleParameters(questionnaire); if (isInputParameters(parameters)) { - const outputAssembleParams = await FHIR.client(endpointUrl).request({ + const outputAssembleParams = await FHIR.client(FORMS_SERVER_URL).request({ url: 'Questionnaire/$assemble', method: 'POST', body: JSON.stringify(parameters), @@ -67,7 +66,7 @@ export async function assembleQuestionnaire( } export function updateAssembledQuestionnaire(questionnaire: Questionnaire) { - return FHIR.client(endpointUrl).request({ + return FHIR.client(FORMS_SERVER_URL).request({ url: `Questionnaire/${questionnaire.id}`, method: 'PUT', body: JSON.stringify(questionnaire), diff --git a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/api/expandValueset.ts b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/api/expandValueset.ts index b4e135d95..3049aa1de 100644 --- a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/api/expandValueset.ts +++ b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/api/expandValueset.ts @@ -18,13 +18,15 @@ import * as FHIR from 'fhirclient'; import type { ValueSetPromise } from '../interfaces/expressions.interface'; import type { QuestionnaireItem } from 'fhir/r4'; - -export const ONTOSERVER_ENDPOINT = 'https://r4.ontoserver.csiro.au/fhir/'; +import type { FetchResourceCallback } from '../interfaces'; +import { TERMINOLOGY_SERVER_URL } from '../../globals'; export function getValueSetPromise( qItem: QuestionnaireItem, fullUrl: string, - valueSetPromiseMap: Record + valueSetPromiseMap: Record, + terminologyCallback?: FetchResourceCallback, + terminologyRequestConfig?: any ) { let valueSetUrl = fullUrl; if (fullUrl.includes('ValueSet/$expand?url=')) { @@ -35,10 +37,19 @@ export function getValueSetPromise( } valueSetUrl = valueSetUrl.replace('|', '&version='); + const query = `ValueSet/$expand?url=${valueSetUrl}`; + + const valueSetPromise = terminologyCallback + ? terminologyCallback(query, terminologyRequestConfig) + : defaultTerminologyRequest(query); valueSetPromiseMap[qItem.linkId] = { - promise: FHIR.client({ serverUrl: ONTOSERVER_ENDPOINT }).request({ - url: 'ValueSet/$expand?url=' + valueSetUrl - }) + promise: valueSetPromise }; } + +export function defaultTerminologyRequest(query: string) { + return FHIR.client({ serverUrl: TERMINOLOGY_SERVER_URL }).request({ + url: query + }); +} diff --git a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/api/lookupCodeSystem.ts b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/api/lookupCodeSystem.ts index 357d87b71..86fe345a0 100644 --- a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/api/lookupCodeSystem.ts +++ b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/api/lookupCodeSystem.ts @@ -16,20 +16,24 @@ */ import type { Coding } from 'fhir/r4'; -import * as FHIR from 'fhirclient'; -import { ONTOSERVER_ENDPOINT } from './expandValueset'; import type { CodeSystemLookupPromise } from '../interfaces/expressions.interface'; +import type { FetchResourceCallback } from '../interfaces'; +import { defaultTerminologyRequest } from './expandValueset'; export function getCodeSystemLookupPromise( coding: Coding, - codeSystemLookupPromiseMap: Record + codeSystemLookupPromiseMap: Record, + terminologyCallback?: FetchResourceCallback, + terminologyRequestConfig?: any ) { - const query = `system=${coding.system}&code=${coding.code}`; + const query = `CodeSystem/$lookup?system=${coding.system}&code=${coding.code}`; + + const lookupPromise = terminologyCallback + ? terminologyCallback(query, terminologyRequestConfig) + : defaultTerminologyRequest(query); codeSystemLookupPromiseMap[query] = { - promise: FHIR.client({ serverUrl: ONTOSERVER_ENDPOINT }).request({ - url: `CodeSystem/$lookup?${query}` - }), + promise: lookupPromise, oldCoding: coding }; } diff --git a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/addDisplayToCodings.ts b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/addDisplayToCodings.ts index 7024cd1e0..fd67c5d16 100644 --- a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/addDisplayToCodings.ts +++ b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/addDisplayToCodings.ts @@ -21,9 +21,12 @@ import type { } from '../interfaces/expressions.interface'; import type { Coding } from 'fhir/r4'; import { getCodeSystemLookupPromise, lookupResponseIsValid } from '../api/lookupCodeSystem'; +import type { FetchResourceCallback } from '../interfaces'; export async function addDisplayToInitialExpressionsCodings( - initialExpressions: Record + initialExpressions: Record, + terminologyCallback?: FetchResourceCallback, + terminologyRequestConfig?: any ): Promise> { // Store code system lookup promises for codings without displays const codeSystemLookupPromises: Record = {}; @@ -36,7 +39,12 @@ export async function addDisplayToInitialExpressionsCodings( for (const value of initialExpression.value) { if (valueIsCoding(value)) { if (!value.display) { - getCodeSystemLookupPromise(value, codeSystemLookupPromises); + getCodeSystemLookupPromise( + value, + codeSystemLookupPromises, + terminologyCallback, + terminologyRequestConfig + ); } } } diff --git a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/constructResponse.ts b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/constructResponse.ts index f8b21737e..10e620195 100644 --- a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/constructResponse.ts +++ b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/constructResponse.ts @@ -41,6 +41,7 @@ import { getItemPopulationContextName } from './readPopulationExpressions'; import { createQuestionnaireReference } from './createQuestionnaireReference'; import { parseItemInitialToAnswer, parseValueToAnswer } from './parse'; import { getValueSetPromise } from '../api/expandValueset'; +import type { FetchResourceCallback } from '../interfaces'; /** * Constructs a questionnaireResponse recursively from a specified questionnaire, its subject and its initialExpressions @@ -49,6 +50,8 @@ import { getValueSetPromise } from '../api/expandValueset'; * @param subject - A subject reference to form the subject within the response * @param populationExpressions - expressions used for pre-population i.e. initialExpressions, itemPopulationContexts * @param encounter - An optional encounter resource to form the questionnaireResponse.encounter property + * @param terminologyCallback - An optional callback function to fetch terminology resources + * @param terminologyRequestConfig - An optional configuration object to pass to the terminologyCallback * @returns A populated questionnaire response wrapped within a Promise * * @author Sean Fong @@ -57,7 +60,9 @@ export async function constructResponse( questionnaire: Questionnaire, subject: Reference, populationExpressions: PopulationExpressions, - encounter?: Encounter + encounter?: Encounter, + terminologyCallback?: FetchResourceCallback, + terminologyRequestConfig?: any ): Promise { const questionnaireResponse: QuestionnaireResponse = { resourceType: 'QuestionnaireResponse', @@ -89,7 +94,9 @@ export async function constructResponse( populationExpressions, valueSetPromises, answerOptions, - containedValueSets + containedValueSets, + terminologyCallback, + terminologyRequestConfig }); if (Array.isArray(newTopLevelQRItem)) { @@ -148,6 +155,8 @@ interface ConstructResponseItemRecursiveParams { valueSetPromises: Record; answerOptions: Record; containedValueSets: Record; + terminologyCallback?: FetchResourceCallback | undefined; + terminologyRequestConfig?: any; } /** @@ -165,7 +174,9 @@ function constructResponseItemRecursive( populationExpressions, valueSetPromises, answerOptions, - containedValueSets + containedValueSets, + terminologyCallback, + terminologyRequestConfig } = params; const items = qItem.item; @@ -196,7 +207,9 @@ function constructResponseItemRecursive( populationExpressions, valueSetPromises, answerOptions, - containedValueSets + containedValueSets, + terminologyCallback, + terminologyRequestConfig }); if (Array.isArray(newQrItem)) { @@ -215,7 +228,9 @@ function constructResponseItemRecursive( populationExpressions, valueSetPromises, answerOptions, - containedValueSets + containedValueSets, + terminologyCallback, + terminologyRequestConfig }); } @@ -237,6 +252,8 @@ interface ConstructGroupItemParams { valueSetPromises: Record; answerOptions: Record; containedValueSets: Record; + terminologyCallback?: FetchResourceCallback | undefined; + terminologyRequestConfig?: any; } function constructGroupItem(params: ConstructGroupItemParams): QuestionnaireResponseItem | null { @@ -247,7 +264,9 @@ function constructGroupItem(params: ConstructGroupItemParams): QuestionnaireResp populationExpressions, valueSetPromises, answerOptions, - containedValueSets + containedValueSets, + terminologyCallback, + terminologyRequestConfig } = params; const { initialExpressions } = populationExpressions; @@ -263,7 +282,12 @@ function constructGroupItem(params: ConstructGroupItemParams): QuestionnaireResp populatedAnswers = newValues; if (expandRequired) { - recordAnswerValueSet(qItem, valueSetPromises); + recordAnswerValueSet( + qItem, + valueSetPromises, + terminologyCallback, + terminologyRequestConfig + ); } recordAnswerOption(qItem, answerOptions); @@ -305,6 +329,8 @@ interface ConstructSingleItemParams { valueSetPromises: Record; answerOptions: Record; containedValueSets: Record; + terminologyCallback?: FetchResourceCallback; + terminologyRequestConfig?: any; } function constructSingleItem(params: ConstructSingleItemParams): QuestionnaireResponseItem | null { @@ -314,7 +340,9 @@ function constructSingleItem(params: ConstructSingleItemParams): QuestionnaireRe populationExpressions, valueSetPromises, answerOptions, - containedValueSets + containedValueSets, + terminologyCallback, + terminologyRequestConfig } = params; const { initialExpressions } = populationExpressions; @@ -328,7 +356,12 @@ function constructSingleItem(params: ConstructSingleItemParams): QuestionnaireRe const { newValues, expandRequired } = getAnswerValues(initialValues, qItem); if (expandRequired) { - recordAnswerValueSet(qItem, valueSetPromises); + recordAnswerValueSet( + qItem, + valueSetPromises, + terminologyCallback, + terminologyRequestConfig + ); } recordAnswerOption(qItem, answerOptions); @@ -358,10 +391,18 @@ function constructSingleItem(params: ConstructSingleItemParams): QuestionnaireRe function recordAnswerValueSet( qItem: QuestionnaireItem, - valueSetPromises: Record + valueSetPromises: Record, + terminologyCallback?: FetchResourceCallback, + terminologyRequestConfig?: any ) { if (qItem.answerValueSet) { - getValueSetPromise(qItem, qItem.answerValueSet, valueSetPromises); + getValueSetPromise( + qItem, + qItem.answerValueSet, + valueSetPromises, + terminologyCallback, + terminologyRequestConfig + ); } } @@ -461,7 +502,9 @@ function constructRepeatGroupInstances( populationExpressions: PopulationExpressions, valueSetPromises: Record, answerOptions: Record, - containedValueSets: Record + containedValueSets: Record, + terminologyCallback?: FetchResourceCallback, + terminologyRequestConfig?: any ): QuestionnaireResponseItem[] { if (!qRepeatGroupParent.item || !qRepeatGroupParent.item[0]) { return []; @@ -529,7 +572,12 @@ function constructRepeatGroupInstances( const { newValues, expandRequired } = getAnswerValues(initialValues, childItem); if (expandRequired) { - recordAnswerValueSet(childItem, valueSetPromises); + recordAnswerValueSet( + childItem, + valueSetPromises, + terminologyCallback, + terminologyRequestConfig + ); } recordAnswerOption(childItem, answerOptions); diff --git a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/populate.ts b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/populate.ts index 078573f20..b9a622009 100644 --- a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/populate.ts +++ b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/populate.ts @@ -42,11 +42,17 @@ import { addDisplayToInitialExpressionsCodings } from './addDisplayToCodings'; export async function populate( parameters: InputParameters, fetchResourceCallback: FetchResourceCallback, - requestConfig: any + fetchResourceRequestConfig: any, + terminologyCallback?: FetchResourceCallback, + terminologyRequestConfig?: any ): Promise { const issues: OperationOutcomeIssue[] = []; - const questionnaire = await fetchQuestionnaire(parameters, fetchResourceCallback, requestConfig); + const questionnaire = await fetchQuestionnaire( + parameters, + fetchResourceCallback, + fetchResourceRequestConfig + ); if (questionnaire.resourceType === 'OperationOutcome') { return questionnaire; } @@ -61,7 +67,7 @@ export async function populate( parameters, questionnaire, fetchResourceCallback, - requestConfig, + fetchResourceRequestConfig, issues ); @@ -86,7 +92,9 @@ export async function populate( // In evaluatedInitialExpressions, add display values to codings lacking them const completeInitialExpressions = await addDisplayToInitialExpressionsCodings( - evaluatedInitialExpressions + evaluatedInitialExpressions, + terminologyCallback, + terminologyRequestConfig ); // Construct response from initialExpressions @@ -97,7 +105,9 @@ export async function populate( initialExpressions: completeInitialExpressions, itemPopulationContexts: evaluatedItemPopulationContexts }, - encounter + encounter, + terminologyCallback, + terminologyRequestConfig ); const cleanQuestionnaireResponse = removeEmptyAnswersFromResponse( diff --git a/packages/sdc-populate/src/globals.ts b/packages/sdc-populate/src/globals.ts new file mode 100644 index 000000000..36d067086 --- /dev/null +++ b/packages/sdc-populate/src/globals.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 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. + */ + +export const TERMINOLOGY_SERVER_URL = 'https://tx.ontoserver.csiro.au/fhir'; From 6022e0be73c168e06ce7b75b314cb36d0d1f5b9e Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Fri, 31 May 2024 13:32:33 +0930 Subject: [PATCH 4/4] Add optional terminology server callback to in-app population exposed functions and update package versions --- apps/smart-forms-app/package.json | 4 +-- documentation/package.json | 2 +- package-lock.json | 12 +++---- packages/sdc-populate/package.json | 2 +- .../utils/populateQuestionnaire.ts | 31 ++++++++++++++++--- packages/smart-forms-renderer/package.json | 4 +-- 6 files changed, 39 insertions(+), 16 deletions(-) diff --git a/apps/smart-forms-app/package.json b/apps/smart-forms-app/package.json index 7f4f4daa9..66e3ce2fb 100644 --- a/apps/smart-forms-app/package.json +++ b/apps/smart-forms-app/package.json @@ -26,8 +26,8 @@ "homepage": "https://github.com/aehrc/smart-forms#readme", "dependencies": { "@aehrc/sdc-assemble": "^1.2.0", - "@aehrc/sdc-populate": "^2.0.3", - "@aehrc/smart-forms-renderer": "^0.32.2", + "@aehrc/sdc-populate": "^2.1.0", + "@aehrc/smart-forms-renderer": "^0.33.0", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fontsource/material-icons": "^5.0.16", diff --git a/documentation/package.json b/documentation/package.json index 16536c30e..1486a31e3 100644 --- a/documentation/package.json +++ b/documentation/package.json @@ -15,7 +15,7 @@ "typecheck": "tsc" }, "dependencies": { - "@aehrc/smart-forms-renderer": "^0.32.2", + "@aehrc/smart-forms-renderer": "^0.33.0", "@docusaurus/core": "3.3.2", "@docusaurus/preset-classic": "3.3.2", "@docusaurus/theme-live-codeblock": "^3.3.2", diff --git a/package-lock.json b/package-lock.json index 420d12fd3..0c3941587 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,8 +49,8 @@ "license": "Apache-2.0", "dependencies": { "@aehrc/sdc-assemble": "^1.2.0", - "@aehrc/sdc-populate": "^2.0.3", - "@aehrc/smart-forms-renderer": "^0.32.2", + "@aehrc/sdc-populate": "^2.1.0", + "@aehrc/smart-forms-renderer": "^0.33.0", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fontsource/material-icons": "^5.0.16", @@ -464,7 +464,7 @@ "name": "@aehrc/smart-forms-documentation", "version": "0.0.0", "dependencies": { - "@aehrc/smart-forms-renderer": "^0.32.2", + "@aehrc/smart-forms-renderer": "^0.33.0", "@docusaurus/core": "3.3.2", "@docusaurus/preset-classic": "3.3.2", "@docusaurus/theme-live-codeblock": "^3.3.2", @@ -43916,7 +43916,7 @@ }, "packages/sdc-populate": { "name": "@aehrc/sdc-populate", - "version": "2.0.3", + "version": "2.1.0", "license": "Apache-2.0", "dependencies": { "dayjs": "^1.11.10", @@ -43945,10 +43945,10 @@ }, "packages/smart-forms-renderer": { "name": "@aehrc/smart-forms-renderer", - "version": "0.32.2", + "version": "0.33.0", "license": "Apache-2.0", "dependencies": { - "@aehrc/sdc-populate": "^2.0.3", + "@aehrc/sdc-populate": "^2.1.0", "@iconify/react": "^4.1.1", "dayjs": "^1.11.10", "deep-diff": "^1.0.2", diff --git a/packages/sdc-populate/package.json b/packages/sdc-populate/package.json index ec58ea9a2..b0a364f34 100644 --- a/packages/sdc-populate/package.json +++ b/packages/sdc-populate/package.json @@ -1,6 +1,6 @@ { "name": "@aehrc/sdc-populate", - "version": "2.0.3", + "version": "2.1.0", "description": "Performs the $populate operation from the HL7 FHIR SDC (Structured Data Capture) specification: http://hl7.org/fhir/uv/sdc", "main": "lib/index.js", "scripts": { diff --git a/packages/sdc-populate/src/inAppPopulation/utils/populateQuestionnaire.ts b/packages/sdc-populate/src/inAppPopulation/utils/populateQuestionnaire.ts index 952515573..65e6093a8 100644 --- a/packages/sdc-populate/src/inAppPopulation/utils/populateQuestionnaire.ts +++ b/packages/sdc-populate/src/inAppPopulation/utils/populateQuestionnaire.ts @@ -55,6 +55,8 @@ export interface PopulateResult { * @property patient - Patient resource as patient in context * @property user - Practitioner resource as user in context * @property encounter - Encounter resource as encounter in context, optional + * @property terminologyCallback - A callback function to fetch terminology resources, optional + * @property terminologyRequestConfig - Any request configuration to be passed to the terminologyCallback i.e. headers, auth etc., optional * * @author Sean Fong */ @@ -65,6 +67,8 @@ export interface PopulateQuestionnaireParams { patient: Patient; user?: Practitioner; encounter?: Encounter; + terminologyCallback?: FetchResourceCallback; + terminologyRequestConfig?: any; } /** @@ -83,7 +87,16 @@ export async function populateQuestionnaire(params: PopulateQuestionnaireParams) populateSuccess: boolean; populateResult: PopulateResult | null; }> { - const { questionnaire, fetchResourceCallback, requestConfig, patient, user, encounter } = params; + const { + questionnaire, + fetchResourceCallback, + requestConfig, + patient, + user, + encounter, + terminologyCallback, + terminologyRequestConfig + } = params; const context: Record = {}; @@ -133,7 +146,9 @@ export async function populateQuestionnaire(params: PopulateQuestionnaireParams) const outputParameters = await performInAppPopulation( inputParameters, fetchResourceCallback, - requestConfig + requestConfig, + terminologyCallback, + terminologyRequestConfig ); if (outputParameters.resourceType === 'OperationOutcome') { @@ -171,9 +186,17 @@ export async function populateQuestionnaire(params: PopulateQuestionnaireParams) async function performInAppPopulation( inputParameters: InputParameters, fetchResourceCallback: FetchResourceCallback, - requestConfig: any + requestConfig: any, + terminologyCallback?: FetchResourceCallback, + terminologyRequestConfig?: any ): Promise { - const populatePromise = populate(inputParameters, fetchResourceCallback, requestConfig); + const populatePromise = populate( + inputParameters, + fetchResourceCallback, + requestConfig, + terminologyCallback, + terminologyRequestConfig + ); try { const promiseResult = await addTimeoutToPromise(populatePromise, 10000); diff --git a/packages/smart-forms-renderer/package.json b/packages/smart-forms-renderer/package.json index b297b83b0..05fd08425 100644 --- a/packages/smart-forms-renderer/package.json +++ b/packages/smart-forms-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@aehrc/smart-forms-renderer", - "version": "0.32.2", + "version": "0.33.0", "description": "FHIR Structured Data Captured (SDC) rendering engine for Smart Forms", "main": "lib/index.js", "scripts": { @@ -27,7 +27,7 @@ }, "homepage": "https://github.com/aehrc/smart-forms#readme", "dependencies": { - "@aehrc/sdc-populate": "^2.0.3", + "@aehrc/sdc-populate": "^2.1.0", "@iconify/react": "^4.1.1", "dayjs": "^1.11.10", "deep-diff": "^1.0.2",