From c006d6e31da118c9850e8a70839fcb18accb4d75 Mon Sep 17 00:00:00 2001 From: Cooper Golemme <135241668+cgolemme@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:05:41 -0400 Subject: [PATCH] Create Patient Service and Implement CRUD operations (#45) * 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. --- .eslintrc.json | 26 ++++---- .prettierignore | 3 +- src/server/app.js | 17 +++++- src/services/group.service.js | 27 +++++++-- src/services/patient.service.js | 83 +++++++++++++++++++++++++ src/util/bundleUtils.js | 13 +++- src/util/mongo.controller.js | 12 +++- test/fixtures/updatedTestPatient.json | 5 ++ test/services/group.service.test.js | 16 ++++- test/services/patient.service.test.js | 87 +++++++++++++++++++++++++++ 10 files changed, 262 insertions(+), 27 deletions(-) create mode 100644 src/services/patient.service.js create mode 100644 test/fixtures/updatedTestPatient.json create mode 100644 test/services/patient.service.test.js diff --git a/.eslintrc.json b/.eslintrc.json index 4490a5bf..0db06dd6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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"] } diff --git a/.prettierignore b/.prettierignore index ed9f9cc1..5fc39454 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ -coverage \ No newline at end of file +coverage +ecqm-content-r4-2021 \ No newline at end of file diff --git a/src/server/app.js b/src/server/app.js index e1b27f6a..5e0b819d 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -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 }); @@ -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; } diff --git a/src/services/group.service.js b/src/services/group.service.js index 6766fc7c..25db4475 100644 --- a/src/services/group.service.js +++ b/src/services/group.service.js @@ -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'); @@ -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 */ @@ -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 */ @@ -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 */ @@ -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 }; diff --git a/src/services/patient.service.js b/src/services/patient.service.js new file mode 100644 index 00000000..6b216012 --- /dev/null +++ b/src/services/patient.service.js @@ -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 +}; diff --git a/src/util/bundleUtils.js b/src/util/bundleUtils.js index c10f25af..639d8ab8 100644 --- a/src/util/bundleUtils.js +++ b/src/util/bundleUtils.js @@ -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 }; diff --git a/src/util/mongo.controller.js b/src/util/mongo.controller.js index c46327ba..63154da1 100644 --- a/src/util/mongo.controller.js +++ b/src/util/mongo.controller.js @@ -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 } }); }; /** @@ -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(); diff --git a/test/fixtures/updatedTestPatient.json b/test/fixtures/updatedTestPatient.json new file mode 100644 index 00000000..7f5dc118 --- /dev/null +++ b/test/fixtures/updatedTestPatient.json @@ -0,0 +1,5 @@ +{ + "resourceType": "Patient", + "id": "testPatient", + "gender": "female" +} diff --git a/test/services/group.service.test.js b/test/services/group.service.test.js index b321321b..ed44999d 100644 --- a/test/services/group.service.test.js +++ b/test/services/group.service.test.js @@ -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); }); }); @@ -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(); diff --git a/test/services/patient.service.test.js b/test/services/patient.service.test.js new file mode 100644 index 00000000..56344906 --- /dev/null +++ b/test/services/patient.service.test.js @@ -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(); + }); +});