From 129f38fca83fa84717c7ba16857b315c25d33b9e Mon Sep 17 00:00:00 2001 From: Kenn Sippell Date: Mon, 16 Dec 2024 10:50:14 -0800 Subject: [PATCH] feat(#650): delete-contacts action (#652) This adds new action cht delete-contacts which recursively deletes all contacts and reports under a place. npx cht delete-contacts upload-docs --local -- --contacts=uuid1 --- src/fn/delete-contacts.js | 64 + .../hierarchy-operations/delete-hierarchy.js | 46 + src/lib/hierarchy-operations/index.js | 18 +- src/lib/hierarchy-operations/jsdocFolder.js | 10 + .../lineage-constraints.js | 39 +- test/fn/upload-docs.spec.js | 1 - .../hierarchy-operations.spec.js | 1065 +++++++++-------- 7 files changed, 717 insertions(+), 526 deletions(-) create mode 100644 src/fn/delete-contacts.js create mode 100644 src/lib/hierarchy-operations/delete-hierarchy.js diff --git a/src/fn/delete-contacts.js b/src/fn/delete-contacts.js new file mode 100644 index 00000000..724c9f4f --- /dev/null +++ b/src/fn/delete-contacts.js @@ -0,0 +1,64 @@ +const minimist = require('minimist'); +const path = require('path'); + +const environment = require('../lib/environment'); +const pouch = require('../lib/db'); +const { info } = require('../lib/log'); + +const HierarchyOperations = require('../lib/hierarchy-operations'); + +module.exports = { + requiresInstance: true, + execute: () => { + const args = parseExtraArgs(environment.pathToProject, environment.extraArgs); + const db = pouch(); + const options = { + docDirectoryPath: args.docDirectoryPath, + force: args.force, + disableUsers: args.disableUsers, + }; + return HierarchyOperations(db, options).delete(args.sourceIds); + } +}; + +// Parses extraArgs and asserts if required parameters are not present +const parseExtraArgs = (projectDir, extraArgs = []) => { + const args = minimist(extraArgs, { boolean: true }); + + const sourceIds = (args.contacts || args.contact || '') + .split(',') + .filter(id => id); + + if (sourceIds.length === 0) { + usage(); + throw Error('Action "delete-contacts" is missing required list of contacts to be deleted'); + } + + return { + sourceIds, + disableUsers: !!args['disable-users'], + docDirectoryPath: path.resolve(projectDir, args.docDirectoryPath || 'json_docs'), + force: !!args.force, + }; +}; + +const bold = text => `\x1b[1m${text}\x1b[0m`; +const usage = () => { + info(` +${bold('cht-conf\'s delete-contacts action')} +When combined with 'upload-docs' this action recursively deletes a contact and all of their descendant contacts and data. ${bold('This operation is permanent. It cannot be undone.')} + +${bold('USAGE')} +cht --local delete-contacts -- --contacts=, + +${bold('OPTIONS')} +--contacts=, (or --contact=,) + A comma delimited list of ids of contacts to be deleted. + +--disable-users + When flag is present, users at any deleted place will be permanently disabled. + +--docDirectoryPath= + Specifies the folder used to store the documents representing the changes in hierarchy. +`); +}; diff --git a/src/lib/hierarchy-operations/delete-hierarchy.js b/src/lib/hierarchy-operations/delete-hierarchy.js new file mode 100644 index 00000000..4d9f470c --- /dev/null +++ b/src/lib/hierarchy-operations/delete-hierarchy.js @@ -0,0 +1,46 @@ +const DataSource = require('./hierarchy-data-source'); +const JsDocs = require('./jsdocFolder'); +const lineageConstraints = require('./lineage-constraints'); +const { trace, info } = require('../log'); + +const prettyPrintDocument = doc => `'${doc.name}' (${doc._id})`; +async function deleteHierarchy(db, options, sourceIds) { + JsDocs.prepareFolder(options); + + const sourceDocs = await DataSource.getContactsByIds(db, sourceIds); + const constraints = await lineageConstraints(db, options); + for (const sourceId of sourceIds) { + const sourceDoc = sourceDocs[sourceId]; + trace(`Deleting descendants and reports under: ${prettyPrintDocument(sourceDoc)}`); + const descendantsAndSelf = await DataSource.getContactWithDescendants(db, sourceId); + + let affectedReportCount = 0; + for (const descendant of descendantsAndSelf) { + const toDeleteUsers = options.disableUsers && constraints.isPlace(descendant); + JsDocs.deleteDoc(options, descendant, toDeleteUsers); + affectedReportCount += await deleteReportsForContact(db, options, descendant); + } + + const affectedContactCount = descendantsAndSelf.length; + + info(`Staged updates to delete ${prettyPrintDocument(sourceDoc)}. ${affectedContactCount.length} contact(s) and ${affectedReportCount} report(s).`); + } +} + +async function deleteReportsForContact(db, options, contact) { + let skip = 0; + let reportBatch; + do { + reportBatch = await DataSource.getReportsForContacts(db, [], contact._id, skip); + + for (const report of reportBatch) { + JsDocs.deleteDoc(options, report); + } + + skip += reportBatch.length; + } while (reportBatch.length >= DataSource.BATCH_SIZE); + + return skip; +} + +module.exports = deleteHierarchy; diff --git a/src/lib/hierarchy-operations/index.js b/src/lib/hierarchy-operations/index.js index 036ac676..1d40685d 100644 --- a/src/lib/hierarchy-operations/index.js +++ b/src/lib/hierarchy-operations/index.js @@ -1,10 +1,10 @@ +const DataSource = require('./hierarchy-data-source'); +const deleteHierarchy = require('./delete-hierarchy'); +const JsDocs = require('./jsdocFolder'); const lineageManipulation = require('./lineage-manipulation'); const LineageConstraints = require('./lineage-constraints'); const { trace, info } = require('../log'); -const JsDocs = require('./jsdocFolder'); -const DataSource = require('./hierarchy-data-source'); - async function moveHierarchy(db, options, sourceIds, destinationId) { JsDocs.prepareFolder(options); trace(`Fetching contact details: ${destinationId}`); @@ -30,12 +30,8 @@ async function moveHierarchy(db, options, sourceIds, destinationId) { await constraints.assertNoPrimaryContactViolations(sourceDoc, destinationDoc, descendantsAndSelf); if (options.merge) { - JsDocs.writeDoc(options, { - _id: sourceDoc._id, - _rev: sourceDoc._rev, - _deleted: true, - cht_disable_linked_users: !!options.disableUsers, - }); + const toDeleteUsers = options.disableUsers && constraints.isPlace(sourceDoc); + JsDocs.deleteDoc(options, sourceDoc, toDeleteUsers); } const prettyPrintDocument = doc => `'${doc.name}' (${doc._id})`; @@ -177,11 +173,11 @@ function replaceLineageInContacts(moveContext) { .filter(Boolean); } -module.exports = (db, options) => { +module.exports = (db, options = {}) => { return { HIERARCHY_ROOT: DataSource.HIERARCHY_ROOT, move: (sourceIds, destinationId) => moveHierarchy(db, { ...options, merge: false }, sourceIds, destinationId), merge: (sourceIds, destinationId) => moveHierarchy(db, { ...options, merge: true }, sourceIds, destinationId), + delete: async (sourceIds) => deleteHierarchy(db, options, sourceIds), }; }; - diff --git a/src/lib/hierarchy-operations/jsdocFolder.js b/src/lib/hierarchy-operations/jsdocFolder.js index b24358ac..8c98967b 100644 --- a/src/lib/hierarchy-operations/jsdocFolder.js +++ b/src/lib/hierarchy-operations/jsdocFolder.js @@ -30,7 +30,17 @@ function deleteAfterConfirmation(docDirectoryPath) { fs.deleteFilesInFolder(docDirectoryPath); } +function deleteDoc(options, doc, disableUsers) { + writeDoc(options, { + _id: doc._id, + _rev: doc._rev, + _deleted: true, + cht_disable_linked_users: !!disableUsers, + }); +} + module.exports = { + deleteDoc, prepareFolder, writeDoc, }; diff --git a/src/lib/hierarchy-operations/lineage-constraints.js b/src/lib/hierarchy-operations/lineage-constraints.js index 971c4417..e2079504 100644 --- a/src/lib/hierarchy-operations/lineage-constraints.js +++ b/src/lib/hierarchy-operations/lineage-constraints.js @@ -5,7 +5,7 @@ const { trace } = log; const lineageManipulation = require('./lineage-manipulation'); module.exports = async (db, options) => { - const mapTypeToAllowedParents = await fetchAllowedParents(db); + const contactTypeInfo = await fetchContactTypeInfo(db); return { assertNoPrimaryContactViolations: async (sourceDoc, destinationDoc, descendantDocs) => { @@ -20,7 +20,7 @@ module.exports = async (db, options) => { const commonViolations = getCommonViolations(sourceDoc, destinationDoc); const specificViolation = options.merge ? getMergeViolations(sourceDoc, destinationDoc) - : getMovingViolations(mapTypeToAllowedParents, sourceDoc, destinationDoc); + : getMovingViolations(contactTypeInfo, sourceDoc, destinationDoc); const hierarchyError = commonViolations || specificViolation; if (hierarchyError) { @@ -41,7 +41,12 @@ module.exports = async (db, options) => { throw Error(`Unable to move two documents from the same lineage: '${doc._id}' and '${violatingParentId}'`); } }); - } + }, + + isPlace: (contact) => { + const contactType = getContactType(contact); + return !contactTypeInfo[contactType]?.person; + }, }; }; @@ -49,22 +54,22 @@ module.exports = async (db, options) => { Enforce the list of allowed parents for each contact type Ensure we are not creating a circular hierarchy */ -const getMovingViolations = (mapTypeToAllowedParents, sourceDoc, destinationDoc) => { - const contactTypeError = getMovingContactTypeError(mapTypeToAllowedParents, sourceDoc, destinationDoc); +const getMovingViolations = (contactTypeInfo, sourceDoc, destinationDoc) => { + const contactTypeError = getMovingContactTypeError(contactTypeInfo, sourceDoc, destinationDoc); const circularHierarchyError = findCircularHierarchyErrors(sourceDoc, destinationDoc); return contactTypeError || circularHierarchyError; }; -function getMovingContactTypeError(mapTypeToAllowedParents, sourceDoc, destinationDoc) { +function getMovingContactTypeError(contactTypeInfo, sourceDoc, destinationDoc) { const sourceContactType = getContactType(sourceDoc); const destinationType = getContactType(destinationDoc); - const rulesForContact = mapTypeToAllowedParents[sourceContactType]; - if (!rulesForContact) { + const parentsForContactType = contactTypeInfo[sourceContactType]?.parents; + if (!parentsForContactType) { return `cannot move contact with unknown type '${sourceContactType}'`; } - const isPermittedMoveToRoot = !destinationDoc && rulesForContact.length === 0; - if (!isPermittedMoveToRoot && !rulesForContact.includes(destinationType)) { + const isPermittedMoveToRoot = !destinationDoc && parentsForContactType.length === 0; + if (!isPermittedMoveToRoot && !parentsForContactType.includes(destinationType)) { return `contacts of type '${sourceContactType}' cannot have parent of type '${destinationType}'`; } } @@ -139,9 +144,9 @@ const getPrimaryContactViolations = async (db, contactDoc, destinationDoc, desce return descendantDocs.find(descendant => primaryContactIds.some(primaryId => descendant._id === primaryId)); }; -const getContactType = doc => doc && (doc.type === 'contact' ? doc.contact_type : doc.type); +const getContactType = doc => doc?.type === 'contact' ? doc?.contact_type : doc?.type; -async function fetchAllowedParents(db) { +async function fetchContactTypeInfo(db) { try { const { settings: { contact_types } } = await db.get('settings'); @@ -150,7 +155,7 @@ async function fetchAllowedParents(db) { const parentDict = {}; contact_types .filter(Boolean) - .forEach(({ id, parents }) => parentDict[id] = parents); + .forEach(({ id, person, parents }) => parentDict[id] = { parents, person: !!person }); return parentDict; } } catch (err) { @@ -161,10 +166,10 @@ async function fetchAllowedParents(db) { trace('Default hierarchy constraints will be enforced.'); return { - district_hospital: [], - health_center: ['district_hospital'], - clinic: ['health_center'], - person: ['district_hospital', 'health_center', 'clinic'], + district_hospital: { parents: [] }, + health_center: { parents: ['district_hospital'] }, + clinic: { parents: ['health_center'] }, + person: { parents: ['district_hospital', 'health_center', 'clinic'], person: true }, }; } diff --git a/test/fn/upload-docs.spec.js b/test/fn/upload-docs.spec.js index 91297639..2824996b 100644 --- a/test/fn/upload-docs.spec.js +++ b/test/fn/upload-docs.spec.js @@ -1,6 +1,5 @@ const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); - const rewire = require('rewire'); const sinon = require('sinon'); diff --git a/test/lib/hierarchy-operations/hierarchy-operations.spec.js b/test/lib/hierarchy-operations/hierarchy-operations.spec.js index 34429c9e..402771e9 100644 --- a/test/lib/hierarchy-operations/hierarchy-operations.spec.js +++ b/test/lib/hierarchy-operations/hierarchy-operations.spec.js @@ -4,8 +4,8 @@ const rewire = require('rewire'); const sinon = require('sinon'); const { mockReport, mockHierarchy, parentsToLineage } = require('../../mock-hierarchies'); -const JsDocs = rewire('../../../src/lib/hierarchy-operations/jsdocFolder.js'); -const DataSource = rewire('../../../src/lib/hierarchy-operations/hierarchy-data-source.js'); +const JsDocs = rewire('../../../src/lib/hierarchy-operations/jsdocFolder'); +const DataSource = rewire('../../../src/lib/hierarchy-operations/hierarchy-data-source'); const PouchDB = require('pouchdb-core'); @@ -13,11 +13,16 @@ chai.use(chaiAsPromised); PouchDB.plugin(require('pouchdb-adapter-memory')); PouchDB.plugin(require('pouchdb-mapreduce')); -const { assert, expect } = chai; +const { expect } = chai; + +const HierarchyOperations = rewire('../../../src/lib/hierarchy-operations'); +const deleteHierarchy = rewire('../../../src/lib/hierarchy-operations/delete-hierarchy'); -const HierarchyOperations = rewire('../../../src/lib/hierarchy-operations/index.js'); HierarchyOperations.__set__('JsDocs', JsDocs); HierarchyOperations.__set__('DataSource', DataSource); +deleteHierarchy.__set__('JsDocs', JsDocs); +deleteHierarchy.__set__('DataSource', DataSource); +HierarchyOperations.__set__('deleteHierarchy', deleteHierarchy); const contacts_by_depth = { // eslint-disable-next-line quotes @@ -92,307 +97,579 @@ describe('hierarchy-operations', () => { }); JsDocs.writeDoc = (docDirectoryPath, doc) => writtenDocs.push(doc); + JsDocs.__set__('writeDoc', JsDocs.writeDoc); + JsDocs.prepareFolder = () => {}; writtenDocs.length = 0; }); afterEach(async () => pouchDb.destroy()); - it('move health_center_1 to district_2', async () => { - await HierarchyOperations(pouchDb).move(['health_center_1'], 'district_2'); + describe('move', () => { + it('move health_center_1 to district_2', async () => { + await HierarchyOperations(pouchDb).move(['health_center_1'], 'district_2'); - expect(getWrittenDoc('health_center_1_contact')).to.deep.eq({ - _id: 'health_center_1_contact', - type: 'person', - parent: parentsToLineage('health_center_1', 'district_2'), - }); + expect(getWrittenDoc('health_center_1_contact')).to.deep.eq({ + _id: 'health_center_1_contact', + type: 'person', + parent: parentsToLineage('health_center_1', 'district_2'), + }); - expect(getWrittenDoc('health_center_1')).to.deep.eq({ - _id: 'health_center_1', - type: 'health_center', - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), - parent: parentsToLineage('district_2'), - }); + expect(getWrittenDoc('health_center_1')).to.deep.eq({ + _id: 'health_center_1', + type: 'health_center', + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), + parent: parentsToLineage('district_2'), + }); - expect(getWrittenDoc('clinic_1')).to.deep.eq({ - _id: 'clinic_1', - type: 'clinic', - contact: parentsToLineage('clinic_1_contact', 'clinic_1', 'health_center_1', 'district_2'), - parent: parentsToLineage('health_center_1', 'district_2'), - }); + expect(getWrittenDoc('clinic_1')).to.deep.eq({ + _id: 'clinic_1', + type: 'clinic', + contact: parentsToLineage('clinic_1_contact', 'clinic_1', 'health_center_1', 'district_2'), + parent: parentsToLineage('health_center_1', 'district_2'), + }); + + expect(getWrittenDoc('patient_1')).to.deep.eq({ + _id: 'patient_1', + type: 'person', + parent: parentsToLineage('clinic_1', 'health_center_1', 'district_2'), + }); - expect(getWrittenDoc('patient_1')).to.deep.eq({ - _id: 'patient_1', - type: 'person', - parent: parentsToLineage('clinic_1', 'health_center_1', 'district_2'), + expect(getWrittenDoc('report_1')).to.deep.eq({ + _id: 'report_1', + form: 'foo', + type: 'data_record', + fields: {}, + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), + }); }); - expect(getWrittenDoc('report_1')).to.deep.eq({ - _id: 'report_1', - form: 'foo', - type: 'data_record', - fields: {}, - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), + it('move root to health_center_1', async () => { + const actual = HierarchyOperations(pouchDb).move(['root'], 'health_center_1'); + await expect(actual).to.eventually.be.rejectedWith(`'root' could not be found`); }); - }); - it('move root to health_center_1', async () => { - const actual = HierarchyOperations(pouchDb).move(['root'], 'health_center_1'); - await expect(actual).to.eventually.be.rejectedWith(`'root' could not be found`); - }); - - it('move health_center_1 to root', async () => { - sinon.spy(pouchDb, 'query'); + it('move health_center_1 to root', async () => { + sinon.spy(pouchDb, 'query'); - await updateHierarchyRules([{ id: 'health_center', parents: [] }]); + await updateHierarchyRules([{ id: 'health_center', parents: [] }]); - await HierarchyOperations(pouchDb).move(['health_center_1'], 'root'); + await HierarchyOperations(pouchDb).move(['health_center_1'], 'root'); - expect(getWrittenDoc('health_center_1_contact')).to.deep.eq({ - _id: 'health_center_1_contact', - type: 'person', - parent: parentsToLineage('health_center_1'), - }); + expect(getWrittenDoc('health_center_1_contact')).to.deep.eq({ + _id: 'health_center_1_contact', + type: 'person', + parent: parentsToLineage('health_center_1'), + }); - expect(getWrittenDoc('health_center_1')).to.deep.eq({ - _id: 'health_center_1', - type: 'health_center', - contact: parentsToLineage('health_center_1_contact', 'health_center_1'), - parent: parentsToLineage(), - }); + expect(getWrittenDoc('health_center_1')).to.deep.eq({ + _id: 'health_center_1', + type: 'health_center', + contact: parentsToLineage('health_center_1_contact', 'health_center_1'), + parent: parentsToLineage(), + }); - expect(getWrittenDoc('clinic_1')).to.deep.eq({ - _id: 'clinic_1', - type: 'clinic', - contact: parentsToLineage('clinic_1_contact', 'clinic_1', 'health_center_1'), - parent: parentsToLineage('health_center_1'), - }); + expect(getWrittenDoc('clinic_1')).to.deep.eq({ + _id: 'clinic_1', + type: 'clinic', + contact: parentsToLineage('clinic_1_contact', 'clinic_1', 'health_center_1'), + parent: parentsToLineage('health_center_1'), + }); - expect(getWrittenDoc('patient_1')).to.deep.eq({ - _id: 'patient_1', - type: 'person', - parent: parentsToLineage('clinic_1', 'health_center_1'), - }); + expect(getWrittenDoc('patient_1')).to.deep.eq({ + _id: 'patient_1', + type: 'person', + parent: parentsToLineage('clinic_1', 'health_center_1'), + }); - expect(getWrittenDoc('report_1')).to.deep.eq({ - _id: 'report_1', - form: 'foo', - type: 'data_record', - fields: {}, - contact: parentsToLineage('health_center_1_contact', 'health_center_1'), - }); + expect(getWrittenDoc('report_1')).to.deep.eq({ + _id: 'report_1', + form: 'foo', + type: 'data_record', + fields: {}, + contact: parentsToLineage('health_center_1_contact', 'health_center_1'), + }); - const contactIdsKeys = [ - ['contact:clinic_1'], - ['contact:clinic_1_contact'], - ['contact:health_center_1'], - ['contact:health_center_1_contact'], - ['contact:patient_1'] - ]; - expect(pouchDb.query.callCount).to.equal(2); - expect(pouchDb.query.args).to.deep.equal([ - ['medic/contacts_by_depth', { key: ['health_center_1'], include_docs: true, group_level: undefined, skip: undefined, limit: undefined }], - ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 10000, skip: 0, group_level: undefined }], - ]); - }); + const contactIdsKeys = [ + ['contact:clinic_1'], + ['contact:clinic_1_contact'], + ['contact:health_center_1'], + ['contact:health_center_1_contact'], + ['contact:patient_1'] + ]; + expect(pouchDb.query.callCount).to.equal(2); + expect(pouchDb.query.args).to.deep.equal([ + ['medic/contacts_by_depth', { key: ['health_center_1'], include_docs: true, group_level: undefined, skip: undefined, limit: undefined }], + ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 10000, skip: 0, group_level: undefined }], + ]); + }); - it('move district_1 from root', async () => { - await updateHierarchyRules([{ id: 'district_hospital', parents: ['district_hospital'] }]); + it('move district_1 from root', async () => { + await updateHierarchyRules([{ id: 'district_hospital', parents: ['district_hospital'] }]); - await HierarchyOperations(pouchDb).move(['district_1'], 'district_2'); + await HierarchyOperations(pouchDb).move(['district_1'], 'district_2'); - expect(getWrittenDoc('district_1')).to.deep.eq({ - _id: 'district_1', - type: 'district_hospital', - contact: parentsToLineage('district_1_contact', 'district_1', 'district_2'), - parent: parentsToLineage('district_2'), - }); + expect(getWrittenDoc('district_1')).to.deep.eq({ + _id: 'district_1', + type: 'district_hospital', + contact: parentsToLineage('district_1_contact', 'district_1', 'district_2'), + parent: parentsToLineage('district_2'), + }); - expect(getWrittenDoc('health_center_1_contact')).to.deep.eq({ - _id: 'health_center_1_contact', - type: 'person', - parent: parentsToLineage('health_center_1', 'district_1', 'district_2'), - }); + expect(getWrittenDoc('health_center_1_contact')).to.deep.eq({ + _id: 'health_center_1_contact', + type: 'person', + parent: parentsToLineage('health_center_1', 'district_1', 'district_2'), + }); - expect(getWrittenDoc('health_center_1')).to.deep.eq({ - _id: 'health_center_1', - type: 'health_center', - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1', 'district_2'), - parent: parentsToLineage('district_1', 'district_2'), - }); + expect(getWrittenDoc('health_center_1')).to.deep.eq({ + _id: 'health_center_1', + type: 'health_center', + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1', 'district_2'), + parent: parentsToLineage('district_1', 'district_2'), + }); - expect(getWrittenDoc('clinic_1')).to.deep.eq({ - _id: 'clinic_1', - type: 'clinic', - contact: parentsToLineage('clinic_1_contact', 'clinic_1', 'health_center_1', 'district_1', 'district_2'), - parent: parentsToLineage('health_center_1', 'district_1', 'district_2'), - }); + expect(getWrittenDoc('clinic_1')).to.deep.eq({ + _id: 'clinic_1', + type: 'clinic', + contact: parentsToLineage('clinic_1_contact', 'clinic_1', 'health_center_1', 'district_1', 'district_2'), + parent: parentsToLineage('health_center_1', 'district_1', 'district_2'), + }); - expect(getWrittenDoc('patient_1')).to.deep.eq({ - _id: 'patient_1', - type: 'person', - parent: parentsToLineage('clinic_1', 'health_center_1', 'district_1', 'district_2'), - }); + expect(getWrittenDoc('patient_1')).to.deep.eq({ + _id: 'patient_1', + type: 'person', + parent: parentsToLineage('clinic_1', 'health_center_1', 'district_1', 'district_2'), + }); - expect(getWrittenDoc('report_1')).to.deep.eq({ - _id: 'report_1', - form: 'foo', - type: 'data_record', - fields: {}, - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1', 'district_2'), + expect(getWrittenDoc('report_1')).to.deep.eq({ + _id: 'report_1', + form: 'foo', + type: 'data_record', + fields: {}, + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1', 'district_2'), + }); }); - }); - it('move district_1 to flexible hierarchy parent', async () => { - await pouchDb.put({ - _id: `county_1`, - type: 'contact', - contact_type: 'county', - }); + it('move district_1 to flexible hierarchy parent', async () => { + await pouchDb.put({ + _id: `county_1`, + type: 'contact', + contact_type: 'county', + }); - await updateHierarchyRules([ - { id: 'county', parents: [] }, - { id: 'district_hospital', parents: ['county'] }, - ]); + await updateHierarchyRules([ + { id: 'county', parents: [] }, + { id: 'district_hospital', parents: ['county'] }, + ]); - await HierarchyOperations(pouchDb).move(['district_1'], 'county_1'); + await HierarchyOperations(pouchDb).move(['district_1'], 'county_1'); - expect(getWrittenDoc('health_center_1_contact')).to.deep.eq({ - _id: 'health_center_1_contact', - type: 'person', - parent: parentsToLineage('health_center_1', 'district_1', 'county_1'), - }); + expect(getWrittenDoc('health_center_1_contact')).to.deep.eq({ + _id: 'health_center_1_contact', + type: 'person', + parent: parentsToLineage('health_center_1', 'district_1', 'county_1'), + }); - expect(getWrittenDoc('health_center_1')).to.deep.eq({ - _id: 'health_center_1', - type: 'health_center', - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1', 'county_1'), - parent: parentsToLineage('district_1', 'county_1'), - }); + expect(getWrittenDoc('health_center_1')).to.deep.eq({ + _id: 'health_center_1', + type: 'health_center', + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1', 'county_1'), + parent: parentsToLineage('district_1', 'county_1'), + }); - expect(getWrittenDoc('clinic_1')).to.deep.eq({ - _id: 'clinic_1', - type: 'clinic', - contact: parentsToLineage('clinic_1_contact', 'clinic_1', 'health_center_1', 'district_1', 'county_1'), - parent: parentsToLineage('health_center_1', 'district_1', 'county_1'), - }); + expect(getWrittenDoc('clinic_1')).to.deep.eq({ + _id: 'clinic_1', + type: 'clinic', + contact: parentsToLineage('clinic_1_contact', 'clinic_1', 'health_center_1', 'district_1', 'county_1'), + parent: parentsToLineage('health_center_1', 'district_1', 'county_1'), + }); - expect(getWrittenDoc('patient_1')).to.deep.eq({ - _id: 'patient_1', - type: 'person', - parent: parentsToLineage('clinic_1', 'health_center_1', 'district_1', 'county_1'), - }); + expect(getWrittenDoc('patient_1')).to.deep.eq({ + _id: 'patient_1', + type: 'person', + parent: parentsToLineage('clinic_1', 'health_center_1', 'district_1', 'county_1'), + }); - expect(getWrittenDoc('report_1')).to.deep.eq({ - _id: 'report_1', - form: 'foo', - type: 'data_record', - fields: {}, - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1', 'county_1'), + expect(getWrittenDoc('report_1')).to.deep.eq({ + _id: 'report_1', + form: 'foo', + type: 'data_record', + fields: {}, + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1', 'county_1'), + }); }); - }); - it('moves flexible hierarchy contact to flexible hierarchy parent', async () => { - await updateHierarchyRules([ - { id: 'county', parents: [] }, - { id: 'subcounty', parents: ['county'] }, - { id: 'focal', parents: ['county', 'subcounty'], person: true } - ]); + it('moves flexible hierarchy contact to flexible hierarchy parent', async () => { + await updateHierarchyRules([ + { id: 'county', parents: [] }, + { id: 'subcounty', parents: ['county'] }, + { id: 'focal', parents: ['county', 'subcounty'], person: true } + ]); - await pouchDb.bulkDocs([ - { _id: `county`, type: 'contact', contact_type: 'county' }, - { _id: `subcounty`, type: 'contact', contact_type: 'subcounty', parent: { _id: 'county' } }, - { _id: `focal`, type: 'contact', contact_type: 'focal', parent: { _id: 'county' } }, - ]); + await pouchDb.bulkDocs([ + { _id: `county`, type: 'contact', contact_type: 'county' }, + { _id: `subcounty`, type: 'contact', contact_type: 'subcounty', parent: { _id: 'county' } }, + { _id: `focal`, type: 'contact', contact_type: 'focal', parent: { _id: 'county' } }, + ]); - await mockReport(pouchDb, { - id: 'report_focal', - creatorId: 'focal', - }); + await mockReport(pouchDb, { + id: 'report_focal', + creatorId: 'focal', + }); - await HierarchyOperations(pouchDb).move(['focal'], 'subcounty'); + await HierarchyOperations(pouchDb).move(['focal'], 'subcounty'); - expect(getWrittenDoc('focal')).to.deep.eq({ - _id: 'focal', - type: 'contact', - contact_type: 'focal', - parent: parentsToLineage('subcounty', 'county'), - }); + expect(getWrittenDoc('focal')).to.deep.eq({ + _id: 'focal', + type: 'contact', + contact_type: 'focal', + parent: parentsToLineage('subcounty', 'county'), + }); - expect(getWrittenDoc('report_focal')).to.deep.eq({ - _id: 'report_focal', - form: 'foo', - type: 'data_record', - fields: {}, - contact: parentsToLineage('focal', 'subcounty', 'county'), + expect(getWrittenDoc('report_focal')).to.deep.eq({ + _id: 'report_focal', + form: 'foo', + type: 'data_record', + fields: {}, + contact: parentsToLineage('focal', 'subcounty', 'county'), + }); }); - }); - it('moving primary contact updates parents', async () => { - await mockHierarchy(pouchDb, { - t_district_1: { - t_health_center_1: { - t_clinic_1: { - t_patient_1: {}, + it('moving primary contact updates parents', async () => { + await mockHierarchy(pouchDb, { + t_district_1: { + t_health_center_1: { + t_clinic_1: { + t_patient_1: {}, + }, + t_clinic_2: { + t_patient_2: {}, + } }, - t_clinic_2: { - t_patient_2: {}, - } }, - }, - }); + }); - const patient1Lineage = parentsToLineage('t_patient_1', 't_clinic_1', 't_health_center_1', 't_district_1'); - await upsert('t_health_center_1', { - type: 'health_center', - contact: patient1Lineage, - parent: parentsToLineage('t_district_1'), - }); + const patient1Lineage = parentsToLineage('t_patient_1', 't_clinic_1', 't_health_center_1', 't_district_1'); + await upsert('t_health_center_1', { + type: 'health_center', + contact: patient1Lineage, + parent: parentsToLineage('t_district_1'), + }); + + await upsert('t_district_1', { + type: 'district_hospital', + contact: patient1Lineage, + parent: parentsToLineage(), + }); + + await HierarchyOperations(pouchDb).move(['t_patient_1'], 't_clinic_2'); + + expect(getWrittenDoc('t_health_center_1')).to.deep.eq({ + _id: 't_health_center_1', + type: 'health_center', + contact: parentsToLineage('t_patient_1', 't_clinic_2', 't_health_center_1', 't_district_1'), + parent: parentsToLineage('t_district_1'), + }); + + expect(getWrittenDoc('t_district_1')).to.deep.eq({ + _id: 't_district_1', + type: 'district_hospital', + contact: parentsToLineage('t_patient_1', 't_clinic_2', 't_health_center_1', 't_district_1'), + }); - await upsert('t_district_1', { - type: 'district_hospital', - contact: patient1Lineage, - parent: parentsToLineage(), + expectWrittenDocs(['t_patient_1', 't_district_1', 't_health_center_1']); }); - await HierarchyOperations(pouchDb).move(['t_patient_1'], 't_clinic_2'); + // We don't want lineage { id, parent: '' } to result from district_hospitals which have parent: '' + it('district_hospital with empty string parent is not preserved', async () => { + await upsert('district_2', { parent: '', type: 'district_hospital' }); + await HierarchyOperations(pouchDb).move(['health_center_1'], 'district_2'); - expect(getWrittenDoc('t_health_center_1')).to.deep.eq({ - _id: 't_health_center_1', - type: 'health_center', - contact: parentsToLineage('t_patient_1', 't_clinic_2', 't_health_center_1', 't_district_1'), - parent: parentsToLineage('t_district_1'), + expect(getWrittenDoc('health_center_1')).to.deep.eq({ + _id: 'health_center_1', + type: 'health_center', + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), + parent: parentsToLineage('district_2'), + }); }); - expect(getWrittenDoc('t_district_1')).to.deep.eq({ - _id: 't_district_1', - type: 'district_hospital', - contact: parentsToLineage('t_patient_1', 't_clinic_2', 't_health_center_1', 't_district_1'), + it('documents should be minified', async () => { + await updateHierarchyRules([{ id: 'clinic', parents: ['district_hospital'] }]); + const patient = { + parent: parentsToLineage('clinic_1', 'health_center_1', 'district_1'), + type: 'person', + important: true, + }; + const clinic = { + parent: parentsToLineage('health_center_1', 'district_1'), + type: 'clinic', + important: true, + }; + patient.parent.important = false; + clinic.parent.parent.important = false; + + await upsert('clinic_1', clinic); + await upsert('patient_1', patient); + + await HierarchyOperations(pouchDb).move(['clinic_1'], 'district_2'); + + expect(getWrittenDoc('clinic_1')).to.deep.eq({ + _id: 'clinic_1', + type: 'clinic', + important: true, + parent: parentsToLineage('district_2'), + }); + expect(getWrittenDoc('patient_1')).to.deep.eq({ + _id: 'patient_1', + type: 'person', + important: true, + parent: parentsToLineage('clinic_1', 'district_2'), + }); + }); + + it('cannot create circular hierarchy', async () => { + // even if the hierarchy rules allow it + await updateHierarchyRules([{ id: 'health_center', parents: ['clinic'] }]); + const actual = HierarchyOperations(pouchDb).move(['health_center_1'], 'clinic_1'); + await expect(actual).to.eventually.be.rejectedWith('circular'); + }); + + it('throw if parent does not exist', async () => { + const actual = HierarchyOperations(pouchDb).move(['clinic_1'], 'dne_parent_id'); + await expect(actual).to.eventually.rejectedWith('could not be found'); + }); + + it('throw when altering same lineage', async () => { + const actual = HierarchyOperations(pouchDb).move(['patient_1', 'health_center_1'], 'district_2'); + await expect(actual).to.eventually.rejectedWith('same lineage'); + }); + + it('throw if contact_id is not a contact', async () => { + const actual = HierarchyOperations(pouchDb).move(['report_1'], 'clinic_1'); + await expect(actual).to.eventually.rejectedWith('unknown type'); + }); + + it('throw if moving primary contact of parent', async () => { + const actual = HierarchyOperations(pouchDb).move(['clinic_1_contact'], 'district_1'); + await expect(actual).to.eventually.rejectedWith('primary contact'); + }); + + it('throw if setting parent to self', async () => { + await updateHierarchyRules([{ id: 'clinic', parents: ['clinic'] }]); + const actual = HierarchyOperations(pouchDb).move(['clinic_1'], 'clinic_1'); + await expect(actual).to.eventually.rejectedWith('itself'); + }); + + it('throw when moving place to unconfigured parent', async () => { + await updateHierarchyRules([{ id: 'district_hospital', parents: [] }]); + const actual = HierarchyOperations(pouchDb).move(['district_1'], 'district_2'); + await expect(actual).to.eventually.rejectedWith('parent of type'); }); - expectWrittenDocs(['t_patient_1', 't_district_1', 't_health_center_1']); - }); + it('throw if source does not exist', async () => { + const nonExistentId = 'dne_parent_id'; + const actual = HierarchyOperations(pouchDb).move(['health_center_1', nonExistentId], 'district_2'); + await expect(actual).to.eventually.rejectedWith(`Contact with id '${nonExistentId}' could not be found.`); + }); - // We don't want lineage { id, parent: '' } to result from district_hospitals which have parent: '' - it('district_hospital with empty string parent is not preserved', async () => { - await upsert('district_2', { parent: '', type: 'district_hospital' }); - await HierarchyOperations(pouchDb).move(['health_center_1'], 'district_2'); + it('throw if ancestor does not exist', async () => { + const sourceId = 'health_center_1'; + await upsert(sourceId, { + type: 'health_center', + name: 'no parent', + parent: parentsToLineage('dne'), + }); - expect(getWrittenDoc('health_center_1')).to.deep.eq({ - _id: 'health_center_1', - type: 'health_center', - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), - parent: parentsToLineage('district_2'), + const actual = HierarchyOperations(pouchDb).move([sourceId], 'district_2'); + await expect(actual).to.eventually.rejectedWith( + `(${sourceId}) has parent id(s) 'dne' which could not be found.` + ); + }); + + describe('batching works as expected', () => { + const initialBatchSize = DataSource.BATCH_SIZE; + beforeEach(async () => { + await mockReport(pouchDb, { + id: 'report_2', + creatorId: 'health_center_1_contact', + }); + + await mockReport(pouchDb, { + id: 'report_3', + creatorId: 'health_center_1_contact', + }); + + await mockReport(pouchDb, { + id: 'report_4', + creatorId: 'health_center_1_contact', + }); + }); + + afterEach(() => { + DataSource.BATCH_SIZE = initialBatchSize; + DataSource.__set__('BATCH_SIZE', initialBatchSize); + }); + + it('move health_center_1 to district_2 in batches of 1', async () => { + DataSource.__set__('BATCH_SIZE', 1); + DataSource.BATCH_SIZE = 1; + sinon.spy(pouchDb, 'query'); + + await HierarchyOperations(pouchDb).move(['health_center_1'], 'district_2'); + + expect(getWrittenDoc('health_center_1_contact')).to.deep.eq({ + _id: 'health_center_1_contact', + type: 'person', + parent: parentsToLineage('health_center_1', 'district_2'), + }); + + expect(getWrittenDoc('health_center_1')).to.deep.eq({ + _id: 'health_center_1', + type: 'health_center', + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), + parent: parentsToLineage('district_2'), + }); + + expect(getWrittenDoc('clinic_1')).to.deep.eq({ + _id: 'clinic_1', + type: 'clinic', + contact: parentsToLineage('clinic_1_contact', 'clinic_1', 'health_center_1', 'district_2'), + parent: parentsToLineage('health_center_1', 'district_2'), + }); + + expect(getWrittenDoc('patient_1')).to.deep.eq({ + _id: 'patient_1', + type: 'person', + parent: parentsToLineage('clinic_1', 'health_center_1', 'district_2'), + }); + + expect(getWrittenDoc('report_1')).to.deep.eq({ + _id: 'report_1', + form: 'foo', + type: 'data_record', + fields: {}, + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), + }); + + expect(getWrittenDoc('report_2')).to.deep.eq({ + _id: 'report_2', + form: 'foo', + type: 'data_record', + fields: {}, + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), + }); + + expect(getWrittenDoc('report_3')).to.deep.eq({ + _id: 'report_3', + form: 'foo', + type: 'data_record', + fields: {}, + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), + }); + + expect(pouchDb.query.callCount).to.deep.equal(6); + + const contactIdsKeys = [ + ['contact:clinic_1'], + ['contact:clinic_1_contact'], + ['contact:health_center_1'], + ['contact:health_center_1_contact'], + ['contact:patient_1'] + ]; + expect(pouchDb.query.args).to.deep.equal([ + ['medic/contacts_by_depth', { key: ['health_center_1'], include_docs: true, group_level: undefined, skip: undefined, limit: undefined }], + ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 1, skip: 0, group_level: undefined }], + ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 1, skip: 1, group_level: undefined }], + ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 1, skip: 2, group_level: undefined }], + ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 1, skip: 3, group_level: undefined }], + ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 1, skip: 4, group_level: undefined }], + ]); + }); + + it('should health_center_1 to district_1 in batches of 2', async () => { + DataSource.__set__('BATCH_SIZE', 2); + DataSource.BATCH_SIZE = 2; + sinon.spy(pouchDb, 'query'); + + await HierarchyOperations(pouchDb).move(['health_center_1'], 'district_1'); + + expect(getWrittenDoc('health_center_1_contact')).to.deep.eq({ + _id: 'health_center_1_contact', + type: 'person', + parent: parentsToLineage('health_center_1', 'district_1'), + }); + + expect(getWrittenDoc('health_center_1')).to.deep.eq({ + _id: 'health_center_1', + type: 'health_center', + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1'), + parent: parentsToLineage('district_1'), + }); + + expect(getWrittenDoc('clinic_1')).to.deep.eq({ + _id: 'clinic_1', + type: 'clinic', + contact: parentsToLineage('clinic_1_contact', 'clinic_1', 'health_center_1', 'district_1'), + parent: parentsToLineage('health_center_1', 'district_1'), + }); + + expect(getWrittenDoc('patient_1')).to.deep.eq({ + _id: 'patient_1', + type: 'person', + parent: parentsToLineage('clinic_1', 'health_center_1', 'district_1'), + }); + + expect(getWrittenDoc('report_1')).to.deep.eq({ + _id: 'report_1', + form: 'foo', + type: 'data_record', + fields: {}, + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1'), + }); + + expect(getWrittenDoc('report_2')).to.deep.eq({ + _id: 'report_2', + form: 'foo', + type: 'data_record', + fields: {}, + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1'), + }); + + expect(getWrittenDoc('report_3')).to.deep.eq({ + _id: 'report_3', + form: 'foo', + type: 'data_record', + fields: {}, + contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1'), + }); + + expect(pouchDb.query.callCount).to.deep.equal(4); + + const contactIdsKeys = [ + ['contact:clinic_1'], + ['contact:clinic_1_contact'], + ['contact:health_center_1'], + ['contact:health_center_1_contact'], + ['contact:patient_1'] + ]; + expect(pouchDb.query.args).to.deep.equal([ + ['medic/contacts_by_depth', { key: ['health_center_1'], include_docs: true, group_level: undefined, skip: undefined, limit: undefined }], + ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 2, skip: 0, group_level: undefined }], + ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 2, skip: 2, group_level: undefined }], + ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 2, skip: 4, group_level: undefined }] + ]); + }); }); }); - describe('merging', () => { + describe('merge', () => { it('merge district_2 into district_1', async () => { // setup await mockReport(pouchDb, { id: 'changing_subject_and_contact', creatorId: 'health_center_2_contact', - patient_id: 'district_2' + patientId: 'district_2' }); await mockReport(pouchDb, { @@ -449,8 +726,9 @@ describe('hierarchy-operations', () => { form: 'foo', type: 'data_record', contact: parentsToLineage('health_center_2_contact', 'health_center_2', 'district_1'), - fields: {}, - patient_id: 'district_1' + fields: { + patient_uuid: 'district_1' + } }); expect(getWrittenDoc('changing_contact')).to.deep.eq({ @@ -515,276 +793,69 @@ describe('hierarchy-operations', () => { }); }); - it('documents should be minified', async () => { - await updateHierarchyRules([{ id: 'clinic', parents: ['district_hospital'] }]); - const patient = { - parent: parentsToLineage('clinic_1', 'health_center_1', 'district_1'), - type: 'person', - important: true, - }; - const clinic = { - parent: parentsToLineage('health_center_1', 'district_1'), - type: 'clinic', - important: true, - }; - patient.parent.important = false; - clinic.parent.parent.important = false; - - await upsert('clinic_1', clinic); - await upsert('patient_1', patient); - - await HierarchyOperations(pouchDb).move(['clinic_1'], 'district_2'); - - expect(getWrittenDoc('clinic_1')).to.deep.eq({ - _id: 'clinic_1', - type: 'clinic', - important: true, - parent: parentsToLineage('district_2'), - }); - expect(getWrittenDoc('patient_1')).to.deep.eq({ - _id: 'patient_1', - type: 'person', - important: true, - parent: parentsToLineage('clinic_1', 'district_2'), - }); - }); - - it('cannot create circular hierarchy', async () => { - // even if the hierarchy rules allow it - await updateHierarchyRules([{ id: 'health_center', parents: ['clinic'] }]); - - try { - await HierarchyOperations(pouchDb).move(['health_center_1'], 'clinic_1'); - assert.fail('should throw'); - } catch (err) { - expect(err.message).to.include('circular'); - } - }); - - it('throw if parent does not exist', async () => { - const actual = HierarchyOperations(pouchDb).move(['clinic_1'], 'dne_parent_id'); - await expect(actual).to.eventually.rejectedWith('could not be found'); - }); - - it('throw when altering same lineage', async () => { - const actual = HierarchyOperations(pouchDb).move(['patient_1', 'health_center_1'], 'district_2'); - await expect(actual).to.eventually.rejectedWith('same lineage'); - }); - - it('throw if contact_id is not a contact', async () => { - const actual = HierarchyOperations(pouchDb).move(['report_1'], 'clinic_1'); - await expect(actual).to.eventually.rejectedWith('unknown type'); - }); - - it('throw if moving primary contact of parent', async () => { - const actual = HierarchyOperations(pouchDb).move(['clinic_1_contact'], 'district_1'); - await expect(actual).to.eventually.rejectedWith('primary contact'); - }); - - it('throw if setting parent to self', async () => { - await updateHierarchyRules([{ id: 'clinic', parents: ['clinic'] }]); - const actual = HierarchyOperations(pouchDb).move(['clinic_1'], 'clinic_1'); - await expect(actual).to.eventually.rejectedWith('itself'); - }); - - it('throw when moving place to unconfigured parent', async () => { - await updateHierarchyRules([{ id: 'district_hospital', parents: [] }]); - const actual = HierarchyOperations(pouchDb).move(['district_1'], 'district_2'); - await expect(actual).to.eventually.rejectedWith('parent of type'); - }); - - it('throw if source does not exist', async () => { - const nonExistentId = 'dne_parent_id'; - const actual = HierarchyOperations(pouchDb).move(['health_center_1', nonExistentId], 'district_2'); - await expect(actual).to.eventually.rejectedWith(`Contact with id '${nonExistentId}' could not be found.`); - }); - - it('throw if ancestor does not exist', async () => { - const sourceId = 'health_center_1'; - await upsert(sourceId, { - type: 'health_center', - name: 'no parent', - parent: parentsToLineage('dne'), - }); - - const actual = HierarchyOperations(pouchDb).move([sourceId], 'district_2'); - await expect(actual).to.eventually.rejectedWith( - `(${sourceId}) has parent id(s) 'dne' which could not be found.` - ); - }); - - describe('batching works as expected', () => { - const initialBatchSize = DataSource.BATCH_SIZE; - beforeEach(async () => { - await mockReport(pouchDb, { - id: 'report_2', - creatorId: 'health_center_1_contact', + describe('delete', () => { + const expectDeleted = (id, disableUsers = false) => { + expect(getWrittenDoc(id)).to.deep.eq({ + _id: id, + _deleted: true, + cht_disable_linked_users: disableUsers, }); + }; + it('delete district_2', async () => { + // setup await mockReport(pouchDb, { - id: 'report_3', - creatorId: 'health_center_1_contact', + id: 'district_report', + creatorId: 'health_center_2_contact', + patientId: 'district_2' }); - + await mockReport(pouchDb, { - id: 'report_4', - creatorId: 'health_center_1_contact', - }); - }); - - afterEach(() => { - DataSource.BATCH_SIZE = initialBatchSize; - DataSource.__set__('BATCH_SIZE', initialBatchSize); - }); - - it('move health_center_1 to district_2 in batches of 1', async () => { - DataSource.__set__('BATCH_SIZE', 1); - DataSource.BATCH_SIZE = 1; - sinon.spy(pouchDb, 'query'); - - await HierarchyOperations(pouchDb).move(['health_center_1'], 'district_2'); - - expect(getWrittenDoc('health_center_1_contact')).to.deep.eq({ - _id: 'health_center_1_contact', - type: 'person', - parent: parentsToLineage('health_center_1', 'district_2'), - }); - - expect(getWrittenDoc('health_center_1')).to.deep.eq({ - _id: 'health_center_1', - type: 'health_center', - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), - parent: parentsToLineage('district_2'), - }); - - expect(getWrittenDoc('clinic_1')).to.deep.eq({ - _id: 'clinic_1', - type: 'clinic', - contact: parentsToLineage('clinic_1_contact', 'clinic_1', 'health_center_1', 'district_2'), - parent: parentsToLineage('health_center_1', 'district_2'), - }); - - expect(getWrittenDoc('patient_1')).to.deep.eq({ - _id: 'patient_1', - type: 'person', - parent: parentsToLineage('clinic_1', 'health_center_1', 'district_2'), - }); - - expect(getWrittenDoc('report_1')).to.deep.eq({ - _id: 'report_1', - form: 'foo', - type: 'data_record', - fields: {}, - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), - }); - - expect(getWrittenDoc('report_2')).to.deep.eq({ - _id: 'report_2', - form: 'foo', - type: 'data_record', - fields: {}, - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), - }); - - expect(getWrittenDoc('report_3')).to.deep.eq({ - _id: 'report_3', - form: 'foo', - type: 'data_record', - fields: {}, - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_2'), + id: 'patient_report', + creatorId: 'health_center_2_contact', + patientId: 'patient_2' }); - - expect(pouchDb.query.callCount).to.deep.equal(6); - - const contactIdsKeys = [ - ['contact:clinic_1'], - ['contact:clinic_1_contact'], - ['contact:health_center_1'], - ['contact:health_center_1_contact'], - ['contact:patient_1'] + + // action + await HierarchyOperations(pouchDb, { disableUsers: true }).delete(['district_2']); + + // assert + const deletedPlaces = [ + 'district_2', + 'health_center_2', + 'clinic_2', ]; - expect(pouchDb.query.args).to.deep.equal([ - ['medic/contacts_by_depth', { key: ['health_center_1'], include_docs: true, group_level: undefined, skip: undefined, limit: undefined }], - ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 1, skip: 0, group_level: undefined }], - ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 1, skip: 1, group_level: undefined }], - ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 1, skip: 2, group_level: undefined }], - ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 1, skip: 3, group_level: undefined }], - ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 1, skip: 4, group_level: undefined }], - ]); + const deletedNonPeople = [ + 'district_2_contact', + 'health_center_2_contact', + 'clinic_2_contact', + 'patient_2', + 'district_report', + 'patient_report', + ]; + expectWrittenDocs([...deletedPlaces, ...deletedNonPeople]); + deletedPlaces.forEach(id => expectDeleted(id, true)); + deletedNonPeople.forEach(id => expectDeleted(id, false)); }); - it('should health_center_1 to district_1 in batches of 2', async () => { - DataSource.__set__('BATCH_SIZE', 2); - DataSource.BATCH_SIZE = 2; - sinon.spy(pouchDb, 'query'); - - await HierarchyOperations(pouchDb).move(['health_center_1'], 'district_1'); - - expect(getWrittenDoc('health_center_1_contact')).to.deep.eq({ - _id: 'health_center_1_contact', - type: 'person', - parent: parentsToLineage('health_center_1', 'district_1'), - }); - - expect(getWrittenDoc('health_center_1')).to.deep.eq({ - _id: 'health_center_1', - type: 'health_center', - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1'), - parent: parentsToLineage('district_1'), - }); - - expect(getWrittenDoc('clinic_1')).to.deep.eq({ - _id: 'clinic_1', - type: 'clinic', - contact: parentsToLineage('clinic_1_contact', 'clinic_1', 'health_center_1', 'district_1'), - parent: parentsToLineage('health_center_1', 'district_1'), - }); - - expect(getWrittenDoc('patient_1')).to.deep.eq({ - _id: 'patient_1', - type: 'person', - parent: parentsToLineage('clinic_1', 'health_center_1', 'district_1'), - }); - - expect(getWrittenDoc('report_1')).to.deep.eq({ - _id: 'report_1', - form: 'foo', - type: 'data_record', - fields: {}, - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1'), - }); - - expect(getWrittenDoc('report_2')).to.deep.eq({ - _id: 'report_2', - form: 'foo', - type: 'data_record', - fields: {}, - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1'), - }); + it('users at are not disabled when disableUsers: false', async () => { + await HierarchyOperations(pouchDb, { disableUsers: false }).delete(['district_2']); + expectDeleted('district_2', false); + }); - expect(getWrittenDoc('report_3')).to.deep.eq({ - _id: 'report_3', - form: 'foo', - type: 'data_record', - fields: {}, - contact: parentsToLineage('health_center_1_contact', 'health_center_1', 'district_1'), + it('reports created by deleted contacts are not deleted', async () => { + // setup + await mockReport(pouchDb, { + id: 'other_report', + creatorId: 'health_center_2_contact', + patientId: 'other' }); + + // action + await HierarchyOperations(pouchDb).delete(['district_2']); - expect(pouchDb.query.callCount).to.deep.equal(4); - - const contactIdsKeys = [ - ['contact:clinic_1'], - ['contact:clinic_1_contact'], - ['contact:health_center_1'], - ['contact:health_center_1_contact'], - ['contact:patient_1'] - ]; - expect(pouchDb.query.args).to.deep.equal([ - ['medic/contacts_by_depth', { key: ['health_center_1'], include_docs: true, group_level: undefined, skip: undefined, limit: undefined }], - ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 2, skip: 0, group_level: undefined }], - ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 2, skip: 2, group_level: undefined }], - ['medic-client/reports_by_freetext', { keys: contactIdsKeys, include_docs: true, limit: 2, skip: 4, group_level: undefined }] - ]); + const writtenIds = writtenDocs.map(doc => doc._id); + expect(writtenIds).to.not.include(['other_report']); }); }); -}); +}); \ No newline at end of file