Skip to content

Commit

Permalink
simplify consent discovery by using chain queries to fetch consents d…
Browse files Browse the repository at this point in the history
…irectly based on patient identifiers.
  • Loading branch information
mojitoj committed Nov 30, 2024
1 parent 46d30ee commit 26cb659
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 225 deletions.
77 changes: 20 additions & 57 deletions lib/consent-discovery.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const _ = require("lodash");
const superagent = require("superagent");
const logger = require("../lib/logger");
const { maybeAddAuth } = require("../lib/auth");
Expand All @@ -13,26 +12,33 @@ const addFhirBaseToEntry = (entry, url) => ({
});

const addFhirBaseToEntries = (entries, url) =>
entries.map((entry) => addFhirBaseToEntry(entry, url));
entries ? entries.map((entry) => addFhirBaseToEntry(entry, url)) : [];

async function fetchConsents(patientIdentifiers, category) {
async function fetchConsents(patientIdentifiers) {
try {
const patientFhirIds = await patientIds(patientIdentifiers);
const consentSearchQueries = CONSENT_FHIR_SERVERS.map((fhirBase) =>
patientIdentifiers.map((patientIdentifier) =>
resolveConsents(fhirBase, patientIdentifier)
)
).flat();

const consentSearchQueries = patientFhirIds.map(({ fhirBase, patientId }) =>
resolveConsents(fhirBase, patientId, category)
);
const fhirBasePointers = CONSENT_FHIR_SERVERS.map((fhirBase) =>
patientIdentifiers.map(() => fhirBase)
).flat();

const consentSearchResults = await Promise.all(consentSearchQueries);
const resolvedConsents = consentSearchResults
.filter(({ body }) => body?.entry?.length)
.map(({ body }, index) => ({
...body,
entry: addFhirBaseToEntries(body.entry, patientFhirIds[index].fhirBase)
entry: addFhirBaseToEntries(
body.entry,
fhirBasePointers[index]
)
}))
.filter((body) => body?.entry?.length)
.map((bundle) => bundle.entry)
.flat();

return resolvedConsents;
} catch (e) {
console.log(e);
Expand All @@ -45,11 +51,12 @@ async function fetchConsents(patientIdentifiers, category) {
}
}

function resolveConsents(fhirBase, patientId, category) {
function resolveConsents(fhirBase, patientIdentifier) {
const patientParam = {
patient: `Patient/${patientId}`
"patient.identifier": patientIdentifier.system
? `${patientIdentifier.system}|${patientIdentifier.value}`
: `|${patientIdentifier.value}`
};
//todo: incorporate categoty into the search parameters

const params = { ...patientParam };
return maybeAddAuth(
Expand All @@ -60,50 +67,6 @@ function resolveConsents(fhirBase, patientId, category) {
);
}

async function patientIds(patientIdentifiers) {
const patientIdQueries = CONSENT_FHIR_SERVERS.map((fhirBase) =>
resolvePatientOnServer(fhirBase, patientIdentifiers)
);

const patientIds = await Promise.all(patientIdQueries);

return _.zipWith(CONSENT_FHIR_SERVERS, patientIds, (fhirBase, patientId) => ({
fhirBase,
patientId
})).filter(({ patientId }) => patientId);
}

async function resolvePatientOnServer(fhirBase, patientIdentifiers) {
const patientSearchQueries = patientIdentifiers.map((patientIdentifier) =>
resolvePatientOnServerByIdentifer(fhirBase, patientIdentifier)
);
const patientIdSearchResults = await Promise.all(patientSearchQueries);

const patientIds = patientIdSearchResults
.map((patientIdResult) => firstPatientId(patientIdResult))
.filter((patientId) => patientId);

return patientIds?.[0];
}

function resolvePatientOnServerByIdentifer(fhirBase, patientIdentifier) {
const query = {
identifier: patientIdentifier.system
? `${patientIdentifier.system}|${patientIdentifier.value}`
: `|${patientIdentifier.value}`
};
return maybeAddAuth(
superagent
.get(`${fhirBase}/Patient`)
.query(query)
// .query({ _summary: "true" }) not implemented by some servers.
.set({ Accept: "application/json, application/fhir+json" })
);
}

const firstPatientId = (patientSearchResult) =>
patientSearchResult.body?.entry?.[0]?.resource?.id;

module.exports = {
fetchConsents
};
75 changes: 8 additions & 67 deletions test/common/setup-mock-consent-servers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,12 @@ const CONSENT_FHIR_SERVERS = (process.env.CONSENT_FHIR_SERVERS || "")
const EMPTY_BUNDLE = require("../fixtures/empty-bundle.json");
const PATIENT = require("../fixtures/patients/patient-boris.json");

const PATIENT_RESULTS_BUNDLE = _.set(
_.set(_.clone(EMPTY_BUNDLE), "entry[0].resource", PATIENT),
"total",
1
);

const MOCK_FHIR_SERVERS = CONSENT_FHIR_SERVERS.map((fhirBase) =>
nock(fhirBase)
.defaultReplyHeaders({ "Content-Type": "application/json; charset=utf-8" })
.replyContentLength()
);

function setupMockPatient(patientId, index) {
const fhirServerIndex = index || 0;
const system = patientId.system;
const value = patientId.value;

MOCK_FHIR_SERVERS[fhirServerIndex]
.get("/Patient")
.query({ identifier: `${system}|${value}` })
.reply(200, PATIENT_RESULTS_BUNDLE);

for (var i = 0; i < MOCK_FHIR_SERVERS.length; i++) {
if (i == index) continue;

MOCK_FHIR_SERVERS[i]
.get("/Patient")
.query({ identifier: `${system}|${value}` })
.reply(200, EMPTY_BUNDLE);
}
}

function setupMockOrganization(url, organizationResource, howManyRequests) {
const numberOfTimes = howManyRequests || 1;
MOCK_FHIR_SERVERS[0]
Expand All @@ -61,42 +35,12 @@ function setupMockPractitioner(url, practitionerResource, howManyRequests) {
.reply(200, practitionerResource);
}

function setupMockConsent(category, consent, index, patientId) {
const fhirServerIndex = index || 0;
const fhirPatientId = patientId || "Patient/52";

const CONSENT_RESULTS_BUNDLE = consent
? _.set(
_.set(
_.set(
_.clone(EMPTY_BUNDLE),
"entry[0].resource",
_.set(consent, "id", "1")
),
"entry[0].fullUrl",
`${CONSENT_FHIR_SERVERS[0]}/Consent/1`
),
"total",
1
)
: EMPTY_BUNDLE;

MOCK_FHIR_SERVERS[fhirServerIndex]
.get(`/Consent?patient=${fhirPatientId}`)
.reply(200, CONSENT_RESULTS_BUNDLE);

for (var i = 0; i < MOCK_FHIR_SERVERS.length; i++) {
if (i == index) continue;

MOCK_FHIR_SERVERS[i]
.get(`/Consent?patient=${fhirPatientId}`)
.reply(200, EMPTY_BUNDLE);
}
}

function setupMockConsentNoCategory(consent, index, patientId) {
function setupMockConsent(consent, index, patientIdentifier) {
const fhirServerIndex = index || 0;
const fhirPatientId = patientId || "Patient/52";
const { system, value } = patientIdentifier || {
system: "http://hl7.org/fhir/sid/us-medicare",
value: "0000-000-0000"
};

const CONSENT_RESULTS_BUNDLE = consent
? _.set(
Expand All @@ -115,22 +59,19 @@ function setupMockConsentNoCategory(consent, index, patientId) {
: EMPTY_BUNDLE;

MOCK_FHIR_SERVERS[fhirServerIndex]
.get(`/Consent?patient=${fhirPatientId}`)
.get(`/Consent?patient.identifier=${system}|${value}`)
.reply(200, CONSENT_RESULTS_BUNDLE);

for (var i = 0; i < MOCK_FHIR_SERVERS.length; i++) {
if (i == index) continue;

if (i == fhirServerIndex) continue;
MOCK_FHIR_SERVERS[i]
.get(`/Consent?patient=${fhirPatientId}`)
.get(`/Consent?patient.identifier=${system}|${value}`)
.reply(200, EMPTY_BUNDLE);
}
}

module.exports = {
setupMockPatient,
setupMockConsent,
setupMockConsentNoCategory,
setupMockOrganization,
setupMockPractitioner,
setupMockAuditEndpoint,
Expand Down
16 changes: 0 additions & 16 deletions test/controllers/discovery.test.js

This file was deleted.

24 changes: 5 additions & 19 deletions test/controllers/patient-consent-consult.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ const Ajv = require("ajv");
const request = require("supertest");
const { app } = require("../../app");
const {
setupMockPatient,
setupMockConsent,
setupMockOrganization,
setupMockAuditEndpoint
Expand Down Expand Up @@ -121,19 +120,13 @@ it("should return 400 on bad content", async () => {

const REQUEST = require("../fixtures/request-samples/patient-consent-consult-hook-request.json");

const MOCK_PATIENT_ID = {
system: "http://hl7.org/fhir/sid/us-medicare",
value: "0000-000-0000"
};

const ORGANIZATION = require("../fixtures/organizations/org-good-health.json");

it("should return 200 and an array including a consent permit card with an OPTIN consent", async () => {
expect.assertions(2);

setupMockAuditEndpoint();
setupMockPatient(MOCK_PATIENT_ID);
setupMockConsent("patient-privacy", CONSENT_OPTIN);
setupMockConsent(CONSENT_OPTIN);
setupMockOrganization(
`/${_.get(
CONSENT_OPTIN,
Expand Down Expand Up @@ -165,8 +158,7 @@ it("should return 200 and an array including a consent deny card with an OPTIN c
expect.assertions(2);

setupMockAuditEndpoint();
setupMockPatient(MOCK_PATIENT_ID);
setupMockConsent("patient-privacy", CONSENT_OPTIN);
setupMockConsent(CONSENT_OPTIN);
setupMockOrganization(
`/${_.get(
CONSENT_OPTIN,
Expand Down Expand Up @@ -204,8 +196,7 @@ it("should return 200 and an array including a consent deny card with an OPTOUT
expect.assertions(2);

setupMockAuditEndpoint();
setupMockPatient(MOCK_PATIENT_ID);
setupMockConsent("patient-privacy", CONSENT_OPTOUT);
setupMockConsent(CONSENT_OPTOUT);
setupMockOrganization(
`/${_.get(
CONSENT_OPTOUT,
Expand Down Expand Up @@ -240,11 +231,7 @@ it("should return 200 and an array including a consent permit card with obligati

const ACTIVE_PRIVACY_CONSENT_WITH_SEC_LABEL_PROVISION = require("../fixtures/consents/r4/consent-boris-deny-restricted-label.json");

setupMockPatient(MOCK_PATIENT_ID);
setupMockConsent(
"patient-privacy",
ACTIVE_PRIVACY_CONSENT_WITH_SEC_LABEL_PROVISION
);
setupMockConsent(ACTIVE_PRIVACY_CONSENT_WITH_SEC_LABEL_PROVISION);
setupMockOrganization(
`/${_.get(
ACTIVE_PRIVACY_CONSENT_WITH_SEC_LABEL_PROVISION,
Expand Down Expand Up @@ -315,8 +302,7 @@ it("should return 200 and an array including a consent permit card with obligati
it("should return 200 and an array including a NO_CONSENT card when no consent exists", async () => {
expect.assertions(2);

setupMockPatient(MOCK_PATIENT_ID);
setupMockConsent("patient-privacy", null);
setupMockConsent(null);

const res = await request(app)
.post(HOOK_ENDPOINT)
Expand Down
11 changes: 1 addition & 10 deletions test/controllers/xacml-schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const nock = require("nock");
const request = require("supertest");
const { app } = require("../../app");
const {
setupMockPatient,
setupMockConsent,
setupMockOrganization,
setupMockAuditEndpoint
Expand Down Expand Up @@ -237,21 +236,13 @@ it("should return 400 on missing required attribtues", async () => {

it("should return a response compliant with the response schema", async () => {
const REQUEST = require("../fixtures/request-samples/xacml-request.json");
const MOCK_PATIENT_ID = {
system: "http://hl7.org/fhir/sid/us-medicare",
value: "0000-000-0000"
};
const ORGANIZATION = require("../fixtures/organizations/org-good-health.json");
const ACTIVE_PRIVACY_CONSENT_WITH_SEC_LABEL_PROVISION = require("../fixtures/consents/r4/consent-boris-deny-restricted-label.json");

expect.assertions(1);

setupMockAuditEndpoint();
setupMockPatient(MOCK_PATIENT_ID);
setupMockConsent(
"patient-privacy",
ACTIVE_PRIVACY_CONSENT_WITH_SEC_LABEL_PROVISION
);
setupMockConsent(ACTIVE_PRIVACY_CONSENT_WITH_SEC_LABEL_PROVISION);
setupMockOrganization(
`/${_.get(
ACTIVE_PRIVACY_CONSENT_WITH_SEC_LABEL_PROVISION,
Expand Down
Loading

0 comments on commit 26cb659

Please sign in to comment.