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 operation (#86)
Browse files Browse the repository at this point in the history
* feat: add US Core $docref implementation (#78)
* feat: enable $docref based on the compiled IGs (#83)
* feat: add docref to capability statement (#85)
  • Loading branch information
carvantes committed May 21, 2021
1 parent 8f7d7f5 commit 105790f
Show file tree
Hide file tree
Showing 20 changed files with 1,219 additions and 53 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@types/cors": "^2.8.7",
"@types/express-serve-static-core": "^4.17.2",
"ajv": "^6.11.0",
"ajv-errors": "^1.0.1",
"aws-sdk": "^2.856.0",
"aws-xray-sdk": "^3.2.0",
"cors": "^2.8.5",
Expand Down
19 changes: 17 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { applicationErrorMapper, httpErrorHandler, unknownErrorHandler } from '.
import ExportRoute from './router/routes/exportRoute';
import WellKnownUriRouteRoute from './router/routes/wellKnownUriRoute';
import { FHIRStructureDefinitionRegistry } from './registry';
import { initializeOperationRegistry } from './operationDefinitions';

const configVersionSupported: ConfigVersion = 1;

Expand Down Expand Up @@ -57,6 +58,7 @@ export function generateServerlessRouter(
const serverUrl: string = fhirConfig.server.url;
let hasCORSEnabled: boolean = false;
const registry = new FHIRStructureDefinitionRegistry(compiledImplementationGuides);
const operationRegistry = initializeOperationRegistry(configHandler);

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

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

if (fhirConfig.auth.strategy.service === 'SMART-on-FHIR') {
Expand All @@ -89,7 +97,9 @@ export function generateServerlessRouter(
// AuthZ
app.use(async (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
const requestInformation = getRequestInformation(req.method, req.path);
const requestInformation =
operationRegistry.getOperation(req.method, req.path)?.requestInformation ??
getRequestInformation(req.method, req.path);
// Clean auth header (remove 'Bearer ')
req.headers.authorization = cleanAuthHeader(req.headers.authorization);
res.locals.requestContext = prepareRequestContext(req);
Expand All @@ -114,6 +124,11 @@ export function generateServerlessRouter(
app.use('/', exportRoute.router);
}

// Operations defined by OperationDefinition resources
operationRegistry.getAllRouters().forEach(router => {
app.use('/', router);
});

// Special Resources
if (fhirConfig.profile.resources) {
Object.entries(fhirConfig.profile.resources).forEach(async resourceEntry => {
Expand Down
39 changes: 39 additions & 0 deletions src/configHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { FhirConfig, FhirVersion, TypeOperation } from 'fhir-works-on-aws-interface';
import ResourceHandler from './router/handlers/resourceHandler';

export default class ConfigHandler {
readonly config: FhirConfig;
Expand Down Expand Up @@ -66,4 +67,42 @@ export default class ConfigHandler {

return resources;
}

/**
* Get a `ResourceHandler` for a given `resourceType`. The `ResourceHandler` uses the most specific dependencies available in `FhirConfig`:
* 1. Use the dependencies specific to the given `resourceType` if they are defined.
* 2. Otherwise use the dependencies for `genericResource` if the given `resourceType` is a valid `genericResource`.
* 3. Otherwise return undefined.
*/
getResourceHandler(resourceType: string): ResourceHandler | undefined {
if (this.config.profile.resources?.[resourceType]) {
const { persistence, typeSearch, typeHistory } = this.config.profile.resources[resourceType];
return new ResourceHandler(
persistence,
typeSearch,
typeHistory,
this.config.auth.authorization,
this.config.server.url,
this.config.validators,
);
}

if (
this.getGenericResources(this.config.profile.fhirVersion).includes(resourceType) &&
this.config.profile.genericResource
) {
const { persistence, typeSearch, typeHistory } = this.config.profile.genericResource;

return new ResourceHandler(
persistence,
typeSearch,
typeHistory,
this.config.auth.authorization,
this.config.server.url,
this.config.validators,
);
}

return undefined;
}
}
59 changes: 42 additions & 17 deletions src/implementationGuides/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { StructureDefinitionImplementationGuides } from './index';
import { RoutingImplementationGuides } from './index';

describe('StructureDefinitionImplementationGuides', () => {
describe('RoutingImplementationGuides', () => {
describe(`compile`, async () => {
test(`valid StructureDefinition`, async () => {
const compiled = new StructureDefinitionImplementationGuides().compile([
test(`valid input`, async () => {
const compiled = new RoutingImplementationGuides().compile([
{
resourceType: 'StructureDefinition',
id: 'CARIN-BB-Organization',
Expand All @@ -28,24 +28,49 @@ describe('StructureDefinitionImplementationGuides', () => {
baseDefinition: 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-organization',
derivation: 'constraint',
},
{
resourceType: 'OperationDefinition',
id: 'docref',
url: 'http://hl7.org/fhir/us/core/OperationDefinition/docref',
version: '3.1.1',
name: 'USCoreFetchDocumentReferences',
title: 'US Core Fetch DocumentReferences',
status: 'active',
kind: 'operation',
date: '2019-05-21',
publisher: 'US Core Project',
description:
'This operation is used to return all the references to documents related to a patient...',
code: 'docref',
system: false,
type: true,
instance: false,
parameter: [],
},
]);

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",
},
]
`);
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",
},
Object {
"description": "This operation is used to return all the references to documents related to a patient...",
"name": "USCoreFetchDocumentReferences",
"resourceType": "OperationDefinition",
"url": "http://hl7.org/fhir/us/core/OperationDefinition/docref",
},
]
`);
});

test(`invalid StructureDefinition`, async () => {
const compiled = new StructureDefinitionImplementationGuides().compile([
test(`invalid input`, async () => {
const compiled = new RoutingImplementationGuides().compile([
{
foo: 'bar',
},
Expand Down
68 changes: 55 additions & 13 deletions src/implementationGuides/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,65 @@ export type FhirStructureDefinition = {
type: string;
};

/**
* Based on the FHIR OperationDefinition. This type only includes the fields that are required for the compile process.
* See: https://www.hl7.org/fhir/operationdefinition.html
*/
export type FhirOperationDefinition = {
resourceType: 'OperationDefinition';
url: string;
name: string;
description: string;
};

/**
* This class compiles StructuredDefinitions from IG packages
*/
export class StructureDefinitionImplementationGuides implements ImplementationGuides {
export class RoutingImplementationGuides 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[] = [];
const validDefinitions: (FhirStructureDefinition | FhirOperationDefinition)[] = [];
resources.forEach(s => {
if (StructureDefinitionImplementationGuides.isFhirStructureDefinition(s)) {
validStructureDefinitions.push(s);
if (
RoutingImplementationGuides.isFhirStructureDefinition(s) ||
RoutingImplementationGuides.isFhirOperationDefinition(s)
) {
validDefinitions.push(s);
} else {
throw new Error(`The following input is not a StructureDefinition: ${s.type} ${s.name}`);
throw new Error(
`The following input is not a StructureDefinition nor a OperationDefinition: ${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,
}));
return validDefinitions.map(fhirDefinition => {
switch (fhirDefinition.resourceType) {
case 'StructureDefinition':
return {
name: fhirDefinition.name,
url: fhirDefinition.url,
type: fhirDefinition.type,
resourceType: fhirDefinition.resourceType,
description: fhirDefinition.description,
baseDefinition: fhirDefinition.baseDefinition,
};
case 'OperationDefinition':
return {
name: fhirDefinition.name,
url: fhirDefinition.url,
resourceType: fhirDefinition.resourceType,
description: fhirDefinition.description,
};
default:
// this should never happen
throw new Error('Unexpected error');
}
});
}

private static isFhirStructureDefinition(x: any): x is FhirStructureDefinition {
Expand All @@ -60,4 +91,15 @@ export class StructureDefinitionImplementationGuides implements ImplementationGu
typeof x.type === 'string'
);
}

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

import { Router } from 'express';
import ConfigHandler from '../configHandler';
import { OperationDefinitionRegistry } from './OperationDefinitionRegistry';
import { OperationDefinitionImplementation } from './types';
import ResourceHandler from '../router/handlers/resourceHandler';

const fakeRouter = (jest.fn() as unknown) as Router;
const fakeOperation: OperationDefinitionImplementation = {
canonicalUrl: 'https://fwoa.com/operation/fakeOperation',
name: 'fakeOperation',
documentation: 'The documentation for the fakeOperation',
httpVerbs: ['GET'],
path: '/Patient/fakeOperation',
targetResourceType: 'Patient',
requestInformation: {
operation: 'read',
resourceType: 'Patient',
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
buildRouter: (resourceHandler: ResourceHandler) => fakeRouter,
};
describe('OperationDefinitionRegistry', () => {
test('getAllRouters', () => {
const configHandlerMock = {
getResourceHandler: jest.fn().mockReturnValue({}),
};

const operationDefinitionRegistry = new OperationDefinitionRegistry(
(configHandlerMock as unknown) as ConfigHandler,
[fakeOperation],
);

expect(operationDefinitionRegistry.getAllRouters()).toHaveLength(1);
expect(operationDefinitionRegistry.getAllRouters()[0]).toBe(fakeRouter);
});

test('getOperation', () => {
const configHandlerMock = {
getResourceHandler: jest.fn().mockReturnValue({}),
};

const operationDefinitionRegistry = new OperationDefinitionRegistry(
(configHandlerMock as unknown) as ConfigHandler,
[fakeOperation],
);

expect(operationDefinitionRegistry.getOperation('PATCH', '/Patient/fakeOperation')).toBeUndefined();
expect(operationDefinitionRegistry.getOperation('GET', '/Patient/someOtherOperation')).toBeUndefined();

expect(operationDefinitionRegistry.getOperation('GET', '/Patient/fakeOperation')).toBe(fakeOperation);
});

test('getCapabilities', () => {
const configHandlerMock = {
getResourceHandler: jest.fn().mockReturnValue({}),
};

const operationDefinitionRegistry = new OperationDefinitionRegistry(
(configHandlerMock as unknown) as ConfigHandler,
[fakeOperation],
);

expect(operationDefinitionRegistry.getCapabilities()).toMatchInlineSnapshot(`
Object {
"Patient": Object {
"operation": Array [
Object {
"definition": "https://fwoa.com/operation/fakeOperation",
"documentation": "The documentation for the fakeOperation",
"name": "fakeOperation",
},
],
},
}
`);
});

test('ResourceHandler not available', () => {
const configHandlerMock = {
getResourceHandler: jest.fn().mockReturnValue(undefined),
};

expect(
() => new OperationDefinitionRegistry((configHandlerMock as unknown) as ConfigHandler, [fakeOperation]),
).toThrowErrorMatchingInlineSnapshot(
`"Failed to initialize operation https://fwoa.com/operation/fakeOperation. Is your FhirConfig correct?"`,
);
});
});
Loading

0 comments on commit 105790f

Please sign in to comment.