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

feat: add HAPI FHIR Lambda Validator #63

Merged
merged 5 commits into from
Mar 8, 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
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.1",
"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;
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';
}
76 changes: 76 additions & 0 deletions src/router/validation/hapiFhirLambdaValidator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import AWS from 'aws-sdk';
import * as AWSMock from 'aws-sdk-mock';
import { InvalidResourceError } from 'fhir-works-on-aws-interface';
import HapiFhirLambdaValidator from './hapiFhirLambdaValidator';

AWSMock.setSDKInstance(AWS);
const SOME_RESOURCE = 'my value does not matter because validation lambda is always mocked';
const VALIDATOR_LAMBDA_ARN = 'my value does not matter because validation lambda is always mocked';

describe('HapiFhirLambdaValidator', () => {
beforeEach(() => {
AWSMock.restore();
});

test('valid resource', async () => {
AWSMock.mock('Lambda', 'invoke', (params: any, callback: Function) => {
callback(null, {
StatusCode: 200,
Payload: JSON.stringify({
successful: true,
}),
});
});

const hapiFhirLambdaValidator = new HapiFhirLambdaValidator(VALIDATOR_LAMBDA_ARN);
await expect(hapiFhirLambdaValidator.validate(SOME_RESOURCE)).resolves.toBeUndefined();
});

test('invalid resource', async () => {
AWSMock.mock('Lambda', 'invoke', (params: any, callback: Function) => {
callback(null, {
StatusCode: 200,
Payload: JSON.stringify({
errorMessages: [
{
severity: 'error',
msg: 'error1',
},
{
severity: 'error',
msg: 'error2',
},
{
severity: 'warning',
msg: 'warning1',
},
],
successful: false,
}),
});
});

const hapiFhirLambdaValidator = new HapiFhirLambdaValidator(VALIDATOR_LAMBDA_ARN);
await expect(hapiFhirLambdaValidator.validate(SOME_RESOURCE)).rejects.toThrowError(
new InvalidResourceError('error1\nerror2'),
);
});

test('lambda execution fails', async () => {
AWSMock.mock('Lambda', 'invoke', (params: any, callback: Function) => {
callback(null, {
StatusCode: 200,
FunctionError: 'unhandled',
Payload: 'some error msg',
});
});

const hapiFhirLambdaValidator = new HapiFhirLambdaValidator(VALIDATOR_LAMBDA_ARN);
await expect(hapiFhirLambdaValidator.validate(SOME_RESOURCE)).rejects.toThrow('lambda function failed');
});
});
64 changes: 64 additions & 0 deletions src/router/validation/hapiFhirLambdaValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { InvalidResourceError, Validator } from 'fhir-works-on-aws-interface';
import { Lambda } from 'aws-sdk';
import AWS from '../../AWS';

interface ErrorMessage {
severity: string;
msg: string;
}

interface HapiValidatorResponse {
errorMessages: ErrorMessage[];
successful: boolean;
}
// a relatively high number to give cold starts a chance to succeed
const TIMEOUT_MILLISECONDS = 25_000;

export default class HapiFhirLambdaValidator implements Validator {
rsmayda marked this conversation as resolved.
Show resolved Hide resolved
private hapiValidatorLambdaArn: string;

private lambdaClient: Lambda;

constructor(hapiValidatorLambdaArn: string) {
this.hapiValidatorLambdaArn = hapiValidatorLambdaArn;
this.lambdaClient = new AWS.Lambda({
httpOptions: {
timeout: TIMEOUT_MILLISECONDS,
},
});
}

async validate(resource: any): Promise<void> {
const params = {
FunctionName: this.hapiValidatorLambdaArn,
InvocationType: 'RequestResponse',
Payload: JSON.stringify(JSON.stringify(resource)),
rsmayda marked this conversation as resolved.
Show resolved Hide resolved
};

const lambdaResponse = await this.lambdaClient.invoke(params).promise();
rsmayda marked this conversation as resolved.
Show resolved Hide resolved

if (lambdaResponse.FunctionError) {
// this means that the lambda function crashed, not necessarily that the resource is invalid.
const msg = `The execution of ${this.hapiValidatorLambdaArn} lambda function failed`;
console.log(msg, lambdaResponse);
throw new Error(msg);
}
// response payload is always a string. the Payload type is also used for invoke parameters
const hapiValidatorResponse = JSON.parse(lambdaResponse.Payload as string) as HapiValidatorResponse;
if (hapiValidatorResponse.successful) {
return;
}

const allErrorMessages = hapiValidatorResponse.errorMessages
.filter(e => e.severity === 'error')
.map(e => e.msg)
.join('\n');

throw new InvalidResourceError(allErrorMessages);
}
}
Loading