Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

Commit

Permalink
feat: add $docref implementation (#332)
Browse files Browse the repository at this point in the history
  • Loading branch information
carvantes authored May 24, 2021
1 parent 396355d commit a1d49fa
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 53 deletions.
10 changes: 10 additions & 0 deletions USING_IMPLEMENTATION_GUIDES.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,16 @@ The following code snippet displays a valid US core patient:
Input validation utilizes the resources of type `StructureDefinition`, `ValueSet`, and `CodeSystem` available in the IG package.
### Operation Definitions
Implementation Guides may contain `OperationDefinition` resources. These resources describe new operations. It is not possible to automatically generate the implementation of an operation, they must be manually implemented.
Applying an Implementation Guide will enable the operations defined in it if there is a matching implementation available in FHIR Works on AWS.
At this moment The only operation available is [$docref from US Core](http://www.hl7.org/fhir/us/core/OperationDefinition-docref.html).
Our $docref implementation has the limitation that it can only search for existing documents, it cannot generate documents on the fly.
The $docref source code can be found [here](https://github.com/awslabs/fhir-works-on-aws-routing/tree/mainline/src/operationDefinitions/USCoreDocRef) and it is a good example of how to add new operations to FHIR Works on AWS.
### Capability Statement
The server capability statement returned by `GET <API_endpoint>/metadata` is updated to reflect the above features. Specifically, the `supportedProfile` field is populated and additional search parameters have a corresponding `searchParam` entry.
Expand Down
184 changes: 182 additions & 2 deletions integration-tests/implementationGuides.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@
import axios, { AxiosInstance } from 'axios';
import waitForExpect from 'wait-for-expect';
import { cloneDeep } from 'lodash';
import { expectResourceToBePartOfSearchResults, getFhirClient, randomPatient } from './utils';
import { Chance } from 'chance';
import {
expectResourceToBeInBundle,
expectResourceToBePartOfSearchResults,
expectResourceToNotBeInBundle,
getFhirClient,
randomPatient,
waitForResourceToBeSearchable,
} from './utils';
import { CapabilityStatement } from './types';

jest.setTimeout(60 * 1000);
Expand All @@ -31,7 +39,7 @@ describe('Implementation Guides - US Core', () => {
return resourcesWithSupportedProfile;
}

test('capability statement includes search parameters and supportedProfile', async () => {
test('capability statement includes search parameters, supportedProfile, and operations', async () => {
const actualCapabilityStatement: CapabilityStatement = (await client.get('metadata')).data;

const usCorePatientSearchParams = actualCapabilityStatement.rest[0].resource
Expand Down Expand Up @@ -92,6 +100,22 @@ describe('Implementation Guides - US Core', () => {

// Check for expected supportedProfile
expect(actualResourcesWithSupportedProfile).toEqual(expectedResourcesWithSupportedProfile);

const usCoreDocumentReference = actualCapabilityStatement.rest[0].resource.find(
resource => resource.type === 'DocumentReference',
);

// Check for docref operation
expect(usCoreDocumentReference).toMatchObject({
operation: [
{
name: 'docref',
definition: 'http://hl7.org/fhir/us/core/OperationDefinition/docref',
documentation:
"This operation is used to return all the references to documents related to a patient. \n\n The operation takes the optional input parameters: \n - patient id\n - start date\n - end date\n - document type \n\n and returns a [Bundle](http://hl7.org/fhir/bundle.html) of type \"searchset\" containing [US Core DocumentReference Profiles](http://hl7.org/fhir/us/core/StructureDefinition/us-core-documentreference) for the patient. If the server has or can create documents that are related to the patient, and that are available for the given user, the server returns the DocumentReference profiles needed to support the records. The principle intended use for this operation is to provide a provider or patient with access to their available document information. \n\n This operation is *different* from a search by patient and type and date range because: \n\n 1. It is used to request a server *generate* a document based on the specified parameters. \n\n 1. If no parameters are specified, the server SHALL return a DocumentReference to the patient's most current CCD \n\n 1. If the server cannot *generate* a document based on the specified parameters, the operation will return an empty search bundle. \n\n This operation is the *same* as a FHIR RESTful search by patient,type and date range because: \n\n 1. References for *existing* documents that meet the requirements of the request SHOULD also be returned unless the client indicates they are only interested in 'on-demand' documents using the *on-demand* parameter.\n\n This server does not generate documents on-demand",
},
],
});
});

const ethnicityCode = '2148-5';
Expand Down Expand Up @@ -252,4 +276,160 @@ describe('Implementation Guides - US Core', () => {
await expectResourceToBePartOfSearchResults(client, testParams, testPatient);
}
});

describe('$docref', () => {
const basicDocumentReference = () => ({
subject: {
reference: 'Patient/lala',
},
content: [
{
attachment: {
url: '/Binary/1-note',
},
},
],
type: {
coding: [
{
system: 'http://loinc.org',
code: '34133-9',
display: 'Summary of episode note',
},
],
},
context: {
period: {
start: '2020-12-10T00:00:00Z',
end: '2021-12-20T00:00:00Z',
},
},
id: '8dc58795-be85-4786-9538-6835eb2bf7b8',
resourceType: 'DocumentReference',
status: 'current',
});
let patientRef: string;
let latestCCDADocRef: any;
let oldCCDADocRef: any;
let otherTypeDocRef: any;

beforeAll(async () => {
const chance = new Chance();
patientRef = `Patient/${chance.word({ length: 15 })}`;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
latestCCDADocRef = (
await client.post('DocumentReference', {
...basicDocumentReference(),
subject: {
reference: patientRef,
},
context: {
period: {
start: '2020-12-10T00:00:00Z',
end: '2020-12-20T00:00:00Z',
},
},
})
).data;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
oldCCDADocRef = (
await client.post('DocumentReference', {
...basicDocumentReference(),
subject: {
reference: patientRef,
},
context: {
period: {
start: '2010-12-10T00:00:00Z',
end: '2010-12-20T00:00:00Z',
},
},
})
).data;

otherTypeDocRef = (
await client.post('DocumentReference', {
...basicDocumentReference(),
subject: {
reference: patientRef,
},
type: {
coding: [
{
system: 'http://fwoa-codes.org',
code: '1111',
},
],
},
})
).data;

// wait for resource to be asynchronously written to ES
await waitForResourceToBeSearchable(client, otherTypeDocRef);
});

test('minimal params', async () => {
const docrefResponse = (await client.get('DocumentReference/$docref', { params: { patient: patientRef } }))
.data;

expectResourceToBeInBundle(latestCCDADocRef, docrefResponse);

expectResourceToNotBeInBundle(oldCCDADocRef, docrefResponse);
expectResourceToNotBeInBundle(otherTypeDocRef, docrefResponse);
});

test('date params', async () => {
const docrefResponse = (
await client.get('DocumentReference/$docref', {
params: { patient: patientRef, start: '1999-01-01', end: '2030-01-01' },
})
).data;

expectResourceToBeInBundle(latestCCDADocRef, docrefResponse);
expectResourceToBeInBundle(oldCCDADocRef, docrefResponse);

expectResourceToNotBeInBundle(otherTypeDocRef, docrefResponse);
});

test('POST document type params', async () => {
const docrefResponse = (
await client.post('DocumentReference/$docref', {
resourceType: 'Parameters',
parameter: [
{
name: 'patient',
valueId: patientRef,
},
{
name: 'codeableConcept',
valueCodeableConcept: {
coding: {
system: 'http://fwoa-codes.org',
code: '1111',
},
},
},
],
})
).data;

expectResourceToBeInBundle(otherTypeDocRef, docrefResponse);
});

test('missing required params', async () => {
await expect(() => client.get('DocumentReference/$docref')).rejects.toMatchObject({
response: { status: 400 },
});
});

test('bad extra params', async () => {
await expect(() =>
client.get('DocumentReference/$docref', { params: { patient: patientRef, someBadParam: 'someValue' } }),
).rejects.toMatchObject({
response: { status: 400 },
});
});
});
});
20 changes: 1 addition & 19 deletions integration-tests/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,17 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { AxiosInstance } from 'axios';
import waitForExpect from 'wait-for-expect';
import {
aFewMinutesAgoAsDate,
expectResourceToBePartOfSearchResults,
expectResourceToNotBePartOfSearchResults,
getFhirClient,
randomPatient,
waitForResourceToBeSearchable,
} from './utils';

jest.setTimeout(600 * 1000);

const waitForResourceToBeSearchable = async (client: AxiosInstance, resource: any) => {
return waitForExpect(
expectResourceToBePartOfSearchResults.bind(
null,
client,
{
url: resource.resourceType,
params: {
_id: resource.id,
},
},
resource,
),
20000,
3000,
);
};

describe('search', () => {
let client: AxiosInstance;
beforeAll(async () => {
Expand Down
1 change: 1 addition & 0 deletions integration-tests/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export interface Resource {
searchInclude?: string[];
searchRevInclude?: string[];
supportedProfile?: string[];
operation?: any;
}

export interface REST {
Expand Down
41 changes: 41 additions & 0 deletions integration-tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as AWS from 'aws-sdk';
import axios, { AxiosInstance } from 'axios';
import { Chance } from 'chance';
import qs from 'qs';
import waitForExpect from 'wait-for-expect';

export const getFhirClient = async (
role: 'auditor' | 'practitioner' = 'practitioner',
Expand Down Expand Up @@ -199,3 +200,43 @@ export const expectResourceToNotBePartOfSearchResults = async (
};

export const aFewMinutesAgoAsDate = () => new Date(Date.now() - 1000 * 60 * 10).toJSON();

export const expectResourceToBeInBundle = (resource: any, bundle: any) => {
expect(bundle).toMatchObject({
resourceType: 'Bundle',
entry: expect.arrayContaining([
expect.objectContaining({
resource,
}),
]),
});
};

export const expectResourceToNotBeInBundle = (resource: any, bundle: any) => {
expect(bundle).toMatchObject({
resourceType: 'Bundle',
entry: expect.not.arrayContaining([
expect.objectContaining({
resource,
}),
]),
});
};

export const waitForResourceToBeSearchable = async (client: AxiosInstance, resource: any) => {
return waitForExpect(
expectResourceToBePartOfSearchResults.bind(
null,
client,
{
url: resource.resourceType,
params: {
_id: resource.id,
},
},
resource,
),
20000,
3000,
);
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"fhir-works-on-aws-authz-rbac": "4.1.1",
"fhir-works-on-aws-interface": "9.0.0",
"fhir-works-on-aws-persistence-ddb": "3.3.4",
"fhir-works-on-aws-routing": "5.3.0",
"fhir-works-on-aws-search-es": "2.6.0",
"fhir-works-on-aws-routing": "5.4.1",
"fhir-works-on-aws-search-es": "2.6.1",
"serverless-http": "^2.3.1",
"yargs": "^16.2.0"
},
Expand Down
11 changes: 5 additions & 6 deletions scripts/compile-igs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import yargs from 'yargs';
import { join } from 'path';
import { existsSync, mkdirSync } from 'fs';
import { SearchImplementationGuides } from 'fhir-works-on-aws-search-es';
import { StructureDefinitionImplementationGuides } from 'fhir-works-on-aws-routing/lib/implementationGuides';
import { RoutingImplementationGuides } from 'fhir-works-on-aws-routing/lib/implementationGuides';
import { IGCompiler } from '../src/implementationGuides/IGCompiler';
import { COMPILED_IGS_DIRECTORY } from '../src/implementationGuides/loadCompiledIGs';

Expand Down Expand Up @@ -43,11 +43,10 @@ async function compileIGs() {
}

try {
await new IGCompiler(
SearchImplementationGuides,
new StructureDefinitionImplementationGuides(),
options,
).compileIGs(cmdArgs.igPath, cmdArgs.outputDir);
await new IGCompiler(SearchImplementationGuides, new RoutingImplementationGuides(), options).compileIGs(
cmdArgs.igPath,
cmdArgs.outputDir,
);
} catch (ex) {
console.error('Exception: ', ex.message, ex.stack);
}
Expand Down
Loading

0 comments on commit a1d49fa

Please sign in to comment.