From ac36753bb119cd92732fb71d12f0c5108e3057d3 Mon Sep 17 00:00:00 2001 From: c-schuler Date: Tue, 19 Nov 2024 12:02:37 -0700 Subject: [PATCH] Fixed lint issues and test failures; replaced manual promise counting, streamlined error handling and consolidated usePost logic for prefetch handling --- src/components/PatientEntry/patient-entry.jsx | 21 +- .../PatientSelect/patient-select.jsx | 8 +- src/reducers/patient-reducers.js | 7 +- .../all-patient-retrieval.js | 19 +- src/retrieve-data-helpers/service-exchange.js | 385 +++++++++--------- 5 files changed, 212 insertions(+), 228 deletions(-) diff --git a/src/components/PatientEntry/patient-entry.jsx b/src/components/PatientEntry/patient-entry.jsx index 1e2f17b..133721f 100644 --- a/src/components/PatientEntry/patient-entry.jsx +++ b/src/components/PatientEntry/patient-entry.jsx @@ -90,11 +90,10 @@ export class PatientEntry extends Component { try { const data = await retrieveAllPatientIds(); const patients = []; - data.forEach((patient) => patients.push({ value: patient.id, label: patient.name + ', ' + patient.dob })); - this.setState({ patients: patients }); + data.forEach((patient) => patients.push({ value: patient.id, label: `${patient.name}, ${patient.dob}` })); + this.setState({ patients }); } catch (error) { this.setState({ shouldDisplayError: true, errorMessage: 'Error fetching patients from FHIR Server' }); - return; } } @@ -160,14 +159,14 @@ export class PatientEntry extends Component { onClose={this.props.isEntryRequired ? null : this.handleCloseModal} > diff --git a/src/components/PatientSelect/patient-select.jsx b/src/components/PatientSelect/patient-select.jsx index 79936b2..2ca686c 100644 --- a/src/components/PatientSelect/patient-select.jsx +++ b/src/components/PatientSelect/patient-select.jsx @@ -31,14 +31,10 @@ const propTypes = { * If the value in the Input component changes (i.e user selects option), pass in a function callback to handle the text */ inputOnChange: PropTypes.func.isRequired, - /** - * The name attribute for the Input component - */ - inputName: PropTypes.string, /** * A list of the Patient identifiers that populate the select options */ - patients: PropTypes.array.isRequired + patients: PropTypes.instanceOf(Array).isRequired, }; /** @@ -53,7 +49,7 @@ const propTypes = { */ const PatientSelect = ({ currentFhirServer, formFieldLabel, shouldDisplayError, - errorMessage, placeholderText, inputOnChange, inputName, + errorMessage, placeholderText, inputOnChange, patients, }) => { let fhirServerDisplay; diff --git a/src/reducers/patient-reducers.js b/src/reducers/patient-reducers.js index bf81aaa..fdbf7d7 100644 --- a/src/reducers/patient-reducers.js +++ b/src/reducers/patient-reducers.js @@ -19,8 +19,11 @@ const patientReducers = (state = initialState, action) => { // Store Patient resource from successful connection to patient in context from FHIR server case types.GET_PATIENT_SUCCESS: { const { patient } = action; - const familyName = (Array.isArray(patient.name[0].family)) ? patient.name[0].family.join(' ') : patient.name[0].family; - const fullName = `${patient.name[0].given.join(' ')} ${familyName}`; + let fullName = 'Unknown'; + if (Array.isArray(patient.name)) { + const familyName = (Array.isArray(patient.name[0].family)) ? patient.name[0].family.join(' ') : patient.name[0].family; + fullName = `${patient.name[0].given.join(' ')} ${familyName}`; + } const newPatient = { id: patient.id, name: fullName, diff --git a/src/retrieve-data-helpers/all-patient-retrieval.js b/src/retrieve-data-helpers/all-patient-retrieval.js index 3d6df5f..8871278 100644 --- a/src/retrieve-data-helpers/all-patient-retrieval.js +++ b/src/retrieve-data-helpers/all-patient-retrieval.js @@ -21,18 +21,19 @@ function retrieveAllPatientIds() { }).then((result) => { if (result.data && result.data.resourceType === 'Bundle' && Array.isArray(result.data.entry) && result.data.entry.length) { - for (const patient of result.data.entry) { - let patientInfo = {id: '', name: 'Unknown', dob: ''}; - patientInfo.id = patient.resource.id; - const familyName = (Array.isArray(patient.resource.name[0].family)) ? patient.resource.name[0].family.join(' ') : patient.resource.name[0].family; + result.data.entry.forEach((patient) => { + const patientInfo = { id: '', name: 'Unknown', dob: '' }; + patientInfo.id = patient.resource.id; + if (Array.isArray(patient.resource.name)) { + const familyName = Array.isArray(patient.resource.name[0].family) ? patient.resource.name[0].family.join(' ') : patient.resource.name[0].family; patientInfo.name = `${patient.resource.name[0].given.join(' ')} ${familyName}`; - patientInfo.dob = patient.resource.birthDate; - patientInfoList.push(patientInfo); } - return resolve(patientInfoList); - } else { - return reject(); + patientInfo.dob = patient.resource.birthDate; + patientInfoList.push(patientInfo); + }); + return resolve(patientInfoList); } + return reject(); }).catch((err) => { console.error('Could not retrieve patients from current FHIR server', err); return reject(err); diff --git a/src/retrieve-data-helpers/service-exchange.js b/src/retrieve-data-helpers/service-exchange.js index e8b4e44..75e4de1 100644 --- a/src/retrieve-data-helpers/service-exchange.js +++ b/src/retrieve-data-helpers/service-exchange.js @@ -2,30 +2,30 @@ import axios from 'axios'; import queryString from 'query-string'; import retrieveLaunchContext from './launch-context-retrieval'; import { - storeExchange, - storeLaunchContext, + storeExchange, + storeLaunchContext, } from '../actions/service-exchange-actions'; -import {productionClientId, allScopes} from '../config/fhir-config'; +import { productionClientId, allScopes } from '../config/fhir-config'; import generateJWT from './jwt-generator'; const uuidv4 = require('uuid/v4'); const remapSmartLinks = ({ - dispatch, - cardResponse, - fhirAccessToken, - patientId, - fhirServerUrl, - }) => { - ((cardResponse && cardResponse.cards) || []) - .flatMap((card) => card.links || []) - .filter(({type}) => type === 'smart') - .forEach((link) => retrieveLaunchContext( - link, - fhirAccessToken, - patientId, - fhirServerUrl, - ).catch((e) => e).then((newLink) => dispatch(storeLaunchContext(newLink)))); + dispatch, + cardResponse, + fhirAccessToken, + patientId, + fhirServerUrl, +}) => { + ((cardResponse && cardResponse.cards) || []) + .flatMap((card) => card.links || []) + .filter(({ type }) => type === 'smart') + .forEach((link) => retrieveLaunchContext( + link, + fhirAccessToken, + patientId, + fhirServerUrl, + ).catch((e) => e).then((newLink) => dispatch(storeLaunchContext(newLink)))); }; /** @@ -34,17 +34,17 @@ const remapSmartLinks = ({ * @returns {*} - String URL with its query parameters URI encoded if necessary */ function encodeUriParameters(template) { - if (template && template.split('?').length > 1) { - const splitUrl = template.split('?'); - const queryParams = queryString.parse(splitUrl[1]); - Object.keys(queryParams).forEach((param) => { - const val = queryParams[param]; - queryParams[param] = encodeURIComponent(val); - }); - splitUrl[1] = queryString.stringify(queryParams, {encode: false}); - return splitUrl.join('?'); - } - return template; + if (template && template.split('?').length > 1) { + const splitUrl = template.split('?'); + const queryParams = queryString.parse(splitUrl[1]); + Object.keys(queryParams).forEach((param) => { + const val = queryParams[param]; + queryParams[param] = encodeURIComponent(val); + }); + splitUrl[1] = queryString.stringify(queryParams, { encode: false }); + return splitUrl.join('?'); + } + return template; } /** @@ -53,20 +53,20 @@ function encodeUriParameters(template) { * @returns {*} - New prefetch key/value pair Object with prefetch template filled out */ function completePrefetchTemplate(state, prefetch) { - const patient = state.patientState.currentPatient.id; - const user = state.patientState.currentUser || state.patientState.defaultUser; - const prefetchRequests = {...prefetch}; - Object.keys(prefetchRequests).forEach((prefetchKey) => { - let prefetchTemplate = prefetchRequests[prefetchKey]; - prefetchTemplate = prefetchTemplate.replace( - /{{\s*context\.patientId\s*}}/g, - patient, - ); - prefetchTemplate = prefetchTemplate.replace(/{{\s*user\s*}}/g, user); - prefetchTemplate = prefetchTemplate.replace(/{{\s*context\.userId\s*}}/g, user); - prefetchRequests[prefetchKey] = encodeUriParameters(prefetchTemplate); - }); - return prefetchRequests; + const patient = state.patientState.currentPatient.id; + const user = state.patientState.currentUser || state.patientState.defaultUser; + const prefetchRequests = { ...prefetch }; + Object.keys(prefetchRequests).forEach((prefetchKey) => { + let prefetchTemplate = prefetchRequests[prefetchKey]; + prefetchTemplate = prefetchTemplate.replace( + /{{\s*context\.patientId\s*}}/g, + patient, + ); + prefetchTemplate = prefetchTemplate.replace(/{{\s*user\s*}}/g, user); + prefetchTemplate = prefetchTemplate.replace(/{{\s*context\.userId\s*}}/g, user); + prefetchRequests[prefetchKey] = encodeUriParameters(prefetchTemplate); + }); + return prefetchRequests; } /** @@ -75,81 +75,66 @@ function completePrefetchTemplate(state, prefetch) { * @param prefetch - Prefetch templates from a CDS Service definition filled out * @returns {Promise} - Promise object to eventually fetch data */ -function prefetchDataPromises(state, baseUrl, prefetch) { - const resultingPrefetch = {}; - const prefetchRequests = { - - ...completePrefetchTemplate(state, prefetch), - }; - return new Promise((resolve) => { - const prefetchKeys = Object.keys(prefetchRequests); - const headers = {Accept: 'application/json+fhir'}; - const accessTokenProperty = state.fhirServerState.accessToken; - if (accessTokenProperty && accessTokenProperty.access_token) { - headers.Authorization = `Bearer ${accessTokenProperty.access_token}`; +async function prefetchDataPromises(state, baseUrl, prefetch) { + const resultingPrefetch = {}; + const prefetchRequests = { + ...completePrefetchTemplate(state, prefetch), + }; + + const prefetchKeys = Object.keys(prefetchRequests); + const headers = { Accept: 'application/json+fhir' }; + const { accessToken } = state.fhirServerState; + if (accessToken && accessToken.access_token) { + headers.Authorization = `Bearer ${accessToken.access_token}`; + } + + // Create an array of promises for each request + const promises = prefetchKeys.map(async (key) => { + const prefetchValue = prefetchRequests[key]; + const resource = prefetchValue.split('?')[0]; + const params = new URLSearchParams(prefetchValue.split('?')[1]); + let usePost = true; + + try { + if (usePost) { + const result = await axios({ + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + data: params.toString(), + url: `${baseUrl}/${resource}/_search`, + }); + if (result.data && Object.keys(result.data).length) { + resultingPrefetch[key] = result.data; } - // Keep count of resolved promises and invoke final resolve when we have them all. NOTE: This can also be - // implemented with Promise.all(), but since we want to swallow errors, using Promise.all() ends up being - // more complicated. - let numDone = 0; - const resolveWhenDone = () => { - numDone += 1; - if (numDone === prefetchKeys.length) { - resolve(resultingPrefetch); - } - }; - for (let i = 0; i < prefetchKeys.length; i += 1) { - const key = prefetchKeys[i]; - const prefetchValue = prefetchRequests[key]; - let usePost = true; - if (usePost) { - const resource = prefetchValue.split('?')[0]; // TODO: investigate edge cases - const params = new URLSearchParams(prefetchValue.split('?')[1]); - axios({ - method: 'POST', - headers: {'content-type': 'application/x-www-form-urlencoded'}, - data: params.toString(), - url: `${baseUrl}/${resource}/_search` - }) - .then((result) => { - if (result.data && Object.keys(result.data).length) { - resultingPrefetch[key] = result.data; - } - resolveWhenDone(); - }) - .catch((err) => { - usePost = false; - // Since prefetch is best-effort, don't throw; just log it and continue - console.log( - `Unable to prefetch data using POST for ${baseUrl}/${prefetchValue}`, - err, - ); - resolveWhenDone(); - }); - } - if (!usePost) { - axios({ - method: 'get', - url: `${baseUrl}/${prefetchValue}`, - headers, - }) - .then((result) => { - if (result.data && Object.keys(result.data).length) { - resultingPrefetch[key] = result.data; - } - resolveWhenDone(); - }) - .catch((err) => { - // Since prefetch is best-effort, don't throw; just log it and continue - console.log( - `Unable to prefetch data for ${baseUrl}/${prefetchValue}`, - err, - ); - resolveWhenDone(); - }); - } + } + } catch (err) { + usePost = false; + console.log( + `Unable to prefetch data using POST for ${baseUrl}/${prefetchValue}`, + err, + ); + } + + if (!usePost) { + try { + const result = await axios({ + method: 'GET', + url: `${baseUrl}/${prefetchValue}`, + headers, + }); + if (result.data && Object.keys(result.data).length) { + resultingPrefetch[key] = result.data; } - }); + } catch (err) { + console.log(`Unable to prefetch data for ${baseUrl}/${prefetchValue}`, err); + } + } + }); + + // Wait for all promises to complete + await Promise.all(promises); + + return resultingPrefetch; } /** @@ -161,99 +146,99 @@ function prefetchDataPromises(state, baseUrl, prefetch) { * @returns {Promise} - Promise object to eventually return service response data */ function callServices(dispatch, state, url, context, exchangeRound = 0) { - const hook = state.hookState.currentHook; - const fhirServer = state.fhirServerState.currentFhirServer; - - const activityContext = {}; - activityContext.patientId = state.patientState.currentPatient.id; - activityContext.userId = state.patientState.currentUser || state.patientState.defaultUser; - - if (context && context.length) { - context.forEach((contextKey) => { - activityContext[contextKey.key] = contextKey.value; - }); - } + const hook = state.hookState.currentHook; + const fhirServer = state.fhirServerState.currentFhirServer; - const hookInstance = uuidv4(); - const accessTokenProperty = state.fhirServerState.accessToken; - let fhirAuthorization; - if (accessTokenProperty) { - fhirAuthorization = { - access_token: accessTokenProperty.access_token, - token_type: 'Bearer', - expires_in: accessTokenProperty.expires_in, - scope: allScopes, - subject: productionClientId, - }; - } - const request = { - hookInstance, - hook, - fhirServer, - context: activityContext, - }; + const activityContext = {}; + activityContext.patientId = state.patientState.currentPatient.id; + activityContext.userId = state.patientState.currentUser || state.patientState.defaultUser; - if (fhirAuthorization) { - request.fhirAuthorization = fhirAuthorization; - } - - const serviceDefinition = state.cdsServicesState.configuredServices[url]; - - const sendRequest = () => axios({ - method: 'post', - url, - data: request, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${generateJWT(url)}`, - }, + if (context && context.length) { + context.forEach((contextKey) => { + activityContext[contextKey.key] = contextKey.value; }); - - const dispatchResult = (result) => { - if (result.data && Object.keys(result.data).length) { - dispatch(storeExchange(url, request, result.data, result.status, exchangeRound)); - remapSmartLinks({ - dispatch, - cardResponse: result.data, - fhirAccessToken: state.fhirServerState.accessToken, - patientId: state.patientState.currentPatient.id, - fhirServerUrl: state.fhirServerState.currentFhirServer, - }); - } else { - dispatch(storeExchange( - url, - request, - 'No response returned. Check developer tools for more details.', - )); - } + } + + const hookInstance = uuidv4(); + const accessTokenProperty = state.fhirServerState.accessToken; + let fhirAuthorization; + if (accessTokenProperty) { + fhirAuthorization = { + access_token: accessTokenProperty.access_token, + token_type: 'Bearer', + expires_in: accessTokenProperty.expires_in, + scope: allScopes, + subject: productionClientId, }; - - const dispatchErrors = (err) => { - console.error(`Could not POST data to CDS Service ${url}`, err); - dispatch(storeExchange( - url, - request, - 'Could not get a response from the CDS Service. ' + } + const request = { + hookInstance, + hook, + fhirServer, + context: activityContext, + }; + + if (fhirAuthorization) { + request.fhirAuthorization = fhirAuthorization; + } + + const serviceDefinition = state.cdsServicesState.configuredServices[url]; + + const sendRequest = () => axios({ + method: 'post', + url, + data: request, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${generateJWT(url)}`, + }, + }); + + const dispatchResult = (result) => { + if (result.data && Object.keys(result.data).length) { + dispatch(storeExchange(url, request, result.data, result.status, exchangeRound)); + remapSmartLinks({ + dispatch, + cardResponse: result.data, + fhirAccessToken: state.fhirServerState.accessToken, + patientId: state.patientState.currentPatient.id, + fhirServerUrl: state.fhirServerState.currentFhirServer, + }); + } else { + dispatch(storeExchange( + url, + request, + 'No response returned. Check developer tools for more details.', + )); + } + }; + + const dispatchErrors = (err) => { + console.error(`Could not POST data to CDS Service ${url}`, err); + dispatch(storeExchange( + url, + request, + 'Could not get a response from the CDS Service. ' + 'See developer tools for more details', - )); - }; + )); + }; - // Wait for prefetch to be fulfilled before making a request to the CDS service, if the service has prefetch expectations - const needPrefetch = serviceDefinition.prefetch + // Wait for prefetch to be fulfilled before making a request to the CDS service, if the service has prefetch expectations + const needPrefetch = serviceDefinition.prefetch && Object.keys(serviceDefinition.prefetch).length > 0; - const prefetchPromise = needPrefetch - ? prefetchDataPromises(state, fhirServer, serviceDefinition.prefetch) - : Promise.resolve({}); + const prefetchPromise = needPrefetch + ? prefetchDataPromises(state, fhirServer, serviceDefinition.prefetch) + : Promise.resolve({}); - return prefetchPromise.then((prefetchResults) => { - if (prefetchResults && Object.keys(prefetchResults).length > 0) { - request.prefetch = prefetchResults; - } - return sendRequest() - .then(dispatchResult) - .catch(dispatchErrors); - }); + return prefetchPromise.then((prefetchResults) => { + if (prefetchResults && Object.keys(prefetchResults).length > 0) { + request.prefetch = prefetchResults; + } + return sendRequest() + .then(dispatchResult) + .catch(dispatchErrors); + }); } export default callServices;