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

feat: add Batch bundle support #602

Merged
merged 7 commits into from
Apr 6, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
278 changes: 278 additions & 0 deletions integration-tests/batchBundle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import { AxiosInstance } from 'axios';
import { getFhirClient } from './utils';

jest.setTimeout(60 * 1000);

const generateGetRequests = (id: string, amount: number) => {
const requests = [];
for (let i = 0; i < amount; i += 1) {
requests.push({
request: {
method: 'GET',
url: `/Patient/${id}`,
},
});
}
return requests;
};

const generateGetResponses = (id: string, amount: number) => {
const responses = [];
for (let i = 0; i < amount; i += 1) {
responses.push({
response: {
status: '200 OK',
location: `Patient/${id}`,
etag: '1',
},
});
}
return responses;
};

describe('Batch bundles', () => {
let client: AxiosInstance;
beforeAll(async () => {
client = await getFhirClient();
});

// expect get and delete to fail in this batch, but batch should succeed
const firstBatch = {
resourceType: 'Bundle',
type: 'batch',
entry: [
{
request: {
method: 'GET',
url: '/Patient/someRandomResource',
},
},
{
request: {
method: 'DELETE',
url: '/Patient/someResource',
},
},
{
resource: {
id: 'createdResource',
resourceType: 'Patient',
text: {
status: 'generated',
div: '<div xmlns="http://www.w3.org/1999/xhtml">Some narrative</div>',
},
active: true,
name: [
{
use: 'official',
family: 'Chalmers',
given: ['Peter', 'James'],
},
],
gender: 'male',
birthDate: '1974-12-25',
},
request: {
method: 'POST',
url: '/Patient/',
},
},
{
resource: {
id: 'resourceToDelete',
resourceType: 'Patient',
text: {
status: 'generated',
div: '<div xmlns="http://www.w3.org/1999/xhtml">Some narrative</div>',
},
active: true,
name: [
{
use: 'official',
family: 'Chalmers',
given: ['Peter', 'James'],
},
],
gender: 'male',
birthDate: '1974-12-25',
},
request: {
method: 'POST',
url: '/Patient/',
},
},
{
resource: {
id: 'resourceToGet',
resourceType: 'Patient',
text: {
status: 'generated',
div: '<div xmlns="http://www.w3.org/1999/xhtml">Some narrative</div>',
},
active: true,
name: [
{
use: 'official',
family: 'Chalmers',
given: ['Peter', 'James'],
},
],
gender: 'male',
birthDate: '1974-12-25',
},
request: {
method: 'POST',
url: '/Patient/',
},
},
],
};

test('post multiple batches with failures', async () => {
const response = await client.post('/', firstBatch);
expect(response.data).toMatchObject({
resourceType: 'Bundle',
type: 'batch-response',
entry: [
{
response: {
status: '404 Not Found',
location: 'Patient/someRandomResource',
},
},
{
response: {
status: '404 Not Found',
location: 'Patient/someResource',
},
},
{
response: {
status: '201 Created',
etag: '1',
ssvegaraju marked this conversation as resolved.
Show resolved Hide resolved
},
},
{
response: {
status: '201 Created',
etag: '1',
},
},
{
response: {
status: '201 Created',
etag: '1',
},
},
],
});

const createdResourceId = response.data.entry[2].response.location;
const deleteResourceId = response.data.entry[3].response.location;
const getResourceId = response.data.entry[4].response.location;

const secondBatch = {
resourceType: 'Bundle',
type: 'batch',
entry: [
{
request: {
method: 'GET',
url: `/${getResourceId}`,
},
},
{
request: {
method: 'DELETE',
url: `/${deleteResourceId}`,
},
},
{
resource: {
id: `${createdResourceId.replace('Patient/', '')}`,
resourceType: 'Patient',
text: {
status: 'generated',
div: '<div xmlns="http://www.w3.org/1999/xhtml">Some narrative</div>',
},
active: true,
name: [
{
use: 'official',
family: 'Chalmers',
given: ['Peter', 'James'],
},
],
gender: 'female',
birthDate: '1974-12-25',
},
request: {
method: 'PUT',
url: `/${createdResourceId}`,
},
},
],
};

const secondResponse = await client.post('/', secondBatch);
expect(secondResponse.data).toMatchObject({
resourceType: 'Bundle',
type: 'batch-response',
entry: [
{
response: {
status: '200 OK',
location: `${getResourceId}`,
etag: '1',
},
},
{
response: {
status: '200 OK',
location: `${deleteResourceId}`,
etag: '1',
},
},
{
response: {
status: '200 OK',
location: `${createdResourceId}`,
etag: '2',
},
},
],
});
});

test('bulk test', async () => {
const postRequest = {
resourceType: 'Patient',
active: true,
name: [
{
family: 'Emily',
given: ['Tester'],
},
],
gender: 'female',
birthDate: '1995-09-24',
id: 'test',
};

const response = await client.post('/Patient', postRequest);
expect(response.status).toEqual(201);
const { id } = response.data;
const requests = generateGetRequests(id, 101);
const batchResponse = await client.post('/', {
resourceType: 'Bundle',
type: 'batch',
entry: requests,
});
expect(batchResponse.status).toEqual(200);
expect(batchResponse.data).toMatchObject({
resourceType: 'Bundle',
type: 'batch-response',
entry: generateGetResponses(id, 101),
});
});
});
4 changes: 4 additions & 0 deletions serverless.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ custom:
enableSubscriptions: ${opt:enableSubscriptions, 'false'}
logLevel: ${opt:logLevel, 'error'}
enableESHardDelete: ${opt:enableESHardDelete, 'false'}
maxBatchEntries: ${opt:maxBatchEntries, '750'}
Bingjiling marked this conversation as resolved.
Show resolved Hide resolved
patientCompartmentFileV3: 'patientCompartmentSearchParams.3.0.2.json'
patientCompartmentFileV4: 'patientCompartmentSearchParams.4.0.1.json'
bundle:
Expand Down Expand Up @@ -67,6 +68,7 @@ provider:
ENABLE_MULTI_TENANCY: !Ref EnableMultiTenancy
ENABLE_SUBSCRIPTIONS: !Ref EnableSubscriptions
LOG_LEVEL: '${self:custom.logLevel}'
MAX_BATCH_ENTRIES: '${self.custom.maxBatchEntries}'
apiKeys:
- name: 'developer-key-${self:custom.stage}' # Full name must be known at package-time
description: Key for developer to access the FHIR Api
Expand Down Expand Up @@ -626,6 +628,8 @@ resources:
- 'dynamodb:UpdateItem'
- 'dynamodb:DeleteItem'
- 'dynamodb:BatchWriteItem'
- 'dynamodb:PartiQLInsert'
- 'dynamodb:PartiQLUpdate'
Resource:
- !GetAtt ResourceDynamoDBTableV2.Arn
- !Join ['', [!GetAtt ResourceDynamoDBTableV2.Arn, '/index/*']]
Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ const dynamoDbBundleService = new DynamoDbBundleService(DynamoDb, undefined, und
enableMultiTenancy,
});

// Lambda payload limit is 6MB, assuming an average request of 4KB,
Bingjiling marked this conversation as resolved.
Show resolved Hide resolved
// we have 6MB / 4Kb = 1500. Dividing by half to allow for contingencies, we get 750.
// This value is customizable during deployment by appending --maxBatchEntries <your value here>
// if no param is defined during deployment, we use this default
export const maxBatchSize: Number = 750;

// Configure the input validators. Validators run in the order that they appear on the array. Use an empty array to disable input validation.
const validators: Validator[] = [];
if (process.env.VALIDATOR_LAMBDA_ALIAS && process.env.VALIDATOR_LAMBDA_ALIAS !== '[object Object]') {
Expand Down