diff --git a/api/src/controllers/action.js b/api/src/controllers/action.js index 9d0f681d5..c2956f8cb 100644 --- a/api/src/controllers/action.js +++ b/api/src/controllers/action.js @@ -52,6 +52,7 @@ router.post( organisation: data.organisation, createdAt: data.createdAt, updatedAt: data.updatedAt, + deletedAt: data.deletedAt, status: data.status, dueAt: data.dueAt, completedAt: data.completedAt, @@ -69,12 +70,14 @@ router.get( z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.limit); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.page); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.lastRefresh); + z.optional(z.enum(["true", "false"])).parse(req.query.withDeleted); + z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.after); } catch (e) { const error = new Error(`Invalid request in action get: ${e}`); error.status = 400; return next(error); } - const { limit, page, lastRefresh } = req.query; + const { limit, page, lastRefresh, after, withDeleted } = req.query; const query = { where: { organisation: req.user.organisation }, @@ -88,7 +91,15 @@ router.get( const total = await Action.count(query); if (limit) query.limit = Number(limit); if (page) query.offset = Number(page) * limit; - if (lastRefresh) query.where.updatedAt = { [Op.gte]: new Date(Number(lastRefresh)) }; + if (lastRefresh) { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(lastRefresh)) } }]; + } + if (withDeleted === "true") query.paranoid = false; + if (after && !isNaN(Number(after)) && withDeleted === "true") { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(after)) } }, { deletedAt: { [Op.gte]: new Date(Number(after)) } }]; + } else if (after && !isNaN(Number(after))) { + query.where.updatedAt = { [Op.gte]: new Date(Number(after)) }; + } const sortDoneOrCancel = (a, b) => { if (!a.dueAt) return -1; @@ -107,6 +118,7 @@ router.get( "organisation", "createdAt", "updatedAt", + "deletedAt", // Specific fields that are not encrypted "status", "dueAt", @@ -168,6 +180,7 @@ router.put( organisation: action.organisation, createdAt: action.createdAt, updatedAt: action.updatedAt, + deletedAt: data.deletedAt, status: action.status, dueAt: action.dueAt, completedAt: action.completedAt, @@ -196,6 +209,10 @@ router.delete( }, }); if (!action) return res.status(200).send({ ok: true }); + + action.set({ encrypted: null, encryptedEntityKey: null }); + await action.save(); + await action.destroy(); res.status(200).send({ ok: true }); diff --git a/api/src/controllers/comment.js b/api/src/controllers/comment.js index 649584e35..5302203cb 100644 --- a/api/src/controllers/comment.js +++ b/api/src/controllers/comment.js @@ -42,6 +42,7 @@ router.post( organisation: data.organisation, createdAt: data.createdAt, updatedAt: data.updatedAt, + deletedAt: data.deletedAt, }, }); }) @@ -56,12 +57,14 @@ router.get( z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.limit); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.page); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.lastRefresh); + z.optional(z.enum(["true", "false"])).parse(req.query.withDeleted); + z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.after); } catch (e) { const error = new Error(`Invalid request in comment get: ${e}`); error.status = 400; return next(error); } - const { limit, page, lastRefresh } = req.query; + const { limit, page, lastRefresh, after, withDeleted } = req.query; const query = { where: { organisation: req.user.organisation }, @@ -71,11 +74,19 @@ router.get( const total = await Comment.count(query); if (limit) query.limit = Number(limit); if (page) query.offset = Number(page) * limit; - if (lastRefresh) query.where.updatedAt = { [Op.gte]: new Date(Number(lastRefresh)) }; + if (lastRefresh) { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(lastRefresh)) } }]; + } + if (withDeleted === "true") query.paranoid = false; + if (after && !isNaN(Number(after)) && withDeleted === "true") { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(after)) } }, { deletedAt: { [Op.gte]: new Date(Number(after)) } }]; + } else if (after && !isNaN(Number(after))) { + query.where.updatedAt = { [Op.gte]: new Date(Number(after)) }; + } const data = await Comment.findAll({ ...query, - attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt"], + attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt", "deletedAt"], }); return res.status(200).send({ ok: true, data, hasMore: data.length === Number(limit), total }); }) @@ -119,6 +130,7 @@ router.put( organisation: newComment.organisation, createdAt: newComment.createdAt, updatedAt: newComment.updatedAt, + deletedAt: newComment.deletedAt, }, }); }) @@ -141,6 +153,9 @@ router.delete( const comment = await Comment.findOne(query); if (!comment) return res.status(200).send({ ok: true }); + comment.set({ encrypted: null, encryptedEntityKey: null }); + await comment.save(); + await comment.destroy(); res.status(200).send({ ok: true }); }) diff --git a/api/src/controllers/organisation.js b/api/src/controllers/organisation.js index 33e6f4995..a2be34172 100644 --- a/api/src/controllers/organisation.js +++ b/api/src/controllers/organisation.js @@ -31,6 +31,8 @@ router.get( try { z.string().regex(looseUuidRegex).parse(req.query.organisation); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.lastRefresh); + z.optional(z.enum(["true", "false"])).parse(req.query.withDeleted); + z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.after); } catch (e) { const error = new Error(`Invalid request in stats get: ${e}`); error.status = 400; @@ -38,8 +40,15 @@ router.get( } const query = { where: { organisation: req.query.organisation } }; - if (Number(req.query.lastRefresh)) { - query.where.updatedAt = { [Op.gte]: new Date(Number(req.query.lastRefresh)) }; + const { lastRefresh, after, withDeleted } = req.query; + if (lastRefresh) { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(lastRefresh)) } }]; + } + if (withDeleted === "true") query.paranoid = false; + if (after && !isNaN(Number(after)) && withDeleted === "true") { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(after)) } }, { deletedAt: { [Op.gte]: new Date(Number(after)) } }]; + } else if (after && !isNaN(Number(after))) { + query.where.updatedAt = { [Op.gte]: new Date(Number(after)) }; } const places = await Place.count(query); diff --git a/api/src/controllers/passage.js b/api/src/controllers/passage.js index 565840bb6..4d821e696 100644 --- a/api/src/controllers/passage.js +++ b/api/src/controllers/passage.js @@ -42,6 +42,7 @@ router.post( organisation: data.organisation, createdAt: data.createdAt, updatedAt: data.updatedAt, + deletedAt: data.deletedAt, }, }); }) @@ -56,12 +57,14 @@ router.get( z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.limit); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.page); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.lastRefresh); + z.optional(z.enum(["true", "false"])).parse(req.query.withDeleted); + z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.after); } catch (e) { const error = new Error(`Invalid request in passage get: ${e}`); error.status = 400; return next(error); } - const { limit, page, lastRefresh } = req.query; + const { limit, page, lastRefresh, after, withDeleted } = req.query; const query = { where: { organisation: req.user.organisation }, @@ -71,11 +74,19 @@ router.get( const total = await Passage.count(query); if (limit) query.limit = Number(limit); if (page) query.offset = Number(page) * limit; - if (lastRefresh) query.where.updatedAt = { [Op.gte]: new Date(Number(lastRefresh)) }; + if (lastRefresh) { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(lastRefresh)) } }]; + } + if (withDeleted === "true") query.paranoid = false; + if (after && !isNaN(Number(after)) && withDeleted === "true") { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(after)) } }, { deletedAt: { [Op.gte]: new Date(Number(after)) } }]; + } else if (after && !isNaN(Number(after))) { + query.where.updatedAt = { [Op.gte]: new Date(Number(after)) }; + } const data = await Passage.findAll({ ...query, - attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt"], + attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt", "deletedAt"], }); return res.status(200).send({ ok: true, data, hasMore: data.length === Number(limit), total }); }) @@ -120,6 +131,7 @@ router.put( organisation: newPassage.organisation, createdAt: newPassage.createdAt, updatedAt: newPassage.updatedAt, + deletedAt: newPassage.deletedAt, }, }); }) @@ -142,6 +154,9 @@ router.delete( const passage = await Passage.findOne(query); if (!passage) return res.status(200).send({ ok: true }); + passage.set({ encrypted: null, encryptedEntityKey: null }); + await passage.save(); + await passage.destroy(); res.status(200).send({ ok: true }); }) diff --git a/api/src/controllers/person.js b/api/src/controllers/person.js index 27366716f..61af85ab8 100644 --- a/api/src/controllers/person.js +++ b/api/src/controllers/person.js @@ -153,6 +153,7 @@ router.post( organisation: p.organisation, createdAt: p.createdAt, updatedAt: p.updatedAt, + deletedAt: p.deletedAt, })), }); }) @@ -190,6 +191,7 @@ router.post( organisation: data.organisation, createdAt: data.createdAt, updatedAt: data.updatedAt, + deletedAt: data.deletedAt, }, }); }) @@ -204,12 +206,14 @@ router.get( z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.limit); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.page); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.lastRefresh); + z.optional(z.enum(["true", "false"])).parse(req.query.withDeleted); + z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.after); } catch (e) { const error = new Error(`Invalid request in person get: ${e}`); error.status = 400; return next(error); } - const { limit, page, lastRefresh } = req.query; + const { limit, page, lastRefresh, after, withDeleted } = req.query; const query = { where: { organisation: req.user.organisation }, @@ -218,11 +222,19 @@ router.get( const total = await Person.count(query); if (limit) query.limit = Number(limit); if (page) query.offset = Number(page) * limit; - if (lastRefresh) query.where.updatedAt = { [Op.gte]: new Date(Number(lastRefresh)) }; + if (lastRefresh) { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(lastRefresh)) } }]; + } + if (withDeleted === "true") query.paranoid = false; + if (after && !isNaN(Number(after)) && withDeleted === "true") { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(after)) } }, { deletedAt: { [Op.gte]: new Date(Number(after)) } }]; + } else if (after && !isNaN(Number(after))) { + query.where.updatedAt = { [Op.gte]: new Date(Number(after)) }; + } const data = await Person.findAll({ ...query, - attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt"], + attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt", "deletedAt"], }); return res.status(200).send({ @@ -272,6 +284,7 @@ router.put( organisation: newPerson.organisation, createdAt: newPerson.createdAt, updatedAt: newPerson.updatedAt, + deletedAt: newPerson.deletedAt, }, }); }) @@ -294,6 +307,8 @@ router.delete( let person = await Person.findOne(query); if (!person) return res.status(404).send({ ok: false, error: "Not Found" }); + person.set({ encrypted: null, encryptedEntityKey: null }); + await person.save(); await person.destroy(); res.status(200).send({ ok: true }); }) diff --git a/api/src/controllers/place.js b/api/src/controllers/place.js index de63e3d2c..2872a84b9 100644 --- a/api/src/controllers/place.js +++ b/api/src/controllers/place.js @@ -41,6 +41,7 @@ router.post( organisation: data.organisation, createdAt: data.createdAt, updatedAt: data.updatedAt, + deletedAt: data.deletedAt, }, }); }) @@ -55,12 +56,14 @@ router.get( z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.limit); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.page); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.lastRefresh); + z.optional(z.enum(["true", "false"])).parse(req.query.withDeleted); + z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.after); } catch (e) { const error = new Error(`Invalid request in place get: ${e}`); error.status = 400; return next(error); } - const { limit, page, lastRefresh } = req.query; + const { limit, page, lastRefresh, after, withDeleted } = req.query; const query = { where: { organisation: req.user.organisation }, @@ -70,11 +73,19 @@ router.get( const total = await Place.count(query); if (limit) query.limit = Number(limit); if (page) query.offset = Number(page) * limit; - if (lastRefresh) query.where.updatedAt = { [Op.gte]: new Date(Number(lastRefresh)) }; + if (lastRefresh) { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(lastRefresh)) } }]; + } + if (withDeleted === "true") query.paranoid = false; + if (after && !isNaN(Number(after)) && withDeleted === "true") { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(after)) } }, { deletedAt: { [Op.gte]: new Date(Number(after)) } }]; + } else if (after && !isNaN(Number(after))) { + query.where.updatedAt = { [Op.gte]: new Date(Number(after)) }; + } const data = await Place.findAll({ ...query, - attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt"], + attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt", "deletedAt"], }); return res.status(200).send({ ok: true, data, hasMore: data.length === Number(limit), total }); }) @@ -115,6 +126,7 @@ router.put( organisation: place.organisation, createdAt: place.createdAt, updatedAt: place.updatedAt, + deletedAt: place.deletedAt, }, }); }) @@ -137,6 +149,9 @@ router.delete( const place = await Place.findOne(query); if (!place) return res.status(404).send({ ok: false, error: "Not Found" }); + place.set({ encrypted: null, encryptedEntityKey: null }); + await place.save(); + await place.destroy(); res.status(200).send({ ok: true }); }) diff --git a/api/src/controllers/relPersonPlace.js b/api/src/controllers/relPersonPlace.js index 643a1461f..6cc55ec87 100644 --- a/api/src/controllers/relPersonPlace.js +++ b/api/src/controllers/relPersonPlace.js @@ -42,6 +42,7 @@ router.post( organisation: data.organisation, createdAt: data.createdAt, updatedAt: data.updatedAt, + deletedAt: data.deletedAt, }, }); }) @@ -56,12 +57,14 @@ router.get( z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.limit); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.page); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.lastRefresh); + z.optional(z.enum(["true", "false"])).parse(req.query.withDeleted); + z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.after); } catch (e) { const error = new Error(`Invalid request in relPersonPlace get: ${e}`); error.status = 400; return next(error); } - const { limit, page, lastRefresh } = req.query; + const { limit, page, lastRefresh, after, withDeleted } = req.query; const query = { where: { organisation: req.user.organisation }, @@ -71,11 +74,19 @@ router.get( const total = await RelPersonPlace.count(query); if (limit) query.limit = Number(limit); if (page) query.offset = Number(page) * limit; - if (lastRefresh) query.where.updatedAt = { [Op.gte]: new Date(Number(lastRefresh)) }; + if (lastRefresh) { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(lastRefresh)) } }]; + } + if (withDeleted === "true") query.paranoid = false; + if (after && !isNaN(Number(after)) && withDeleted === "true") { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(after)) } }, { deletedAt: { [Op.gte]: new Date(Number(after)) } }]; + } else if (after && !isNaN(Number(after))) { + query.where.updatedAt = { [Op.gte]: new Date(Number(after)) }; + } const data = await RelPersonPlace.findAll({ ...query, - attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt"], + attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt", "deletedAt"], }); return res.status(200).send({ ok: true, data, hasMore: data.length === Number(limit), total }); }) @@ -93,7 +104,15 @@ router.delete( error.status = 400; return next(error); } - await RelPersonPlace.destroy({ where: { _id: req.params._id, organisation: req.user.organisation } }); + const query = { where: { _id: req.params._id, organisation: req.user.organisation } }; + + const relPersonPlace = await RelPersonPlace.findOne(query); + if (!relPersonPlace) return res.status(404).send({ ok: false, error: "Not Found" }); + + relPersonPlace.set({ encrypted: null, encryptedEntityKey: null }); + await relPersonPlace.save(); + + await relPersonPlace.destroy(); res.status(200).send({ ok: true }); }) ); diff --git a/api/src/controllers/report.js b/api/src/controllers/report.js index cf79cf9de..7607d0638 100644 --- a/api/src/controllers/report.js +++ b/api/src/controllers/report.js @@ -18,23 +18,33 @@ router.get( z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.limit); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.page); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.lastRefresh); + z.optional(z.enum(["true", "false"])).parse(req.query.withDeleted); + z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.after); } catch (e) { const error = new Error(`Invalid request in report get: ${e}`); error.status = 400; return next(error); } - const { limit, page, lastRefresh } = req.query; + const { limit, page, lastRefresh, after, withDeleted } = req.query; const query = { where: { organisation: req.user.organisation } }; const total = await Report.count(query); if (limit) query.limit = Number(limit); if (page) query.offset = Number(page) * limit; - if (lastRefresh) query.where.updatedAt = { [Op.gte]: new Date(Number(lastRefresh)) }; + if (lastRefresh) { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(lastRefresh)) } }]; + } + if (withDeleted === "true") query.paranoid = false; + if (after && !isNaN(Number(after)) && withDeleted === "true") { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(after)) } }, { deletedAt: { [Op.gte]: new Date(Number(after)) } }]; + } else if (after && !isNaN(Number(after))) { + query.where.updatedAt = { [Op.gte]: new Date(Number(after)) }; + } const data = await Report.findAll({ ...query, - attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt"], + attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt", "deletedAt"], }); return res.status(200).send({ ok: true, data, hasMore: data.length === Number(limit), total }); }) @@ -72,6 +82,7 @@ router.post( organisation: data.organisation, createdAt: data.createdAt, updatedAt: data.updatedAt, + deletedAt: data.deletedAt, }, }); }) @@ -111,6 +122,7 @@ router.put( organisation: report.organisation, createdAt: report.createdAt, updatedAt: report.updatedAt, + deletedAt: report.deletedAt, }, }); }) @@ -133,6 +145,9 @@ router.delete( const report = await Report.findOne(query); if (!report) return res.status(200).send({ ok: true }); + report.set({ encrypted: null, encryptedEntityKey: null }); + await report.save(); + await report.destroy(); res.status(200).send({ ok: true }); }) diff --git a/api/src/controllers/territory.js b/api/src/controllers/territory.js index 1475474fc..c0cae656e 100644 --- a/api/src/controllers/territory.js +++ b/api/src/controllers/territory.js @@ -40,6 +40,7 @@ router.post( organisation: data.organisation, createdAt: data.createdAt, updatedAt: data.updatedAt, + deletedAt: data.deletedAt, }, }); }) @@ -54,12 +55,14 @@ router.get( z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.limit); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.page); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.lastRefresh); + z.optional(z.enum(["true", "false"])).parse(req.query.withDeleted); + z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.after); } catch (e) { const error = new Error(`Invalid request in territory get: ${e}`); error.status = 400; return next(error); } - const { limit, page, lastRefresh } = req.query; + const { limit, page, lastRefresh, after, withDeleted } = req.query; const query = { where: { organisation: req.user.organisation }, @@ -69,11 +72,19 @@ router.get( const total = await Territory.count(query); if (limit) query.limit = Number(limit); if (page) query.offset = Number(page) * limit; - if (lastRefresh) query.where.updatedAt = { [Op.gte]: new Date(Number(lastRefresh)) }; + if (lastRefresh) { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(lastRefresh)) } }]; + } + if (withDeleted === "true") query.paranoid = false; + if (after && !isNaN(Number(after)) && withDeleted === "true") { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(after)) } }, { deletedAt: { [Op.gte]: new Date(Number(after)) } }]; + } else if (after && !isNaN(Number(after))) { + query.where.updatedAt = { [Op.gte]: new Date(Number(after)) }; + } const data = await Territory.findAll({ ...query, - attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt"], + attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt", "deletedAt"], }); return res.status(200).send({ ok: true, data, hasMore: data.length === Number(limit), total }); }) @@ -129,6 +140,9 @@ router.delete( const territory = await Territory.findOne(query); if (!territory) return res.status(404).send({ ok: false, error: "Not Found" }); + territory.set({ encrypted: null, encryptedEntityKey: null }); + await territory.save(); + await territory.destroy(); res.status(200).send({ ok: true }); }) diff --git a/api/src/controllers/territoryObservation.js b/api/src/controllers/territoryObservation.js index 06592a016..d010b3185 100644 --- a/api/src/controllers/territoryObservation.js +++ b/api/src/controllers/territoryObservation.js @@ -39,6 +39,7 @@ router.post( organisation: data.organisation, createdAt: data.createdAt, updatedAt: data.updatedAt, + deletedAt: data.deletedAt, }, }); }) @@ -53,12 +54,14 @@ router.get( z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.limit); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.page); z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.lastRefresh); + z.optional(z.enum(["true", "false"])).parse(req.query.withDeleted); + z.optional(z.string().regex(positiveIntegerRegex)).parse(req.query.after); } catch (e) { const error = new Error(`Invalid request in observation get: ${e}`); error.status = 400; return next(error); } - const { limit, page, lastRefresh } = req.query; + const { limit, page, lastRefresh, after, withDeleted } = req.query; const query = { where: { organisation: req.user.organisation }, @@ -68,11 +71,19 @@ router.get( const total = await TerritoryObservation.count(query); if (limit) query.limit = Number(limit); if (page) query.offset = Number(page) * limit; - if (lastRefresh) query.where.updatedAt = { [Op.gte]: new Date(Number(lastRefresh)) }; + if (lastRefresh) { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(lastRefresh)) } }]; + } + if (withDeleted === "true") query.paranoid = false; + if (after && !isNaN(Number(after)) && withDeleted === "true") { + query.where[Op.or] = [{ updatedAt: { [Op.gte]: new Date(Number(after)) } }, { deletedAt: { [Op.gte]: new Date(Number(after)) } }]; + } else if (after && !isNaN(Number(after))) { + query.where.updatedAt = { [Op.gte]: new Date(Number(after)) }; + } const data = await TerritoryObservation.findAll({ ...query, - attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt"], + attributes: ["_id", "encrypted", "encryptedEntityKey", "organisation", "createdAt", "updatedAt", "deletedAt"], }); return res.status(200).send({ ok: true, data, hasMore: data.length === Number(limit), total }); }) @@ -116,6 +127,7 @@ router.put( organisation: newTerritoryObservation.organisation, createdAt: newTerritoryObservation.createdAt, updatedAt: newTerritoryObservation.updatedAt, + deletedAt: newTerritoryObservation.deletedAt, }, }); }) @@ -138,6 +150,9 @@ router.delete( let observation = await TerritoryObservation.findOne(query); if (!observation) return res.status(404).send({ ok: false, error: "Not Found" }); + observation.set({ encrypted: null, encryptedEntityKey: null }); + await observation.save(); + await observation.destroy(); res.status(200).send({ ok: true }); }) diff --git a/api/src/db/migrations/2022-04-25_soft-delete.js b/api/src/db/migrations/2022-04-25_soft-delete.js new file mode 100644 index 000000000..ce038f5df --- /dev/null +++ b/api/src/db/migrations/2022-04-25_soft-delete.js @@ -0,0 +1,40 @@ +const sequelize = require("../sequelize"); + +(async () => { + await sequelize.query(` + ALTER TABLE "mano"."Action" + ADD COLUMN IF NOT EXISTS "deletedAt" timestamp with time zone + `); + await sequelize.query(` + ALTER TABLE "mano"."Person" + ADD COLUMN IF NOT EXISTS "deletedAt" timestamp with time zone + `); + await sequelize.query(` + ALTER TABLE "mano"."Comment" + ADD COLUMN IF NOT EXISTS "deletedAt" timestamp with time zone + `); + await sequelize.query(` + ALTER TABLE "mano"."Place" + ADD COLUMN IF NOT EXISTS "deletedAt" timestamp with time zone + `); + await sequelize.query(` + ALTER TABLE "mano"."Passage" + ADD COLUMN IF NOT EXISTS "deletedAt" timestamp with time zone + `); + await sequelize.query(` + ALTER TABLE "mano"."RelPersonPlace" + ADD COLUMN IF NOT EXISTS "deletedAt" timestamp with time zone + `); + await sequelize.query(` + ALTER TABLE "mano"."Report" + ADD COLUMN IF NOT EXISTS "deletedAt" timestamp with time zone + `); + await sequelize.query(` + ALTER TABLE "mano"."Territory" + ADD COLUMN IF NOT EXISTS "deletedAt" timestamp with time zone + `); + await sequelize.query(` + ALTER TABLE "mano"."TerritoryObservation" + ADD COLUMN IF NOT EXISTS "deletedAt" timestamp with time zone + `); +})(); diff --git a/api/src/db/migrations/index.js b/api/src/db/migrations/index.js index ee45714be..d345c6998 100644 --- a/api/src/db/migrations/index.js +++ b/api/src/db/migrations/index.js @@ -17,3 +17,4 @@ require("./2022-03-16-migrating"); require("./2022-03-17_user_healthcare_professional.js"); require("./2022-03-21-debug-app-dashboard-user.js"); require("./2022-03-29_add_consultations_columns.js"); +require("./2022-04-25_soft-delete.js"); diff --git a/api/src/models/action.js b/api/src/models/action.js index f1e430d9b..b3fab8909 100644 --- a/api/src/models/action.js +++ b/api/src/models/action.js @@ -16,6 +16,6 @@ const schema = { encryptedEntityKey: { type: DataTypes.TEXT }, }; -Action.init(schema, { sequelize, modelName: "Action", freezeTableName: true }); +Action.init(schema, { sequelize, modelName: "Action", freezeTableName: true, timestamps: true, paranoid: true }); module.exports = Action; diff --git a/api/src/models/comment.js b/api/src/models/comment.js index 3049e9a9a..53b685006 100644 --- a/api/src/models/comment.js +++ b/api/src/models/comment.js @@ -11,6 +11,6 @@ const schema = { encryptedEntityKey: { type: DataTypes.TEXT }, }; -Comment.init(schema, { sequelize, modelName: "Comment", freezeTableName: true }); +Comment.init(schema, { sequelize, modelName: "Comment", freezeTableName: true, timestamps: true, paranoid: true }); module.exports = Comment; diff --git a/api/src/models/passage.js b/api/src/models/passage.js index a7b74f772..7cc1bba4c 100644 --- a/api/src/models/passage.js +++ b/api/src/models/passage.js @@ -11,6 +11,6 @@ const schema = { encryptedEntityKey: { type: DataTypes.TEXT }, }; -Passage.init(schema, { sequelize, modelName: "Passage", freezeTableName: true }); +Passage.init(schema, { sequelize, modelName: "Passage", freezeTableName: true, timestamps: true, paranoid: true }); module.exports = Passage; diff --git a/api/src/models/person.js b/api/src/models/person.js index 65b8ec1c4..96d6ff0e7 100644 --- a/api/src/models/person.js +++ b/api/src/models/person.js @@ -11,6 +11,6 @@ const schema = { encryptedEntityKey: { type: DataTypes.TEXT }, }; -Person.init(schema, { sequelize, modelName: "Person", freezeTableName: true }); +Person.init(schema, { sequelize, modelName: "Person", freezeTableName: true, timestamps: true, paranoid: true }); module.exports = Person; diff --git a/api/src/models/place.js b/api/src/models/place.js index 9923af8d8..3bc4d2dda 100644 --- a/api/src/models/place.js +++ b/api/src/models/place.js @@ -12,6 +12,6 @@ const schema = { class Place extends Model {} -Place.init(schema, { sequelize, modelName: "Place", freezeTableName: true }); +Place.init(schema, { sequelize, modelName: "Place", freezeTableName: true, timestamps: true, paranoid: true }); module.exports = Place; diff --git a/api/src/models/relPersonPlace.js b/api/src/models/relPersonPlace.js index bb85d8d21..7bb949459 100644 --- a/api/src/models/relPersonPlace.js +++ b/api/src/models/relPersonPlace.js @@ -10,6 +10,6 @@ const schema = { encryptedEntityKey: { type: DataTypes.TEXT }, }; -RelPersonPlace.init(schema, { sequelize, modelName: "RelPersonPlace", freezeTableName: true }); +RelPersonPlace.init(schema, { sequelize, modelName: "RelPersonPlace", freezeTableName: true, timestamps: true, paranoid: true }); module.exports = RelPersonPlace; diff --git a/api/src/models/report.js b/api/src/models/report.js index 226b91902..72a700231 100644 --- a/api/src/models/report.js +++ b/api/src/models/report.js @@ -10,6 +10,6 @@ const schema = { encryptedEntityKey: { type: DataTypes.TEXT }, }; -Report.init(schema, { sequelize, modelName: "Report", freezeTableName: true }); +Report.init(schema, { sequelize, modelName: "Report", freezeTableName: true, timestamps: true, paranoid: true }); module.exports = Report; diff --git a/api/src/models/territory.js b/api/src/models/territory.js index 6bf5227a0..6de8cb5b0 100644 --- a/api/src/models/territory.js +++ b/api/src/models/territory.js @@ -10,6 +10,6 @@ const schema = { encryptedEntityKey: { type: DataTypes.TEXT }, }; -Territory.init(schema, { sequelize, modelName: "Territory", freezeTableName: true }); +Territory.init(schema, { sequelize, modelName: "Territory", freezeTableName: true, timestamps: true, paranoid: true }); module.exports = Territory; diff --git a/api/src/models/territoryObservation.js b/api/src/models/territoryObservation.js index 64d9ff5b9..0a202131d 100644 --- a/api/src/models/territoryObservation.js +++ b/api/src/models/territoryObservation.js @@ -10,6 +10,6 @@ const schema = { encryptedEntityKey: { type: DataTypes.TEXT }, }; -TerritoryObservation.init(schema, { sequelize, modelName: "TerritoryObservation", freezeTableName: true }); +TerritoryObservation.init(schema, { sequelize, modelName: "TerritoryObservation", freezeTableName: true, timestamps: true, paranoid: true }); module.exports = TerritoryObservation; diff --git a/app/src/components/Loader.js b/app/src/components/Loader.js index 570565b1c..481f57893 100644 --- a/app/src/components/Loader.js +++ b/app/src/components/Loader.js @@ -55,7 +55,7 @@ const mergeItems = (oldItems, newItems) => { const Loader = () => { const [picture, setPicture] = useState([picture1, picture3, picture2][randomIntFromInterval(0, 2)]); - const [lastRefresh, setLastRefresh] = useStorage('last-refresh', 0); + const [lastRefresh, setLastRefresh] = useStorage('last-refresh--cache-version-2022-04-25', null); const [loading, setLoading] = useRecoilState(loadingState); const [progress, setProgress] = useRecoilState(progressState); const [fullScreen, setFullScreen] = useRecoilState(loaderFullScreenState); diff --git a/app/src/services/dataManagement.js b/app/src/services/dataManagement.js index 2d5899bad..c015022b7 100644 --- a/app/src/services/dataManagement.js +++ b/app/src/services/dataManagement.js @@ -5,6 +5,7 @@ export const mergeNewUpdatedData = (newData, oldData) => { const oldDataIds = oldData.map((p) => p._id); const updatedItems = newData.filter((p) => oldDataIds.includes(p._id)); const newItems = newData.filter((p) => !oldDataIds.includes(p._id)); + const deletedItemsIds = newData.filter((p) => !!p.deletedAt).map((p) => p._id); return [ ...newItems, ...oldData.map((person) => { @@ -12,7 +13,7 @@ export const mergeNewUpdatedData = (newData, oldData) => { if (updatedItem) return updatedItem; return person; }), - ]; + ].filter((p) => !deletedItemsIds.includes(p._id)); }; export const MMKV = new MMKVStorage.Loader().initialize(); @@ -28,12 +29,25 @@ export function clearCache() { } // Get data from cache or fetch from server. -export async function getData({ collectionName, data = [], isInitialization = false, setProgress = () => {}, setBatchData = null, lastRefresh = 0 }) { +export async function getData({ + collectionName, + data = [], + isInitialization = false, + setProgress = () => {}, + setBatchData = null, + lastRefresh = null, +}) { if (isInitialization) { data = (await MMKV.getMapAsync(collectionName)) || []; } - const response = await API.get({ path: `/${collectionName}`, batch: 1000, setProgress, query: { lastRefresh }, setBatchData }); + const response = await API.get({ + path: `/${collectionName}`, + batch: 1000, + setProgress, + query: { after: lastRefresh, withDeleted: Boolean(lastRefresh) }, + setBatchData, + }); if (!response.ok) throw { message: `Error getting ${collectionName} data`, response }; // avoid sending data if no new data, to avoid big useless `map` calculations in selectors diff --git a/dashboard/src/services/dataManagement.js b/dashboard/src/services/dataManagement.js index 3919ca887..4864a23f5 100644 --- a/dashboard/src/services/dataManagement.js +++ b/dashboard/src/services/dataManagement.js @@ -6,6 +6,8 @@ export const mergeNewUpdatedData = (newData, oldData) => { const oldDataIds = oldData.map((p) => p._id); const updatedItems = newData.filter((p) => oldDataIds.includes(p._id)); const newItems = newData.filter((p) => !oldDataIds.includes(p._id)); + const deletedItemsIds = newData.filter((p) => !!p.deletedAt).map((p) => p._id); + return [ ...newItems, ...oldData.map((person) => { @@ -13,7 +15,7 @@ export const mergeNewUpdatedData = (newData, oldData) => { if (updatedItem) return updatedItem; return person; }), - ]; + ].filter((p) => !deletedItemsIds.includes(p._id)); }; // export const useStorage = (key, defaultValue) => { @@ -37,9 +39,15 @@ export async function getData({ isInitialization = false, setProgress = () => {}, setBatchData = null, - lastRefresh = 0, + lastRefresh = null, }) { - const response = await API.get({ path: `/${collectionName}`, batch: 1000, setProgress, query: { lastRefresh }, setBatchData }); + const response = await API.get({ + path: `/${collectionName}`, + batch: 1000, + setProgress, + query: { after: lastRefresh, withDeleted: Boolean(lastRefresh) }, + setBatchData, + }); if (!response.ok) console.log({ message: `Error getting ${collectionName} data`, response }); if (response.ok && response.decryptedData && response.decryptedData.length) { data = mergeNewUpdatedData(response.decryptedData, data);