Skip to content

Commit f321315

Browse files
authored
Deleting Patients (#540)
* Added button for deleting patient in ManagePatientModal.js. * Wrote DELETE patients/:id for deleting a patient's data from MongoDB and AWS S3. * Updated DELETE patients/:id/files/:stepKey/:fieldKey/:fileName by adding code for deleting a file from AWS. * Added SweetAlert Warning to the buttons for deleting files and audio recordings on the Patient Detail page. * Resolved bug where clicking on the delete button for audio recordings threw an error. * Added tests for DELETE patients/:id and updated tests for DELETE patients/:id/files/:stepKey/:fieldKey/:fileName.
1 parent c96b860 commit f321315

File tree

17 files changed

+368
-44
lines changed

17 files changed

+368
-44
lines changed

backend/__tests__/routes/patients/test-delete-patients-id-files-stepKey-fieldKey-fileName.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
setCurrentUser,
1111
withAuthentication,
1212
getCurrentAuthenticatedUserAttribute,
13+
initS3DeleteObjectMocker,
1314
} = require('../../utils/auth');
1415

1516
describe('DELETE /patients/:id/files/:stepKey/:fieldKey/:fileName #214', () => {
@@ -23,6 +24,7 @@ describe('DELETE /patients/:id/files/:stepKey/:fieldKey/:fileName #214', () => {
2324
await db.connect();
2425
initAuthMocker(AWS);
2526
setCurrentUser(AWS);
27+
initS3DeleteObjectMocker(AWS);
2628
});
2729

2830
beforeEach(() => {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const _ = require('lodash');
2+
const request = require('supertest');
3+
const AWS = require('aws-sdk-mock');
4+
const mongoose = require('mongoose');
5+
6+
let server = require('../../../app');
7+
const db = require('../../utils/db');
8+
const {
9+
initAuthMocker,
10+
setCurrentUser,
11+
initIdentityServiceMocker,
12+
withAuthentication,
13+
initS3ListObjectsV2Mocker,
14+
initS3DeleteObjectsMocker,
15+
} = require('../../utils/auth');
16+
17+
describe('DELETE /patients/:id', () => {
18+
const PATIENT_ID_WITH_ONE_FILE = '60944e084f4c0d4330cc258b';
19+
const BAD_PATIENT_ID = 'badid';
20+
21+
afterAll(async () => await db.closeDatabase());
22+
afterEach(async () => await db.resetDatabase());
23+
beforeAll(async () => {
24+
await db.connect();
25+
initAuthMocker(AWS);
26+
initIdentityServiceMocker(AWS);
27+
setCurrentUser(AWS);
28+
initS3DeleteObjectsMocker(AWS);
29+
initS3ListObjectsV2Mocker(AWS);
30+
});
31+
32+
beforeEach(() => {
33+
server = require('../../../app');
34+
});
35+
36+
it('returns 404 when given patient ID that does not exist', (done) => {
37+
withAuthentication(
38+
request(server).delete(
39+
`/api/patients/${BAD_PATIENT_ID}`,
40+
),
41+
).expect(404, done);
42+
});
43+
44+
it('deletes patient data when given existing patient ID', async () => {
45+
const res = await withAuthentication(
46+
request(server).delete(
47+
`/api/patients/${PATIENT_ID_WITH_ONE_FILE}`,
48+
),
49+
);
50+
expect(res.status).toBe(200);
51+
52+
/* Check if the patient has been deleted from the Patient collection */
53+
const actual_patient = await mongoose
54+
.model('Patient')
55+
.findById(PATIENT_ID_WITH_ONE_FILE);
56+
const expected_patient = null;
57+
58+
expect(actual_patient).toBe(expected_patient);
59+
60+
/* Check if the patient has been deleted from each Step's collection */
61+
const stepsToCheck = ['medicalInfo', 'survey', 'example'];
62+
63+
const testPromiseArray = stepsToCheck.map(async (stepKey) => {
64+
let Model;
65+
try {
66+
Model = mongoose.model(stepKey);
67+
// eslint-disable-next-line no-await-in-loop
68+
const actual_patient_step_data = await Model.findOne({ patientId: PATIENT_ID_WITH_ONE_FILE });
69+
const expected_patient_step_data = null;
70+
expect(actual_patient_step_data).toBe(expected_patient_step_data);
71+
} catch (error) {
72+
console.error(`test-delete-patients-id - step ${stepKey} not found`);
73+
return false;
74+
}
75+
return true;
76+
});
77+
78+
await Promise.all(testPromiseArray);
79+
});
80+
});

backend/__tests__/utils/auth.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,26 @@ module.exports.initS3GetMocker = (AWS) => {
8080
);
8181
};
8282

83+
module.exports.initS3DeleteObjectMocker = (AWS) => {
84+
AWS.mock('S3', 'deleteObject', (params) => {
85+
return Promise.resolve();
86+
});
87+
};
88+
89+
module.exports.initS3ListObjectsV2Mocker = (AWS) => {
90+
AWS.mock('S3', 'listObjectsV2', (params) => {
91+
return Promise.resolve({
92+
Contents: []
93+
});
94+
});
95+
};
96+
97+
module.exports.initS3DeleteObjectsMocker = (AWS) => {
98+
AWS.mock('S3', 'deleteObjects', (params) => {
99+
return Promise.resolve();
100+
});
101+
};
102+
83103
/**
84104
* Mocks the Cognito Identity Service Provider so that whenever the server queries for the current user, a static
85105
* user is returned.

backend/routes/api/patients.js

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@ const _ = require('lodash');
66

77
const { errorWrap } = require('../../utils');
88
const { models } = require('../../models');
9-
const { uploadFile, downloadFile } = require('../../utils/aws/awsS3Helpers');
109
const {
11-
ACCESS_KEY_ID,
12-
SECRET_ACCESS_KEY,
13-
} = require('../../utils/aws/awsExports');
10+
uploadFile, downloadFile, deleteFile, deleteFolder,
11+
} = require('../../utils/aws/awsS3Helpers');
1412
const { removeRequestAttributes } = require('../../middleware/requests');
1513
const {
1614
STEP_IMMUTABLE_ATTRIBUTES,
@@ -162,10 +160,6 @@ router.get(
162160
// Open a stream from the S3 bucket
163161
const s3Stream = downloadFile(
164162
`${id}/${stepKey}/${fieldKey}/${fileName}`,
165-
{
166-
accessKeyId: ACCESS_KEY_ID,
167-
secretAccessKey: SECRET_ACCESS_KEY,
168-
},
169163
).createReadStream();
170164

171165
// Setup callbacks for stream error and stream close
@@ -223,8 +217,6 @@ router.delete(
223217
return sendResponse(res, 404, `File "${fileName}" does not exist`);
224218
}
225219

226-
// TODO: Remove this file from AWS as well once we have
227-
// a "do you want to remove this" on the frontend
228220
// Remove the file from Mongo tracking
229221
stepData[fieldKey].splice(index, 1);
230222

@@ -238,6 +230,11 @@ router.delete(
238230
patient.lastEditedBy = req.user.name;
239231
await patient.save();
240232

233+
// Remove this file from AWS as well
234+
await deleteFile(
235+
`${id}/${stepKey}/${fieldKey}/${fileName}`,
236+
);
237+
241238
return sendResponse(res, 200, 'File deleted');
242239
}),
243240
);
@@ -283,10 +280,6 @@ router.post(
283280
await uploadFile(
284281
file.data,
285282
`${id}/${stepKey}/${fieldKey}/${fileName}`,
286-
{
287-
accessKeyId: ACCESS_KEY_ID,
288-
secretAccessKey: SECRET_ACCESS_KEY,
289-
},
290283
);
291284

292285
// Record this file in the DB
@@ -362,6 +355,55 @@ router.post(
362355
}),
363356
);
364357

358+
/**
359+
* Deletes all of the patient's data from the database.
360+
*/
361+
router.delete(
362+
'/:id',
363+
errorWrap(async (req, res) => {
364+
const { id } = req.params;
365+
366+
// Makes sure patient exists
367+
let patient;
368+
try {
369+
patient = await models.Patient.findById(id);
370+
} catch {
371+
return sendResponse(res, 404, `${id} is not a valid patient id`);
372+
}
373+
374+
if (!patient) return sendResponse(res, 404, `Patient "${id}" not found`);
375+
376+
// Deletes the patient from the Patient Collection
377+
await models.Patient.findOneAndDelete({ _id: mongoose.Types.ObjectId(id) });
378+
379+
const allStepKeys = await models.Step.find({}, 'key');
380+
381+
// Create array of promises to speed this up a bit
382+
const lookups = allStepKeys.map(async (stepKeyData) => {
383+
let Model;
384+
const stepKey = stepKeyData.key;
385+
try {
386+
Model = mongoose.model(stepKey);
387+
// eslint-disable-next-line no-await-in-loop
388+
await Model.findOneAndDelete({ patientId: id });
389+
} catch (error) {
390+
// eslint-disable-next-line no-console
391+
console.error(`DELETE /patients/:id - step ${stepKey} not found`);
392+
return false;
393+
}
394+
return true;
395+
});
396+
397+
// Deletes the patient from each Steps's Collection
398+
await Promise.all(lookups);
399+
400+
// Deletes the patient's files from the AWS S3 bucket
401+
await deleteFolder(id);
402+
403+
return sendResponse(res, 200, 'Deleted patient');
404+
}),
405+
);
406+
365407
const updatePatientStepData = async (patientId, StepModel, data) => {
366408
let patientStepData = await StepModel.findOne({ patientId });
367409

backend/utils/aws/awsS3Helpers.js

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
const AWS = require('aws-sdk');
22

3-
const { S3_BUCKET_NAME, S3_REGION } = require('./awsExports');
3+
const {
4+
S3_BUCKET_NAME, S3_REGION, ACCESS_KEY_ID, SECRET_ACCESS_KEY,
5+
} = require('./awsExports');
6+
7+
// S3 Credential Object created with access id and secret key
8+
const S3_CREDENTIALS = {
9+
accessKeyId: ACCESS_KEY_ID,
10+
secretAccessKey: SECRET_ACCESS_KEY,
11+
};
412

513
/**
614
* Uploads a file to the S3 bucket
@@ -9,14 +17,14 @@ const { S3_BUCKET_NAME, S3_REGION } = require('./awsExports');
917
* @param credentials The temporary credentials of the end user. Frontend should provide this.
1018
* @param onUploaded Callback after finished uploading. Params are (err, data).
1119
*/
12-
const uploadFile = async (content, remoteFileName, credentials) => {
20+
const uploadFile = async (content, remoteFileName) => {
1321
const params = {
1422
Body: content,
1523
Bucket: S3_BUCKET_NAME,
1624
Key: remoteFileName,
1725
};
1826

19-
const s3 = getS3(credentials);
27+
const s3 = getS3(S3_CREDENTIALS);
2028
await s3.putObject(params).promise();
2129
};
2230

@@ -26,18 +34,62 @@ const uploadFile = async (content, remoteFileName, credentials) => {
2634
* @param credentials The temporary credentials of the end user. Frontend should provide this.
2735
* @param onDownloaded Callback after finished downloading. Params are (err, data).
2836
*/
29-
const downloadFile = (objectKey, credentials) => {
37+
const downloadFile = (objectKey) => {
3038
const params = {
3139
Bucket: S3_BUCKET_NAME,
3240
Key: objectKey,
3341
};
3442

35-
const s3 = getS3(credentials);
43+
const s3 = getS3(S3_CREDENTIALS);
3644
const object = s3.getObject(params);
3745

3846
return object;
3947
};
4048

49+
const deleteFile = async (filePath) => {
50+
const params = {
51+
Bucket: S3_BUCKET_NAME,
52+
Key: filePath,
53+
};
54+
55+
const s3 = getS3(S3_CREDENTIALS);
56+
await s3.deleteObject(params).promise();
57+
};
58+
59+
/**
60+
* Delete's all of the files in a folder
61+
* @param {String} folderName The id of the patient
62+
* Source: https://stackoverflow.com/questions/20207063/how-can-i-delete-folder-on-s3-with-node-js
63+
*/
64+
const deleteFolder = async (folderName) => {
65+
const params = {
66+
Bucket: S3_BUCKET_NAME,
67+
Prefix: `${folderName}/`,
68+
};
69+
70+
const s3 = getS3(S3_CREDENTIALS);
71+
72+
// Gets up to 1000 files that need to be deleted
73+
const listedObjects = await s3.listObjectsV2(params).promise();
74+
if (listedObjects.Contents.length === 0) return;
75+
76+
const deleteParams = {
77+
Bucket: S3_BUCKET_NAME,
78+
Delete: { Objects: [] },
79+
};
80+
81+
// Builds a list of the files to delete
82+
listedObjects.Contents.forEach(({ Key }) => {
83+
deleteParams.Delete.Objects.push({ Key });
84+
});
85+
86+
// Deletes the files from S3
87+
await s3.deleteObjects(deleteParams).promise();
88+
89+
// If there are more than 1000 objects that need to be deleted from the folder
90+
if (listedObjects.IsTruncated) await deleteFolder(folderName, S3_CREDENTIALS);
91+
};
92+
4193
function getS3(credentials) {
4294
const s3 = new AWS.S3({
4395
accessKeyId: credentials.accessKeyId,
@@ -51,3 +103,5 @@ function getS3(credentials) {
51103

52104
exports.uploadFile = uploadFile;
53105
exports.downloadFile = downloadFile;
106+
exports.deleteFolder = deleteFolder;
107+
exports.deleteFile = deleteFile;

frontend/src/api/api.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ export const updatePatient = async (patientId, updatedData) => {
7171
return res.data;
7272
};
7373

74+
export const deletePatientById = async (patientId) => {
75+
const requestString = `/patients/${patientId}`;
76+
const res = await instance.delete(requestString);
77+
78+
if (!res?.data?.success) throw new Error(res?.data?.message);
79+
80+
return res.data;
81+
};
82+
7483
export const getAllStepsMetadata = async (showHiddenFieldsAndSteps = false) => {
7584
const requestString = `/metadata/steps?showHiddenFields=${showHiddenFieldsAndSteps}&showHiddenSteps=${showHiddenFieldsAndSteps}`;
7685

0 commit comments

Comments
 (0)