From d0207d526bb361c68be5c0c2f60493bfe4addffa Mon Sep 17 00:00:00 2001 From: Sean Hogun Kim Date: Sat, 2 Apr 2022 19:44:56 +1300 Subject: [PATCH] [#234] Auto Prune Settings (#343) * Added TTL for encounters of 1 year * Pruning endpoint created. Takes input of date which we want to prune before TODO testing. * Added test to check that encounters before prune date are deleted * Fixed prune test to properly call api --- .../src/controllers/encounter.controller.ts | 27 +++++++ backend/src/models/encounter.model.ts | 4 +- .../routes/__test__/encounter.route.test.ts | 70 +++++++++++++++++++ backend/src/routes/encounter.route.ts | 2 + backend/src/services/encounter.service.ts | 25 +++++++ 5 files changed, 127 insertions(+), 1 deletion(-) diff --git a/backend/src/controllers/encounter.controller.ts b/backend/src/controllers/encounter.controller.ts index f4dccf53..a5336cf5 100644 --- a/backend/src/controllers/encounter.controller.ts +++ b/backend/src/controllers/encounter.controller.ts @@ -220,3 +220,30 @@ export const getAllEncounters = async ( res.status(httpStatus.INTERNAL_SERVER_ERROR).end(); } }; + +export const pruneEncounters = async ( + req: Request, + expressRes: Response, + next: NextFunction, +): Promise => { + const res = expressRes as PaginateableResponse; + logger.info('DELETE /encounters/:pruneLength request from frontend'); + + const authId = req.headers.authorization?.['user_id']; + const user = await userService.getUserByAuthId(authId); + const { pruneDate } = req.params; + + try { + if (!user) { + res.status(httpStatus.NOT_FOUND).end(); + } else { + // The stringify-parse combo removes typing allowing alteration of the persons field in each encounter + // Calls pruneEncounters with inputs of list of all signed in user's encounters and the date they want pruned before + const foundUserEncounters = JSON.parse(JSON.stringify(await encounterService.pruneEncounters(user.encounters, pruneDate))); + res.status(httpStatus.OK).paginate(foundUserEncounters); + } + } catch (e) { + next(e); + res.status(httpStatus.INTERNAL_SERVER_ERROR).end(); + } +}; diff --git a/backend/src/models/encounter.model.ts b/backend/src/models/encounter.model.ts index b3349194..51a4dda9 100644 --- a/backend/src/models/encounter.model.ts +++ b/backend/src/models/encounter.model.ts @@ -13,7 +13,9 @@ export interface EncounterModel { const schema = new Schema({ title: { type: String, required: true }, date: { type: Date, required: false }, - time_updated: { type: Date, default: new Date(Date.now()), required: true }, + time_updated: { + type: Date, default: new Date(Date.now()), expires: 31536000, required: true, // Added a one year expiry time to Encounter entries + }, location: { type: String, required: false }, latLong: { type: [Number], required: false }, description: { type: String, required: true }, diff --git a/backend/src/routes/__test__/encounter.route.test.ts b/backend/src/routes/__test__/encounter.route.test.ts index faacefc0..89dbdf19 100644 --- a/backend/src/routes/__test__/encounter.route.test.ts +++ b/backend/src/routes/__test__/encounter.route.test.ts @@ -162,6 +162,24 @@ const encounterData: EncounterModel = { persons: [] as any, } +const encounterPruneData: EncounterModel = { + title: "PruneEncounter", + date: new Date("2021-10-01T00:51:11.707Z"), + time_updated: new Date("2021-10-01T00:51:11.707Z"), + description: "To be pruned", + persons: [] as any, + location: '' +} + +const encounterDontPruneData: EncounterModel = { + title: "DontPruneEncounter", + date: new Date("2021-01-01T00:51:11.707Z"), + time_updated: new Date("2021-01-01T00:51:11.707Z"), + description: "Should not be pruend", + persons: [] as any, + location: '' +} + describe('POST /encounter', () => { it('Successfully creates an encounter with all info given', async () => { await supertest(app).post('/api/users') @@ -1091,6 +1109,58 @@ describe('DELETE /encounter/:id', () => { }) }); +// Prune Encounter 200 +describe('DELETE /encounter/prune/:pruneDate', () => { + it('Successfully prunes entries before a given date: ', async () => { + // Get Authentication ID for User + const auth_id = await testUtils.getAuthIdFromToken(token); + + // Create Person + const personOne = new Person(person1Data); + const personOneId = (await personOne.save())._id; + + // Create Encounter that needs to be pruned - 2021-10-1 + const encounterPrune = new Encounter(encounterPruneData); + const encounterPruneId = (await encounterPrune.save())._id; + + // Create Encounter that should not be pruned - 2022-1-1 + const encounterDontPrune = new Encounter(encounterDontPruneData); + const encounterDontPruneId = (await encounterDontPrune.save())._id; + + // Create User + const user = new User(user1Data); + + // Add Encounters and Person ID to User encounters + user.persons.push(personOneId); + user.encounters.push(encounterPruneId); + user.encounters.push(encounterDontPruneId); + user.auth_id = auth_id; + await user.save(); + + // Add Encounter IDs and Person ID to each other + personOne.encounters.push(encounterPruneId); + encounterPrune.persons.push(personOneId); + personOne.encounters.push(encounterDontPruneId); + encounterDontPrune.persons.push(personOneId); + await personOne.save(); + await encounterPrune.save(); + await encounterDontPrune.save(); + + await supertest(app).delete(`/api/encounters/prune/2021-12-30T00:51:11.707Z`) + .set('Accept', 'application/json') + .set('Authorization', token) + .expect(httpStatus.OK); + + // Check that encounterPrune has been removed + const newUser = await User.findOne({auth_id: user.auth_id}); + expect(newUser?.encounters).not.toContain(encounterPruneId); + + const newPerson = await Person.findOne({_id: personOne._id}); + expect(newPerson?.encounters).not.toContain(encounterPruneId); + + expect(await Encounter.findById({_id: encounterPruneId})).toEqual(null); + })}) + /***************************************************************** * Utility functions ****************************************************************/ diff --git a/backend/src/routes/encounter.route.ts b/backend/src/routes/encounter.route.ts index 4003046d..d29fabf5 100644 --- a/backend/src/routes/encounter.route.ts +++ b/backend/src/routes/encounter.route.ts @@ -8,6 +8,7 @@ import { updateEncounter, getEncounter, deleteEncounters, + pruneEncounters, } from '../controllers/encounter.controller'; const routes = Router(); @@ -17,5 +18,6 @@ routes.post('/', createEncounter); routes.put('/:id', updateEncounter); routes.get('/:id', getEncounter); routes.delete('/:id', deleteEncounters); +routes.delete('/prune/:pruneDate', pruneEncounters); export default routes; diff --git a/backend/src/services/encounter.service.ts b/backend/src/services/encounter.service.ts index 6994a3b2..7437e5f9 100644 --- a/backend/src/services/encounter.service.ts +++ b/backend/src/services/encounter.service.ts @@ -90,6 +90,30 @@ const deleteEncounter = async (encounterID: String) => { return false; }; +/** + * Service used for pruning encounters + * Users can specify a date where they want all encounters that haven't been modified before that date to be deleted + * @param userEncounters List of encounters belonging to the signed in user + * @param pruneDateString Prune date in string form, e.g. 2021-10-30T00:51:11.707Z + * @returns List of encounters after pruning + */ +const pruneEncounters = async (userEncounters: mongoose.Types.ObjectId[], pruneDateString: string) => { + let foundUserEncounters = await Encounter.find({ _id: { $in: userEncounters } }); + let pruneDate = new Date(pruneDateString); + + foundUserEncounters = foundUserEncounters.filter(async (encounter) => { + let currentEncounter = await getEncounter(encounter); + if (currentEncounter?.time_updated) { + let encounterDate = new Date(currentEncounter.time_updated); + // Delete all encounters whose last time_updated precedes pruneDate + if (encounterDate.getTime() <= pruneDate.getTime()) { + deleteEncounter(currentEncounter._id.toString()); + } + } + }); + return foundUserEncounters; +}; + const encounterService = { createEncounter, updateEncounter, @@ -97,6 +121,7 @@ const encounterService = { getAllEncounters, deleteEncounter, deleteEncounterPerson, + pruneEncounters, }; export default encounterService;