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

feat: enable $docref based on the compiled IGs #83

Merged
merged 2 commits into from
May 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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 All @@ -38,6 +39,7 @@ export function generateServerlessRouter(
const serverUrl: string = fhirConfig.server.url;
let hasCORSEnabled: boolean = false;
const registry = new FHIRStructureDefinitionRegistry(compiledImplementationGuides);
const operationRegistry = initializeOperationRegistry(configHandler);
carvantes marked this conversation as resolved.
Show resolved Hide resolved

const app = express();
app.use(express.urlencoded({ extended: true }));
Expand Down Expand Up @@ -70,7 +72,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.userIdentity = await fhirConfig.auth.authorization.verifyAccessToken({
Expand All @@ -93,6 +97,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'
);
}
}
68 changes: 68 additions & 0 deletions src/operationDefinitions/OperationDefinitionRegistry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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',
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('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?"`,
);
});
});
38 changes: 38 additions & 0 deletions src/operationDefinitions/OperationDefinitionRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*
*/

import { Router } from 'express';
import { OperationDefinitionImplementation } from './types';
import ConfigHandler from '../configHandler';

export class OperationDefinitionRegistry {
private readonly operations: OperationDefinitionImplementation[];

private readonly routers: Router[];

constructor(configHandler: ConfigHandler, operations: OperationDefinitionImplementation[]) {
this.operations = operations;

this.routers = operations.map(operation => {
const resourceHandler = configHandler.getResourceHandler(operation.targetResourceType);
if (!resourceHandler) {
throw new Error(
`Failed to initialize operation ${operation.canonicalUrl}. Is your FhirConfig correct?`,
);
}
console.log(`Enabling operation ${operation.canonicalUrl} at ${operation.path}`);
return operation.buildRouter(resourceHandler);
});
}

getOperation(method: string, path: string): OperationDefinitionImplementation | undefined {
return this.operations.find(o => o.path === path && o.httpVerbs.includes(method));
}

getAllRouters(): Router[] {
return this.routers;
}
}
Loading