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

Commit

Permalink
feat: add Implementation Guides support (#66)
Browse files Browse the repository at this point in the history
* feat: compile Implementation Guides StructureDefinitions (#59)
* feat: add HAPI FHIR Lambda Validator (#63)

Co-authored-by: Emil Diaz <emilhdiaz@users.noreply.github.com>
  • Loading branch information
carvantes and emilhdiaz committed Mar 29, 2021
1 parent 87857d1 commit 151228c
Show file tree
Hide file tree
Showing 16 changed files with 736 additions and 24 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@
"@types/cors": "^2.8.7",
"@types/express-serve-static-core": "^4.17.2",
"ajv": "^6.11.0",
"aws-sdk": "^2.856.0",
"aws-xray-sdk": "^3.2.0",
"cors": "^2.8.5",
"errorhandler": "^1.5.1",
"express": "^4.17.1",
"fhir-works-on-aws-interface": "^8.0.0",
"fhir-works-on-aws-interface": "^8.1.0",
"flat": "^5.0.0",
"http-errors": "^1.8.0",
"lodash": "^4.17.15",
Expand All @@ -55,6 +57,7 @@
"@types/uuid": "^3.4.7",
"@typescript-eslint/eslint-plugin": "^4.11.1",
"@typescript-eslint/parser": "^4.11.1",
"aws-sdk-mock": "^5.1.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.10.0",
Expand Down
21 changes: 21 additions & 0 deletions src/AWS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import './offlineEnvVariables';
import AWSXRay from 'aws-xray-sdk';
import AWS from 'aws-sdk';

const AWSWithXray = AWSXRay.captureAWS(AWS);

const { IS_OFFLINE } = process.env;
if (IS_OFFLINE === 'true') {
AWS.config.update({
region: process.env.AWS_REGION || 'us-west-2',
accessKeyId: process.env.ACCESS_KEY,
secretAccessKey: process.env.SECRET_KEY,
});
}

export default AWSWithXray;
7 changes: 5 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import RootRoute from './router/routes/rootRoute';
import { applicationErrorMapper, httpErrorHandler, unknownErrorHandler } from './router/routes/errorHandling';
import ExportRoute from './router/routes/exportRoute';
import WellKnownUriRouteRoute from './router/routes/wellKnownUriRoute';
import { FHIRStructureDefinitionRegistry } from './registry';

const configVersionSupported: ConfigVersion = 1;

Expand All @@ -33,9 +34,11 @@ export function generateServerlessRouter(
throw new Error(`This router does not support ${fhirConfig.configVersion} version`);
}
const configHandler: ConfigHandler = new ConfigHandler(fhirConfig, supportedGenericResources);
const { fhirVersion, genericResource } = fhirConfig.profile;
const { fhirVersion, genericResource, compiledImplementationGuides } = fhirConfig.profile;
const serverUrl: string = fhirConfig.server.url;
let hasCORSEnabled: boolean = false;
const registry = new FHIRStructureDefinitionRegistry(compiledImplementationGuides);

const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(
Expand All @@ -52,7 +55,7 @@ export function generateServerlessRouter(
}

// Metadata
const metadataRoute: MetadataRoute = new MetadataRoute(fhirVersion, configHandler, hasCORSEnabled);
const metadataRoute: MetadataRoute = new MetadataRoute(fhirVersion, configHandler, registry, hasCORSEnabled);
app.use('/metadata', metadataRoute.router);

if (fhirConfig.auth.strategy.service === 'SMART-on-FHIR') {
Expand Down
56 changes: 56 additions & 0 deletions src/implementationGuides/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { StructureDefinitionImplementationGuides } from './index';

describe('StructureDefinitionImplementationGuides', () => {
describe(`compile`, async () => {
test(`valid StructureDefinition`, async () => {
const compiled = new StructureDefinitionImplementationGuides().compile([
{
resourceType: 'StructureDefinition',
id: 'CARIN-BB-Organization',
url: 'http://hl7.org/fhir/us/carin/StructureDefinition/carin-bb-organization',
version: '0.1.0',
name: 'CARINBBOrganization',
title: 'CARIN Blue Button Organization Profile',
status: 'active',
date: '2019-12-23T19:40:59+00:00',
publisher: 'CARIN Alliance',
description:
'This profile builds on the USCoreOrganization Profile. It includes additional constraints relevant for the use cases addressed by this IG.',
fhirVersion: '4.0.1',
kind: 'resource',
abstract: false,
type: 'Organization',
baseDefinition: 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-organization',
derivation: 'constraint',
},
]);

await expect(compiled).resolves.toMatchInlineSnapshot(`
Array [
Object {
"baseDefinition": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-organization",
"description": "This profile builds on the USCoreOrganization Profile. It includes additional constraints relevant for the use cases addressed by this IG.",
"name": "CARINBBOrganization",
"resourceType": "StructureDefinition",
"type": "Organization",
"url": "http://hl7.org/fhir/us/carin/StructureDefinition/carin-bb-organization",
},
]
`);
});

test(`invalid StructureDefinition`, async () => {
const compiled = new StructureDefinitionImplementationGuides().compile([
{
foo: 'bar',
},
]);
await expect(compiled).rejects.toThrowError();
});
});
});
63 changes: 63 additions & 0 deletions src/implementationGuides/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { ImplementationGuides } from 'fhir-works-on-aws-interface';

/**
* Based on the FHIR StructuredDefinition. This type only includes the fields that are required for the compile process.
* See: http://www.hl7.org/fhir/structuredefinition.html
*/
export type FhirStructureDefinition = {
resourceType: 'StructureDefinition';
url: string;
name: string;
description: string;
baseDefinition: string;
type: string;
};

/**
* This class compiles StructuredDefinitions from IG packages
*/
export class StructureDefinitionImplementationGuides implements ImplementationGuides {
/**
* Compiles the contents of an Implementation Guide into an internal representation used to build the Capability Statement
*
* @param resources - an array of FHIR resources. See: https://www.hl7.org/fhir/profiling.html
*/
// eslint-disable-next-line class-methods-use-this
async compile(resources: any[]): Promise<any> {
const validStructureDefinitions: FhirStructureDefinition[] = [];
resources.forEach(s => {
if (StructureDefinitionImplementationGuides.isFhirStructureDefinition(s)) {
validStructureDefinitions.push(s);
} else {
throw new Error(`The following input is not a StructureDefinition: ${s.type} ${s.name}`);
}
});

return validStructureDefinitions.map((structureDefinition: any) => ({
name: structureDefinition.name,
url: structureDefinition.url,
type: structureDefinition.type,
resourceType: structureDefinition.resourceType,
description: structureDefinition.description,
baseDefinition: structureDefinition.baseDefinition,
}));
}

private static isFhirStructureDefinition(x: any): x is FhirStructureDefinition {
return (
typeof x === 'object' &&
x &&
x.resourceType === 'StructureDefinition' &&
typeof x.url === 'string' &&
typeof x.name === 'string' &&
typeof x.description === 'string' &&
typeof x.baseDefinition === 'string' &&
typeof x.type === 'string'
);
}
}
12 changes: 12 additions & 0 deletions src/offlineEnvVariables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

const { IS_OFFLINE } = process.env;
// Set environment variables that are convenient when testing locally with "serverless offline"
if (IS_OFFLINE === 'true') {
// https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-configuration.html#xray-sdk-nodejs-configuration-envvars
process.env.AWS_XRAY_CONTEXT_MISSING = 'LOG_ERROR';
process.env.AWS_XRAY_LOG_LEVEL = 'silent';
}
13 changes: 13 additions & 0 deletions src/registry/ResourceCapabilityInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

export interface ResourceCapability {
type: string;
supportedProfile: string[];
}

export interface ResourceCapabilityStatement {
[resourceType: string]: ResourceCapability;
}
58 changes: 58 additions & 0 deletions src/registry/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { FHIRStructureDefinitionRegistry } from './index';

const IGCompiledStructureDefinition = [
{
name: 'C4BBPatient',
url: 'http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Patient',
type: 'Patient',
resourceType: 'StructureDefinition',
description:
'This profile builds upon the US Core Patient profile. It is used to convey information about the patient who received the services described on the claim.',
baseDefinition: 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient',
},
{
name: 'C4BBCoverage',
url: 'http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Coverage',
type: 'Coverage',
resourceType: 'StructureDefinition',
description:
'Data that reflect a payer’s coverage that was effective as of the date of service or the date of admission of the claim.',
baseDefinition: 'http://hl7.org/fhir/StructureDefinition/Coverage',
},
];

describe('FHIRStructureDefinitionRegistry', () => {
test('getCapabilities snapshot', () => {
expect(new FHIRStructureDefinitionRegistry(IGCompiledStructureDefinition).getCapabilities())
.toMatchInlineSnapshot(`
Object {
"Coverage": Object {
"supportedProfile": Array [
"http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Coverage",
],
"type": "Coverage",
},
"Patient": Object {
"supportedProfile": Array [
"http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Patient",
],
"type": "Patient",
},
}
`);
});

test('getProfiles snapshot', () => {
expect(new FHIRStructureDefinitionRegistry(IGCompiledStructureDefinition).getProfiles('Patient'))
.toMatchInlineSnapshot(`
Array [
"http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Patient",
]
`);
});
});
56 changes: 56 additions & 0 deletions src/registry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { FhirStructureDefinition } from '../implementationGuides';
import { ResourceCapabilityStatement } from './ResourceCapabilityInterface';

/**
* This class is the single authority over the supported FHIR StructuredDefinition and their definitions
*/
export class FHIRStructureDefinitionRegistry {
private readonly capabilityStatement: ResourceCapabilityStatement;

constructor(compiledImplementationGuides?: any[]) {
let compiledStructureDefinitions: FhirStructureDefinition[] = [];

if (compiledImplementationGuides !== undefined) {
compiledStructureDefinitions = [...compiledImplementationGuides];
}

this.capabilityStatement = {};

compiledStructureDefinitions.forEach(compiledStructureDefinition => {
const structuredDefinition = this.capabilityStatement[compiledStructureDefinition.type];

if (structuredDefinition) {
this.capabilityStatement[compiledStructureDefinition.type].supportedProfile.push(
compiledStructureDefinition.url,
);
} else {
this.capabilityStatement[compiledStructureDefinition.type] = {
type: compiledStructureDefinition.type,
supportedProfile: [compiledStructureDefinition.url],
};
}
});
}

/**
* Retrieve the profiles for a given resource type. Returns undefined if the parameter is not found on the registry.
* @param resourceType FHIR resource type
* @return a list of profiles
*/
getProfiles(resourceType: string): string[] {
return this.capabilityStatement[resourceType]?.supportedProfile ?? [];
}

/**
* Retrieve a subset of the CapabilityStatement with the resource definitions
* See https://www.hl7.org/fhir/capabilitystatement.html
*/
getCapabilities(): ResourceCapabilityStatement {
return this.capabilityStatement;
}
}
8 changes: 8 additions & 0 deletions src/router/metadata/cap.rest.resource.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import {
SearchCapabilities,
Resource,
} from 'fhir-works-on-aws-interface';
import { ResourceCapabilityStatement, ResourceCapability } from '../../registry/ResourceCapabilityInterface';

function makeResourceObject(
resourceType: string,
resourceOperations: any[],
updateCreate: boolean,
hasTypeSearch: boolean,
searchCapabilities?: SearchCapabilities,
resourceCapability?: ResourceCapability,
) {
const result: any = {
type: resourceType,
Expand All @@ -34,6 +36,10 @@ function makeResourceObject(
Object.assign(result, searchCapabilities);
}

if (resourceCapability) {
Object.assign(result, resourceCapability);
}

return result;
}

Expand All @@ -51,6 +57,7 @@ export function makeGenericResources(
fhirResourcesToMake: string[],
operations: TypeOperation[],
searchCapabilityStatement: SearchCapabilityStatement,
resourceCapabilityStatement: ResourceCapabilityStatement,
updateCreate: boolean,
) {
const resources: any[] = [];
Expand All @@ -66,6 +73,7 @@ export function makeGenericResources(
updateCreate,
hasTypeSearch,
searchCapabilityStatement[resourceType],
resourceCapabilityStatement[resourceType],
),
);
});
Expand Down
Loading

0 comments on commit 151228c

Please sign in to comment.