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

Commit

Permalink
feat: add HAPI FHIR Lambda Validator (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
carvantes authored Mar 8, 2021
1 parent d68a31a commit d22fdf2
Show file tree
Hide file tree
Showing 6 changed files with 446 additions and 4 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.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 {
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)),
};

const lambdaResponse = await this.lambdaClient.invoke(params).promise();

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

0 comments on commit d22fdf2

Please sign in to comment.