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

feat: add docref to capability statement #85

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
8 changes: 7 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,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 Down
27 changes: 27 additions & 0 deletions src/operationDefinitions/OperationDefinitionRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ 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',
Expand Down Expand Up @@ -54,6 +56,31 @@ describe('OperationDefinitionRegistry', () => {
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",
Bingjiling marked this conversation as resolved.
Show resolved Hide resolved
"documentation": "The documentation for the fakeOperation",
"name": "fakeOperation",
},
],
},
}
`);
});

test('ResourceHandler not available', () => {
const configHandlerMock = {
getResourceHandler: jest.fn().mockReturnValue(undefined),
Expand Down
31 changes: 31 additions & 0 deletions src/operationDefinitions/OperationDefinitionRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ import { Router } from 'express';
import { OperationDefinitionImplementation } from './types';
import ConfigHandler from '../configHandler';

export interface OperationCapability {
operation: {
name: string;
definition: string;
documentation: string;
}[];
}

export interface OperationCapabilityStatement {
[resourceType: string]: OperationCapability;
}

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

Expand Down Expand Up @@ -35,4 +47,23 @@ export class OperationDefinitionRegistry {
getAllRouters(): Router[] {
return this.routers;
}

getCapabilities(): OperationCapabilityStatement {
const capabilities: OperationCapabilityStatement = {};

this.operations.forEach(operation => {
Bingjiling marked this conversation as resolved.
Show resolved Hide resolved
if (!capabilities[operation.targetResourceType]) {
capabilities[operation.targetResourceType] = {
operation: [],
};
}
capabilities[operation.targetResourceType].operation.push({
name: operation.name,
definition: operation.canonicalUrl,
documentation: operation.documentation,
});
});

return capabilities;
}
}
4 changes: 4 additions & 0 deletions src/operationDefinitions/USCoreDocRef/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const docRefImpl = async (resourceHandler: ResourceHandler, userIdentity: KeyVal

export const USCoreDocRef: OperationDefinitionImplementation = {
canonicalUrl: 'http://hl7.org/fhir/us/core/OperationDefinition/docref',
name: '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',
path: '/DocumentReference/$docref',
httpVerbs: ['GET', 'POST'],
targetResourceType: 'DocumentReference',
Expand Down
11 changes: 11 additions & 0 deletions src/operationDefinitions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ export interface OperationDefinitionImplementation {
*/
readonly canonicalUrl: string;

/**
* common name for the operation. It is found as `code` or `id` in the corresponding OperationDefinition resource
*/
readonly name: string;

/**
* Usually based on the `description` of the corresponding OperationDefinition resource.
* documentation should also include details that are specific to this implementation of the operation
*/
readonly documentation: string;

/**
* url path used to invoke the operation
* @example '/DocumentReference/$docref'
Expand Down
11 changes: 11 additions & 0 deletions src/router/metadata/cap.rest.resource.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
Resource,
} from 'fhir-works-on-aws-interface';
import { ResourceCapabilityStatement, ResourceCapability } from '../../registry/ResourceCapabilityInterface';
import {
OperationCapability,
OperationCapabilityStatement,
} from '../../operationDefinitions/OperationDefinitionRegistry';

function makeResourceObject(
resourceType: string,
Expand All @@ -19,6 +23,7 @@ function makeResourceObject(
hasTypeSearch: boolean,
searchCapabilities?: SearchCapabilities,
resourceCapability?: ResourceCapability,
operationCapability?: OperationCapability,
) {
const result: any = {
type: resourceType,
Expand All @@ -40,6 +45,10 @@ function makeResourceObject(
Object.assign(result, resourceCapability);
}

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

return result;
}

Expand All @@ -58,6 +67,7 @@ export function makeGenericResources(
operations: TypeOperation[],
searchCapabilityStatement: SearchCapabilityStatement,
resourceCapabilityStatement: ResourceCapabilityStatement,
operationCapabilityStatement: OperationCapabilityStatement,
updateCreate: boolean,
) {
const resources: any[] = [];
Expand All @@ -74,6 +84,7 @@ export function makeGenericResources(
hasTypeSearch,
searchCapabilityStatement[resourceType],
resourceCapabilityStatement[resourceType],
operationCapabilityStatement[resourceType],
),
);
});
Expand Down
84 changes: 66 additions & 18 deletions src/router/metadata/metadataHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import JsonSchemaValidator from '../validation/jsonSchemaValidator';
import ConfigHandler from '../../configHandler';
import { utcTimeRegExp } from '../../regExpressions';
import { FHIRStructureDefinitionRegistry } from '../../registry';
import { OperationDefinitionRegistry } from '../../operationDefinitions/OperationDefinitionRegistry';

const r4Validator = new JsonSchemaValidator('4.0.1');
const stu3Validator = new JsonSchemaValidator('3.0.1');
Expand Down Expand Up @@ -331,6 +332,20 @@ const overrideStubs = {
};
const registry: FHIRStructureDefinitionRegistry = new FHIRStructureDefinitionRegistry();

const operationRegistryMock: OperationDefinitionRegistry = ({
getCapabilities: jest.fn().mockReturnValue({
Account: {
operation: [
{
definition: 'https://fwoa.com/operation/fakeOperation',
documentation: 'The documentation for the fakeOperation',
name: 'fakeOperation',
},
],
},
}),
} as unknown) as OperationDefinitionRegistry;

describe('ERROR: test cases', () => {
beforeEach(() => {
// Ensures that for each test, we test the assertions in the catch block
Expand All @@ -342,7 +357,7 @@ describe('ERROR: test cases', () => {
stu3FhirConfigWithExclusions(),
SUPPORTED_STU3_RESOURCES,
);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry, operationRegistryMock);
try {
// OPERATE
await metadataHandler.capabilities({ fhirVersion: '4.0.1', mode: 'full' });
Expand All @@ -360,7 +375,7 @@ describe('ERROR: test cases', () => {
r4FhirConfigGeneric(overrideStubs),
SUPPORTED_R4_RESOURCES,
);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry, operationRegistryMock);
try {
// OPERATE
await metadataHandler.capabilities({ fhirVersion: '3.0.1', mode: 'full' });
Expand All @@ -377,7 +392,7 @@ test('STU3: FHIR Config V3 with 2 exclusions and search', async () => {
const config = stu3FhirConfigWithExclusions(overrideStubs);
const supportedGenericResources = ['AllergyIntolerance', 'Organization', 'Account', 'Patient'];
const configHandler: ConfigHandler = new ConfigHandler(config, supportedGenericResources);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry, true);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry, operationRegistryMock, true);
const response = await metadataHandler.capabilities({ fhirVersion: '3.0.1', mode: 'full' });
const { genericResource } = config.profile;
const excludedResources = genericResource ? genericResource.excludedSTU3Resources || [] : [];
Expand Down Expand Up @@ -417,20 +432,53 @@ test('STU3: FHIR Config V3 with 2 exclusions and search', async () => {
});
test('R4: FHIR Config V4 without search', async () => {
const configHandler: ConfigHandler = new ConfigHandler(r4FhirConfigGeneric(overrideStubs), SUPPORTED_R4_RESOURCES);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry, operationRegistryMock);
const response = await metadataHandler.capabilities({ fhirVersion: '4.0.1', mode: 'full' });
expect(response.resource).toBeDefined();
expect(response.resource.acceptUnknown).toBeUndefined();
expect(response.resource.fhirVersion).toEqual('4.0.1');
expect(response.resource.rest.length).toEqual(1);
expect(response.resource.rest[0].resource.length).toEqual(SUPPORTED_R4_RESOURCES.length);
expect(response.resource.rest[0].security.cors).toBeFalsy();
// see if the four CRUD + vRead operations are chosen
const expectedResourceSubset = {
interaction: makeOperation(['create', 'read', 'update', 'delete', 'vread', 'history-instance']),
updateCreate: configHandler.config.profile.genericResource!.persistence.updateCreateSupported,
};
expect(response.resource.rest[0].resource[0]).toMatchObject(expectedResourceSubset);
expect(response.resource.rest[0].resource[0]).toMatchInlineSnapshot(`
Bingjiling marked this conversation as resolved.
Show resolved Hide resolved
Object {
"conditionalCreate": false,
"conditionalDelete": "not-supported",
"conditionalRead": "not-supported",
"conditionalUpdate": false,
"interaction": Array [
Object {
"code": "create",
},
Object {
"code": "read",
},
Object {
"code": "update",
},
Object {
"code": "delete",
},
Object {
"code": "vread",
},
Object {
"code": "history-instance",
},
],
"operation": Array [
Object {
"definition": "https://fwoa.com/operation/fakeOperation",
"documentation": "The documentation for the fakeOperation",
"name": "fakeOperation",
},
],
"readHistory": false,
"type": "Account",
"updateCreate": false,
"versioning": "versioned",
}
`);
expect(response.resource.rest[0].interaction).toEqual(
makeOperation(r4FhirConfigGeneric(overrideStubs).profile.systemOperations),
);
Expand All @@ -441,7 +489,7 @@ test('R4: FHIR Config V4 without search', async () => {
test('R4: FHIR Config V4 with 3 exclusions and AllergyIntollerance special', async () => {
const config = r4FhirConfigWithExclusions(overrideStubs);
const configHandler: ConfigHandler = new ConfigHandler(config, SUPPORTED_R4_RESOURCES);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry, operationRegistryMock);
const response = await metadataHandler.capabilities({ fhirVersion: '4.0.1', mode: 'full' });
const { genericResource } = config.profile;
const excludedResources = genericResource ? genericResource.excludedR4Resources || [] : [];
Expand Down Expand Up @@ -483,7 +531,7 @@ test('R4: FHIR Config V4 with 3 exclusions and AllergyIntollerance special', asy
test('R4: FHIR Config V4 no generic set-up & mix of STU3 & R4', async () => {
const config = r4FhirConfigNoGeneric(overrideStubs);
const configHandler: ConfigHandler = new ConfigHandler(config, SUPPORTED_R4_RESOURCES);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry, operationRegistryMock);
const configResource: any = config.profile.resources;
const response = await metadataHandler.capabilities({ fhirVersion: '4.0.1', mode: 'full' });
expect(response.resource).toBeDefined();
Expand Down Expand Up @@ -531,7 +579,7 @@ each([
const fhirConfig = fhirConfigBuilder({ persistence, ...overrideStubs });

const configHandler: ConfigHandler = new ConfigHandler(fhirConfig, SUPPORTED_R4_RESOURCES);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry, operationRegistryMock);
const response = await metadataHandler.capabilities({ fhirVersion: '4.0.1', mode: 'full' });
response.resource.rest[0].resource.forEach((resource: any) => {
const expectedResourceSubset = {
Expand All @@ -545,7 +593,7 @@ test('R4: FHIR Config V4 with bulkDataAccess', async () => {
const r4ConfigWithBulkDataAccess = r4FhirConfigGeneric(overrideStubs);
r4ConfigWithBulkDataAccess.profile.bulkDataAccess = stubs.bulkDataAccess;
const configHandler: ConfigHandler = new ConfigHandler(r4ConfigWithBulkDataAccess, SUPPORTED_R4_RESOURCES);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry, operationRegistryMock);
const response = await metadataHandler.capabilities({ fhirVersion: '4.0.1', mode: 'full' });

expect(response.resource.rest[0].operation).toEqual([
Expand All @@ -564,7 +612,7 @@ test('R4: FHIR Config V4 with bulkDataAccess', async () => {

test('R4: FHIR Config V4 without bulkDataAccess', async () => {
const configHandler: ConfigHandler = new ConfigHandler(r4FhirConfigGeneric(overrideStubs), SUPPORTED_R4_RESOURCES);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry, operationRegistryMock);
const response = await metadataHandler.capabilities({ fhirVersion: '4.0.1', mode: 'full' });

expect(response.resource.rest[0].operation).toBeUndefined();
Expand All @@ -584,7 +632,7 @@ test('R4: FHIR Config V4 with all Oauth Policy endpoints', async () => {
},
};
const configHandler: ConfigHandler = new ConfigHandler(r4ConfigWithOauthEndpoints, SUPPORTED_R4_RESOURCES);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry, operationRegistryMock);
const response = await metadataHandler.capabilities({ fhirVersion: '4.0.1', mode: 'full' });

expect(response.resource.rest[0].security).toEqual({
Expand Down Expand Up @@ -645,7 +693,7 @@ test('R4: FHIR Config V4 with some Oauth Policy endpoints', async () => {
},
};
const configHandler: ConfigHandler = new ConfigHandler(r4ConfigWithOauthEndpoints, SUPPORTED_R4_RESOURCES);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry, operationRegistryMock);
const response = await metadataHandler.capabilities({ fhirVersion: '4.0.1', mode: 'full' });

expect(response.resource.rest[0].security).toEqual({
Expand Down Expand Up @@ -695,7 +743,7 @@ test('R4: FHIR Config V4 with all productInfo params', async () => {
copyright: 'Copyright',
};
const configHandler: ConfigHandler = new ConfigHandler(r4ConfigWithAllProductInfo, SUPPORTED_R4_RESOURCES);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry);
const metadataHandler: MetadataHandler = new MetadataHandler(configHandler, registry, operationRegistryMock);
const response = await metadataHandler.capabilities({ fhirVersion: '4.0.1', mode: 'full' });

const expectedResponse: any = {
Expand Down
Loading