Skip to content

Commit

Permalink
Create Patient Service and Implement CRUD operations (#45)
Browse files Browse the repository at this point in the history
* Added /Patient endpoint with CRUD ops and tests

* Fixed comments

* Make errors not appear

* restore main of testPatient.json

* Add delete endpoints for patients and groups, update projection, change to response to a searchset.

* Got rid of double projection set.
  • Loading branch information
cgolemme committed Jun 20, 2024
1 parent 806fcc6 commit c006d6e
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 27 deletions.
26 changes: 13 additions & 13 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true,
"jest": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 13,
"sourceType": "module"
},
"rules": {
}
"env": {
"browser": true,
"es2021": true,
"node": true,
"jest": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 13,
"sourceType": "module"
},
"rules": {},
"ignorePatterns": ["ecqm-content-r4-2021"]
}
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
coverage
coverage
ecqm-content-r4-2021
17 changes: 15 additions & 2 deletions src/server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ const cors = require('@fastify/cors');
const { bulkExport, patientBulkExport, groupBulkExport } = require('../services/export.service');
const { checkBulkStatus } = require('../services/bulkstatus.service');
const { returnNDJsonContent } = require('../services/ndjson.service');
const { groupSearchById, groupSearch, groupCreate, groupUpdate } = require('../services/group.service');
const { groupSearchById, groupSearch, groupCreate, groupUpdate, groupRemove } = require('../services/group.service');
const { uploadTransactionOrBatchBundle } = require('../services/bundle.service');
const { generateCapabilityStatement } = require('../services/metadata.service');

const {
patientSearch,
patientSearchById,
patientCreate,
patientUpdate,
patientRemove
} = require('../services/patient.service');
// set bodyLimit to 50mb
function build(opts) {
const app = fastify({ ...opts, bodyLimit: 50 * 1024 * 1024 });
Expand All @@ -25,7 +31,14 @@ function build(opts) {
app.get('/Group', groupSearch);
app.post('/Group', groupCreate);
app.put('/Group/:groupId', groupUpdate);
app.delete('/Group/:groupId', groupRemove);
app.post('/', uploadTransactionOrBatchBundle);
app.get('/Patient/:patientId', patientSearchById);
app.get('/Patient', patientSearch);
app.post('/Patient', patientCreate);
app.put('/Patient/:patientId', patientUpdate);
app.delete('/Patient/:patientId', patientRemove);

return app;
}

Expand Down
27 changes: 21 additions & 6 deletions src/services/group.service.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const { createSearchsetBundle } = require('../util/bundleUtils');
const {
findResourceById,
findResourcesWithQuery,
createResource,
updateResource
updateResource,
removeResource
} = require('../util/mongo.controller');
const { v4: uuidv4 } = require('uuid');

Expand All @@ -22,7 +24,6 @@ const groupSearchById = async (request, reply) => {

/**
* Result of sending a GET request to [base]/Group to find all available Groups.
* Searches for a Group resource with the passed in id
* @param {Object} request the request object passed in by the user
* @param {Object} reply the response object
*/
Expand All @@ -31,11 +32,11 @@ const groupSearch = async (request, reply) => {
if (!result.length > 0) {
reply.code(404).send(new Error('No Group resources were found on the server'));
}
return result;
return createSearchsetBundle(result);
};

/**
* Creates an object and generates an id for it regardless of the id passed in
* Creates a Group object and generates an id for it regardless of the id passed in.
* @param {Object} request the request object passed in by the user
* @param {Object} reply the response object
*/
Expand All @@ -48,7 +49,7 @@ const groupCreate = async (request, reply) => {

/**
* Updates the Group resource with the passed in id or creates a new document if
* no document with passed id is found
* no document with passed id is found.
* @param {Object} request the request object passed in by the user
* @param {Object} reply the response object
*/
Expand All @@ -60,9 +61,23 @@ const groupUpdate = async (request, reply) => {
return updateResource(request.params.groupId, data, 'Group');
};

/**
* Deletes the Group resource with the passed in id. Sends 404 if Group with id passed in is not found.
* @param {Object} request the request object passed in by the user
* @param {Object} reply the response object
*/
const groupRemove = async (request, reply) => {
const found = await findResourceById(request.params.groupId, 'Group');
if (!found) {
reply.code(404).send(new Error(`The requested group ${request.params.groupId} was not found.`));
}
return removeResource(request.params.groupId, 'Group');
};

module.exports = {
groupSearchById,
groupSearch,
groupCreate,
groupUpdate
groupUpdate,
groupRemove
};
83 changes: 83 additions & 0 deletions src/services/patient.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const { createSearchsetBundle } = require('../util/bundleUtils');
const {
findResourceById,
findResourcesWithQuery,
createResource,
updateResource,
removeResource
} = require('../util/mongo.controller');
const { v4: uuidv4 } = require('uuid');

/**
* Result of sending a GET request to [base]/Patient/[id].
* Searches for a Patient resource with the passed in id
* @param {Object} request the request object passed in by the user
* @param {Object} reply the response object
*/
const patientSearchById = async (request, reply) => {
const result = await findResourceById(request.params.patientId, 'Patient');
if (!result) {
reply.code(404).send(new Error(`The requested patient ${request.params.patientId} was not found.`));
}
return result;
};

/**
* Result of sending a GET request to [base]/Patient to find all available Patients.
* @param {Object} request the request object passed in by the user
* @param {Object} reply the response object
*/
const patientSearch = async (request, reply) => {
const result = await findResourcesWithQuery({}, 'Patient');
if (!result.length > 0) {
reply.code(404).send(new Error('No Patient resources were found on the server'));
}
return createSearchsetBundle(result);
};

/**
* Creates a Patient object and generates an id for it regardless of the id passed in.
* @param {Object} request the request object passed in by the user
* @param {Object} reply the response object
*/
const patientCreate = async (request, reply) => {
const data = request.body;
data['id'] = uuidv4();
reply.code(201);
return createResource(data, 'Patient');
};

/**
* Updates the Patient resource with the passed in id or creates a new document if
* no document with passed id is found.
* @param {Object} request the request object passed in by the user
* @param {Object} reply the response object
*/
const patientUpdate = async (request, reply) => {
const data = request.body;
if (data.id !== request.params.patientId) {
reply.code(400).send(new Error('Argument id must match request body id for PUT request'));
}
return updateResource(request.params.patientId, data, 'Patient');
};

/**
* Deletes the Patient resource with the passed in id. Sends 404 if Patient with id passed in not found.
* @param {Object} request the request object passed in by the user
* @param {Object} reply the response object
*/
const patientRemove = async (request, reply) => {
const found = await findResourceById(request.params.patientId, 'Patient');
if (!found) {
reply.code(404).send(new Error(`The requested patient ${request.params.patientId} was not found.`));
}
return removeResource(request.params.patientId, 'Patient');
};

module.exports = {
patientSearchById,
patientSearch,
patientCreate,
patientUpdate,
patientRemove
};
13 changes: 12 additions & 1 deletion src/util/bundleUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,15 @@ const replaceReferences = entries => {
return newEntries;
};

module.exports = { replaceReferences };
function createSearchsetBundle(entries) {
return {
resourceType: 'Bundle',
meta: { lastUpdated: new Date().toISOString() },
id: uuidv4(),
type: 'searchset',
total: entries.length,
entry: entries.map(e => ({ resource: e }))
};
}

module.exports = { replaceReferences, createSearchsetBundle };
12 changes: 9 additions & 3 deletions src/util/mongo.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const createResource = async (data, resourceType) => {
*/
const findResourceById = async (id, resourceType) => {
const collection = db.collection(resourceType);
return collection.findOne({ id: id });
return collection.findOne({ id: id }, { projection: { _id: 0 } });
};

/**
Expand All @@ -37,10 +37,16 @@ const findResourceById = async (id, resourceType) => {
*/
const findOneResourceWithQuery = async (query, resourceType) => {
const collection = db.collection(resourceType);
return collection.findOne(query);
return collection.findOne(query, { projection: { _id: 0 } });
};

const findResourcesWithQuery = async (query, resourceType, options = {}) => {
/**
* Searches the database for the one or more resources based on a mongo query and returns the data.
* @param {Object} query the mongo query to use
* @param {string} resourceType type of desired resource, signifies collection resource is stored in
* @return {Array} the data of the found documents
*/
const findResourcesWithQuery = async (query, resourceType, options = { projection: { _id: 0 } }) => {
const collection = db.collection(resourceType);
const results = collection.find(query, options);
return results.toArray();
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/updatedTestPatient.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"resourceType": "Patient",
"id": "testPatient",
"gender": "female"
}
16 changes: 15 additions & 1 deletion test/services/group.service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('CRUD operations for Group resource', () => {
.get(`/Group`)
.expect(200)
.then(response => {
expect(JSON.parse(response.text).length).toEqual(1);
expect(response.body.total).toEqual(1);
});
});

Expand All @@ -68,6 +68,20 @@ describe('CRUD operations for Group resource', () => {
await supertest(app.server).put(`/Group/${TEST_GROUP_ID}`).send(updatedTestGroup).expect(200);
});

test('test delete returns 200 when group in db', async () => {
await createTestResource(testGroup, 'Group');
await supertest(app.server).delete(`/Group/${TEST_GROUP_ID}`).expect(200);
});

test('test delete returns 404 when group is not in db', async () => {
await supertest(app.server)
.delete(`/Group/${TEST_GROUP_ID}`)
.expect(404)
.then(res => {
expect(JSON.parse(res.text).message).toEqual(`The requested group ${TEST_GROUP_ID} was not found.`);
});
});

afterEach(async () => {
await cleanUpDb();
await queue.close();
Expand Down
87 changes: 87 additions & 0 deletions test/services/patient.service.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const build = require('../../src/server/app');
const app = build();
const { client } = require('../../src/util/mongo');
const supertest = require('supertest');
const { cleanUpDb, createTestResource } = require('../populateTestData');
const testPatient = require('../fixtures/testPatient.json');
const updatedTestPatient = require('../fixtures/updatedTestPatient.json');
const queue = require('../../src/resources/exportQueue');

const TEST_PATIENT_ID = 'testPatient';
const INVALID_PATIENT_ID = 'INVALID';

describe('CRUD operations for Patient resource', () => {
beforeEach(async () => {
await client.connect();
await app.ready();
});
test('test create returns 201', async () => {
await supertest(app.server).post('/Patient').send(testPatient).expect(201);
});

test('test searchById should return 200 when patient is in db', async () => {
await createTestResource(testPatient, 'Patient');
await supertest(app.server)
.get(`/Patient/${TEST_PATIENT_ID}`)
.expect(200)
.then(response => {
expect(response.body.id).toEqual(TEST_PATIENT_ID);
});
});

test('test searchById should return 404 when patient is not in db', async () => {
await supertest(app.server)
.get(`/Patient/${INVALID_PATIENT_ID}`)
.expect(404)
.then(response => {
expect(JSON.parse(response.text).message).toEqual('The requested patient INVALID was not found.');
});
});

test('test search should return 200 when patients are in the db', async () => {
await createTestResource(testPatient, 'Patient');
await supertest(app.server)
.get(`/Patient`)
.expect(200)
.then(response => {
expect(response.body.total).toEqual(1);
});
});

test('test search should return 404 if no patients are in the db', async () => {
await supertest(app.server)
.get(`/Patient`)
.expect(404)
.then(response => {
expect(JSON.parse(response.text).message).toEqual('No Patient resources were found on the server');
});
});

test('test update returns 200 when patient is in db', async () => {
await createTestResource(testPatient, 'Patient');
await supertest(app.server).put(`/Patient/${TEST_PATIENT_ID}`).send(updatedTestPatient).expect(200);
});

test('test update returns 201 when patient is not in db', async () => {
await supertest(app.server).put(`/Patient/${TEST_PATIENT_ID}`).send(updatedTestPatient).expect(200);
});

test('test delete returns 200 when patient in db', async () => {
await createTestResource(testPatient, 'Patient');
await supertest(app.server).delete(`/Patient/${TEST_PATIENT_ID}`).expect(200);
});

test('test delete returns 404 when patient is not in db', async () => {
await supertest(app.server)
.delete(`/Patient/${TEST_PATIENT_ID}`)
.expect(404)
.then(res => {
expect(JSON.parse(res.text).message).toEqual(`The requested patient ${TEST_PATIENT_ID} was not found.`);
});
});

afterEach(async () => {
await cleanUpDb();
await queue.close();
});
});

0 comments on commit c006d6e

Please sign in to comment.