Skip to content

Commit

Permalink
Merge pull request #39 from jembi/retain-source-id
Browse files Browse the repository at this point in the history
Allow source ID ot be saved as FHIR logical ID, map between source id and interaction id
  • Loading branch information
bradsawadye authored May 2, 2024
2 parents 375665d + 7eea2fb commit 715186d
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 33 deletions.
74 changes: 73 additions & 1 deletion src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import { matchAsyncHandler } from './handlers/matchPatientAsync';
import { matchSyncHandler } from './handlers/matchPatientSync';
import { mpiMdmQueryLinksMiddleware } from '../middlewares/mpi-mdm-query-links';
import { validationMiddleware } from '../middlewares/validation';
import { buildOpenhimResponseObject } from '../utils/utils';
import { buildOpenhimResponseObject, getData } from '../utils/utils';
import { fetchEverythingByRef } from './handlers/fetchPatientResources';
import { mpiMdmSummaryMiddleware } from '../middlewares/mpi-mdm-summary';
import { fetchPatientSummaryByRef } from './handlers/fetchPatientSummaries';
import { getConfig } from '../config/config';
import { Patient } from 'fhir/r3';
import { getMpiAuthToken } from '../utils/mpi';
import logger from '../logger';

const routes = express.Router();

Expand Down Expand Up @@ -61,6 +64,75 @@ routes.post(
mpiAccessProxyMiddleware
);

// swap source ID for interaction ID
routes.get('/fhir/Patient/:patientId', async (req, res) => {
const requestedId = req.params.patientId;

logger.debug(`Fetching patient ${requestedId} from FHIR store`);

const {
fhirDatastoreProtocol: fhirProtocol,
fhirDatastoreHost: fhirHost,
fhirDatastorePort: fhirPort,
mpiProtocol: mpiProtocol,
mpiHost: mpiHost,
mpiPort: mpiPort,
mpiAuthEnabled,
} = getConfig();
const fhirResponse = await getData(
fhirProtocol,
fhirHost,
fhirPort,
`/fhir/Patient/${requestedId}`,
{}
);

let upstreamId = requestedId;

if (fhirResponse.status === 200) {
const patient = fhirResponse.body as Patient;
const interactionId =
patient.link && patient.link[0]?.other.reference?.match(/Patient\/([^/]+)/)?.[1];

if (interactionId) {
upstreamId = interactionId;
logger.debug(`Swapping source ID ${requestedId} for interaction ID ${upstreamId}`);
}
}

logger.debug(`Fetching patient ${upstreamId} from MPI`);

const headers: HeadersInit = {
'Content-Type': 'application/fhir+json',
};

if (mpiAuthEnabled) {
const token = await getMpiAuthToken();

headers['Authorization'] = `Bearer ${token.accessToken}`;
}

const mpiResponse = await getData(
mpiProtocol,
mpiHost,
mpiPort,
`/fhir/links/Patient/${upstreamId}`,
{}
);

// Map the upstreamId to the requestedId
if (mpiResponse.status === 200) {
const patient = mpiResponse.body as Patient;

patient.id = requestedId;
logger.debug(
`Mapped upstream ID ${upstreamId} to requested ID ${requestedId} in response body`
);
}

res.status(mpiResponse.status).send(mpiResponse.body);
});

routes.get(
'/fhir/Patient/:patientId/\\$everything',
mpiMdmEverythingMiddleware,
Expand Down
84 changes: 68 additions & 16 deletions src/utils/mpi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Patient, Resource } from 'fhir/r3';
import { getConfig } from '../config/config';
import { getData, isHttpStatusOk } from './utils';
import { createNewPatientRef, getData, isHttpStatusOk } from './utils';
import { ClientOAuth2, OAuth2Token } from './client-oauth2';

// Singleton instance of MPI Token stored in memory
Expand Down Expand Up @@ -60,24 +60,76 @@ export const fetchMpiResourceByRef = async <T extends Resource>(
return isHttpStatusOk(response.status) ? (response.body as T) : undefined;
};

/**
* Recursively fetch linked patient refs from the MPI
*/
export const fetchMpiPatientLinks = async (patientRef: string, patientLinks: string[]) => {
patientLinks.push(patientRef);

const patient = await fetchMpiResourceByRef<Patient>(patientRef);
const patientLinksSet: Set<string> = new Set();
patientLinksSet.add(patientRef);

const {
mpiProtocol: protocol,
mpiHost: host,
mpiPort: port,
mpiAuthEnabled,
fhirDatastoreHost,
fhirDatastorePort,
fhirDatastoreProtocol,
} = getConfig();
const headers: HeadersInit = {
'Content-Type': 'application/fhir+json',
};

if (patient?.link) {
const linkedRefs = patient.link.map(({ other }) => other.reference);
const refsToFetch = linkedRefs.filter((ref) => {
return ref && !patientLinks.includes(ref);
}) as string[];
if (mpiAuthEnabled) {
const token = await getMpiAuthToken();

if (refsToFetch.length > 0) {
const promises = refsToFetch.map((ref) => fetchMpiPatientLinks(ref, patientLinks));
headers['Authorization'] = `Bearer ${token.accessToken}`;
}

await Promise.all(promises);
}
const guttedPatient = await getData(
fhirDatastoreProtocol,
fhirDatastoreHost,
fhirDatastorePort,
`/fhir/${patientRef}`,
headers
);

// Fetch patient links from MPI
let mpiPatient = await getData(
protocol,
host,
port,
`/fhir/links/Patient/${Object.assign(guttedPatient.body)
.link[0].other.reference.split('/')
.pop()}`,
headers
);

// Cater for SanteMpi client registry
if (!isHttpStatusOk(mpiPatient.status)) {
mpiPatient = await getData(
protocol,
host,
port,
`/fhir/Patient/${Object.assign(guttedPatient.body)
.link[0].other.reference.split('/')
.pop()}`,
headers
);
}

const links: string[] = Object.assign(mpiPatient.body).link.map(
(element: { other: { reference: string } }) =>
createNewPatientRef(element.other.reference.split('/').pop() || '')
);

const guttedPatients = await getData(
fhirDatastoreProtocol,
fhirDatastoreHost,
fhirDatastorePort,
`/fhir/Patient?link=${encodeURIComponent(links.join(','))}`,
headers
);

Object.assign(guttedPatients.body).entry?.forEach((patient: { fullUrl: string }) => {
patientLinksSet.add(patient.fullUrl);
});
patientLinks.push(...patientLinksSet);
};
3 changes: 2 additions & 1 deletion src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export const modifyBundle = (
fullUrl: entry.fullUrl,
resource: {
resourceType: 'Patient',
id: entry.resource.id,
link: [
{
other: {
Expand All @@ -174,7 +175,7 @@ export const modifyBundle = (
},
request: {
method: 'PUT',
url: `Patient/${newPatientId}`,
url: `Patient/${entry.resource.id}`,
},
};

Expand Down
3 changes: 2 additions & 1 deletion tests/cucumber/features/fhirAccessProxy.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ Feature: FHIR Access Proxy

Scenario: Valid $everything Request
Given MPI and FHIR services are up and running
When an $everything search request is sent
When there is data
And an $everything search request is sent
Then a successful response containing a bundle of related patient resources is sent back

Scenario: Valid $everything Request without MDM
Expand Down
14 changes: 12 additions & 2 deletions tests/cucumber/step-definitions/fhirAccessProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,19 @@ Given('MPI and FHIR services are up and running', async (): Promise<void> => {
request = supertest(server);
});

When('there is data', async (): Promise<void> => {
const response = await request
.post('/fhir')
.send(bundle)
.set('content-type', 'application/fhir+json')
.expect(200);

responseBody = response.body;
});

When('an $everything search request is sent', async (): Promise<void> => {
const response = await request
.get('/fhir/Patient/1/$everything?_mdm=true')
.get('/fhir/Patient/testPatient/$everything?_mdm=true')
.set('Content-Type', 'application/fhir+json')
.expect(200);
responseBody = response.body;
Expand Down Expand Up @@ -77,7 +87,7 @@ Then('a successful response containing a bundle is sent back', (): void => {

When('an MDM search request is sent', async (): Promise<void> => {
const response = await request
.get('/fhir/Observation?subject:mdm=Patient/1')
.get('/fhir/Observation?subject:mdm=Patient/testPatient')
.set('Content-Type', 'application/fhir+json')
.expect(200);

Expand Down
14 changes: 13 additions & 1 deletion tests/cucumber/step-definitions/patientSyncMatching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,20 @@ Then('a patient should be created on the client registry', async (): Promise<voi

const patientId = response.location.split('/')[1];

const fhirPatient = await fetch(
`${config.fhirDatastoreProtocol}://${config.fhirDatastoreHost}:${config.fhirDatastorePort}/fhir/Patient/${patientId}`,
{
headers: {
Authorization: `Bearer ${auth.accessToken}`,
},
method: 'GET',
}
);

const jsonPatient = await fhirPatient.json()

const res = await fetch(
`${config.mpiProtocol}://${config.mpiHost}:${config.mpiPort}/fhir/Patient/${patientId}`,
jsonPatient.link[0].other.reference,
{
headers: {
Authorization: `Bearer ${auth.accessToken}`,
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/matchPatientSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,9 +343,10 @@ describe('Match Patient Synchronously', (): void => {
fullUrl: 'Patient/12333',
request: {
method: 'PUT',
url: 'Patient/testPatient',
url: 'Patient/12333',
},
resource: {
id: '12333',
link: [
{
other: {
Expand Down
21 changes: 16 additions & 5 deletions tests/unit/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { mpiMdmQueryLinksMiddleware } from '../../src/middlewares/mpi-mdm-query-
import { validationMiddleware } from '../../src/middlewares/validation';
import { mpiAuthMiddleware } from '../../src/middlewares/mpi-auth';
import { mpiMdmSummaryMiddleware } from '../../src/middlewares/mpi-mdm-summary';
import { createNewPatientRef } from '../../src/utils/utils';

const config = getConfig();

Expand All @@ -31,7 +32,7 @@ const patientFhirResource1: Patient = {
link: [
{
other: {
reference: 'Patient/2',
reference: 'Patient/0x4',
},
type: 'refer',
},
Expand All @@ -44,7 +45,7 @@ const patientFhirResource2: Patient = {
link: [
{
other: {
reference: 'Patient/1',
reference: 'Patient/0x7',
},
type: 'seealso',
},
Expand Down Expand Up @@ -324,8 +325,11 @@ describe('Middlewares', (): void => {

it('should perform MDM expansion when mdm param is supplied', async () => {
nock(mpiUrl).persist().post('/auth/oauth2_token').reply(200, newOauth2TokenGenerated);
nock(mpiUrl).get('/fhir/Patient/1').reply(200, patientFhirResource1);
nock(mpiUrl).get('/fhir/links/Patient/0x4').reply(200, {...patientFhirResource1, link: [{other: {reference: 'Patient/0x4'}}, {other: {reference: 'Patient/0x7'}}]});
nock(mpiUrl).get('/fhir/Patient/2').reply(200, patientFhirResource2);
const links = encodeURIComponent([createNewPatientRef('0x4'),createNewPatientRef('0x7')].join(','))
nock(fhirDatastoreUrl).get(`/fhir/Patient?link=${links}`).reply(200, {entry: [{fullUrl: 'Patient/1'}, {fullUrl: 'Patient/2'}]});
nock(fhirDatastoreUrl).get('/fhir/Patient/1').reply(200, patientFhirResource1);
nock(fhirDatastoreUrl)
.get(`/fhir/Encounter?subject=${encodeURIComponent('Patient/1,Patient/2')}`)
.reply(200, Encounters);
Expand Down Expand Up @@ -383,8 +387,11 @@ describe('Middlewares', (): void => {

it('should preform MDM expansion when mdm param is supplied', async () => {
nock(mpiUrl).persist().post('/auth/oauth2_token').reply(200, newOauth2TokenGenerated);
nock(mpiUrl).get('/fhir/Patient/1').reply(200, patientFhirResource1);
nock(mpiUrl).get('/fhir/links/Patient/0x4').reply(200, {...patientFhirResource1, link: [{other: {reference: 'Patient/0x4'}}, {other: {reference: 'Patient/0x7'}}]});
nock(mpiUrl).get('/fhir/Patient/2').reply(200, patientFhirResource2);
const links = encodeURIComponent([createNewPatientRef('0x4'),createNewPatientRef('0x7')].join(','))
nock(fhirDatastoreUrl).get(`/fhir/Patient?link=${links}`).reply(200, {entry: [{fullUrl: 'Patient/1'}, {fullUrl: 'Patient/2'}]});
nock(fhirDatastoreUrl).get('/fhir/Patient/1').reply(200, patientFhirResource1);
nock(fhirDatastoreUrl)
.get(`/fhir/${patientRefSummary1}/$summary`)
.reply(200, patientSummary1);
Expand Down Expand Up @@ -435,8 +442,12 @@ describe('Middlewares', (): void => {

it('should perform MDM expansion when mdm param is supplied', async () => {
nock(mpiUrl).persist().post('/auth/oauth2_token').reply(200, {});
nock(mpiUrl).get('/fhir/Patient/1').reply(200, patientFhirResource1);
nock(mpiUrl).get('/fhir/links/Patient/0x4').reply(200, {...patientFhirResource1, link: [{other: {reference: 'Patient/0x4'}}, {other: {reference: 'Patient/0x7'}}]});
nock(mpiUrl).get('/fhir/Patient/2').reply(200, patientFhirResource2);
const links = encodeURIComponent([createNewPatientRef('0x4'),createNewPatientRef('0x7')].join(','))
nock(fhirDatastoreUrl).get(`/fhir/Patient?link=${links}`).reply(200, {entry: [{fullUrl: 'Patient/1'}, {fullUrl: 'Patient/2'}]});
nock(fhirDatastoreUrl).get('/fhir/Patient/1').reply(200, patientFhirResource1);

const request = {
body: {},
headers: {},
Expand Down
15 changes: 12 additions & 3 deletions tests/unit/mpi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import {
fetchMpiResourceByRef,
fetchMpiPatientLinks,
} from '../../src/utils/mpi';
import { createNewPatientRef } from '../../src/utils/utils';

const config = getConfig();

const { mpiProtocol, mpiHost, mpiPort, mpiClientId, mpiClientSecret } = config;

const mpiUrl = `${mpiProtocol}://${mpiHost}:${mpiPort}`;

const { fhirDatastoreProtocol, fhirDatastoreHost, fhirDatastorePort } = config;
const fhirDatastoreUrl = `${fhirDatastoreProtocol}://${fhirDatastoreHost}:${fhirDatastorePort}`;

const newOauth2TokenGenerated = {
token_type: 'bearer',
access_token: 'accessToken',
Expand Down Expand Up @@ -51,7 +55,7 @@ const patientFhirResource1: Patient = {
link: [
{
other: {
reference: 'Patient/2',
reference: 'Patient/0x4',
},
type: 'refer',
},
Expand All @@ -64,7 +68,7 @@ const patientFhirResource2: Patient = {
link: [
{
other: {
reference: 'Patient/1',
reference: 'Patient/0x7',
},
type: 'seealso',
},
Expand Down Expand Up @@ -140,8 +144,13 @@ describe('MPI', (): void => {

describe('*fetchMpiPatientLinks', async (): Promise<void> => {
it('should fetch patient links from MPI fhir', async (): Promise<void> => {
nock(mpiUrl).get('/fhir/Patient/1').reply(200, patientFhirResource1);
nock(mpiUrl).persist().post('/auth/oauth2_token').reply(200, newOauth2TokenGenerated);
nock(mpiUrl).get('/fhir/links/Patient/0x4').reply(200, {...patientFhirResource1, link: [{other: {reference: 'Patient/0x4'}}, {other: {reference: 'Patient/0x7'}}]});
nock(mpiUrl).get('/fhir/Patient/2').reply(200, patientFhirResource2);
const links = encodeURIComponent([createNewPatientRef('0x4'),createNewPatientRef('0x7')].join(','))
nock(fhirDatastoreUrl).get(`/fhir/Patient?link=${links}`).reply(200, {entry: [{fullUrl: 'Patient/1'}, {fullUrl: 'Patient/2'}]});
nock(fhirDatastoreUrl).get('/fhir/Patient/1').reply(200, patientFhirResource1);

const refs: string[] = [];
await fetchMpiPatientLinks(`Patient/1`, refs);
expect(refs).to.deep.equal(['Patient/1', 'Patient/2']);
Expand Down
Loading

0 comments on commit 715186d

Please sign in to comment.