This repository has been archived by the owner on Apr 13, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add HAPI FHIR Lambda Validator (#63)
- Loading branch information
Showing
6 changed files
with
446 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.