diff --git a/src/lib/hierarchy-operations/hierarchy-data-source.js b/src/lib/hierarchy-operations/hierarchy-data-source.js index da5ec33a..fe912cf5 100644 --- a/src/lib/hierarchy-operations/hierarchy-data-source.js +++ b/src/lib/hierarchy-operations/hierarchy-data-source.js @@ -92,8 +92,8 @@ async function getAncestorsOf(db, contactDoc) { } module.exports = { - HIERARCHY_ROOT, BATCH_SIZE, + HIERARCHY_ROOT, getAncestorsOf, getContactWithDescendants, getContact, diff --git a/src/lib/hierarchy-operations/index.js b/src/lib/hierarchy-operations/index.js index 1b72dbdc..47d4d017 100644 --- a/src/lib/hierarchy-operations/index.js +++ b/src/lib/hierarchy-operations/index.js @@ -5,62 +5,61 @@ const { trace, info } = require('../log'); const JsDocs = require('./jsdocFolder'); const DataSource = require('./hierarchy-data-source'); -function moveHierarchy(db, options) { - return async function (sourceIds, destinationId) { - JsDocs.prepareFolder(options); - trace(`Fetching contact details: ${destinationId}`); - const constraints = await LineageConstraints(db, options); - const destinationDoc = await DataSource.getContact(db, destinationId); - const sourceDocs = await DataSource.getContactsByIds(db, sourceIds); - constraints.assertNoHierarchyErrors(Object.values(sourceDocs), destinationDoc); - - let affectedContactCount = 0; - let affectedReportCount = 0; - const replacementLineage = lineageManipulation.createLineageFromDoc(destinationDoc); - for (const sourceId of sourceIds) { - const sourceDoc = sourceDocs[sourceId]; - const descendantsAndSelf = await DataSource.getContactWithDescendants(db, sourceId); - const moveContext = { - sourceId, - destinationId, - descendantsAndSelf, - replacementLineage, - }; - - if (options.merge) { - JsDocs.writeDoc(options, { - _id: sourceDoc._id, - _rev: sourceDoc._rev, - _deleted: true, - }); - } - - const prettyPrintDocument = doc => `'${doc.name}' (${doc._id})`; - await constraints.assertNoPrimaryContactViolations(sourceDoc, destinationDoc, descendantsAndSelf); - - trace(`Considering lineage updates to ${descendantsAndSelf.length} descendant(s) of contact ${prettyPrintDocument(sourceDoc)}.`); - const updatedDescendants = replaceLineageInContacts(options, moveContext); - - const ancestors = await DataSource.getAncestorsOf(db, sourceDoc); - trace(`Considering primary contact updates to ${ancestors.length} ancestor(s) of contact ${prettyPrintDocument(sourceDoc)}.`); - const updatedAncestors = replaceLineageInAncestors(descendantsAndSelf, ancestors); - - minifyLineageAndWriteToDisk(options, [...updatedDescendants, ...updatedAncestors]); - - const movedReportsCount = await moveReports(db, options, moveContext); - trace(`${movedReportsCount} report(s) created by these affected contact(s) will be updated`); - - affectedContactCount += updatedDescendants.length + updatedAncestors.length; - affectedReportCount += movedReportsCount; - - info(`Staged updates to ${prettyPrintDocument(sourceDoc)}. ${updatedDescendants.length} contact(s) and ${movedReportsCount} report(s).`); +async function moveHierarchy(db, options, sourceIds, destinationId) { + JsDocs.prepareFolder(options); + trace(`Fetching contact details: ${destinationId}`); + const constraints = await LineageConstraints(db, options); + const destinationDoc = await DataSource.getContact(db, destinationId); + const sourceDocs = await DataSource.getContactsByIds(db, sourceIds); + constraints.assertNoHierarchyErrors(Object.values(sourceDocs), destinationDoc); + + let affectedContactCount = 0; + let affectedReportCount = 0; + const replacementLineage = lineageManipulation.createLineageFromDoc(destinationDoc); + for (const sourceId of sourceIds) { + const sourceDoc = sourceDocs[sourceId]; + const descendantsAndSelf = await DataSource.getContactWithDescendants(db, sourceId); + const moveContext = { + sourceId, + destinationId, + descendantsAndSelf, + replacementLineage, + merge: !!options.merge, + }; + + if (options.merge) { + JsDocs.writeDoc(options, { + _id: sourceDoc._id, + _rev: sourceDoc._rev, + _deleted: true, + }); } - info(`Staged changes to lineage information for ${affectedContactCount} contact(s) and ${affectedReportCount} report(s).`); - }; + const prettyPrintDocument = doc => `'${doc.name}' (${doc._id})`; + await constraints.assertNoPrimaryContactViolations(sourceDoc, destinationDoc, descendantsAndSelf); + + trace(`Considering lineage updates to ${descendantsAndSelf.length} descendant(s) of contact ${prettyPrintDocument(sourceDoc)}.`); + const updatedDescendants = replaceLineageInContacts(options, moveContext); + + const ancestors = await DataSource.getAncestorsOf(db, sourceDoc); + trace(`Considering primary contact updates to ${ancestors.length} ancestor(s) of contact ${prettyPrintDocument(sourceDoc)}.`); + const updatedAncestors = replaceLineageInAncestors(descendantsAndSelf, ancestors); + + minifyLineageAndWriteToDisk(options, [...updatedDescendants, ...updatedAncestors]); + + const movedReportsCount = await updateReports(db, options, moveContext); + trace(`${movedReportsCount} report(s) created by these affected contact(s) will be updated`); + + affectedContactCount += updatedDescendants.length + updatedAncestors.length; + affectedReportCount += movedReportsCount; + + info(`Staged updates to ${prettyPrintDocument(sourceDoc)}. ${updatedDescendants.length} contact(s) and ${movedReportsCount} report(s).`); + } + + info(`Staged changes to lineage information for ${affectedContactCount} contact(s) and ${affectedReportCount} report(s).`); } -async function moveReports(db, options, moveContext) { +async function updateReports(db, options, moveContext) { const descendantIds = moveContext.descendantsAndSelf.map(contact => contact._id); let skip = 0; @@ -70,8 +69,8 @@ async function moveReports(db, options, moveContext) { const createdAtId = options.merge && moveContext.sourceId; reportDocsBatch = await DataSource.getReportsForContacts(db, descendantIds, createdAtId, skip); - const lineageUpdates = replaceLineageInReports(options, reportDocsBatch, moveContext); - const reassignUpdates = reassignReports(options, reportDocsBatch, moveContext); + const lineageUpdates = replaceCreatorLineageInReports(reportDocsBatch, moveContext); + const reassignUpdates = reassignReports(reportDocsBatch, moveContext); const updatedReports = reportDocsBatch.filter(doc => lineageUpdates.has(doc._id) || reassignUpdates.has(doc._id)); minifyLineageAndWriteToDisk(options, updatedReports); @@ -82,28 +81,34 @@ async function moveReports(db, options, moveContext) { return skip; } -function reassignReports(options, reports, { sourceId, destinationId }) { - function reassignReportWithSubject(report, subjectId) { +function reassignReportSubjects(report, { sourceId, destinationId }) { + const SUBJECT_IDS = ['patient_id', 'patient_uuid', 'place_id', 'place_uuid']; + let updated = false; + for (const subjectId of SUBJECT_IDS) { if (report[subjectId] === sourceId) { report[subjectId] = destinationId; - updated.add(report._id); + updated = true; } if (report.fields[subjectId] === sourceId) { report.fields[subjectId] = destinationId; - updated.add(report._id); + updated = true; } } + + return updated; +} +function reassignReports(reports, moveContext) { const updated = new Set(); - if (!options.merge) { + if (!moveContext.merge) { return updated; } for (const report of reports) { - const subjectIds = ['patient_id', 'patient_uuid', 'place_id', 'place_uuid']; - for (const subjectId of subjectIds) { - reassignReportWithSubject(report, subjectId); + const isUpdated = reassignReportSubjects(report, moveContext); + if (isUpdated) { + updated.add(report._id); } } @@ -117,16 +122,16 @@ function minifyLineageAndWriteToDisk(options, docs) { }); } -function replaceLineageInReports(options, reports, moveContext) { - const replaceLineageOptions = { +function replaceCreatorLineageInReports(reports, moveContext) { + const replaceContactLineage = doc => lineageManipulation.replaceContactLineage(doc, { replaceWith: moveContext.replacementLineage, startingFromId: moveContext.sourceId, - merge: options.merge, - }; + merge: moveContext.merge, + }); const updates = new Set(); reports.forEach(doc => { - if (lineageManipulation.replaceContactLineage(doc, replaceLineageOptions)) { + if (replaceContactLineage(doc)) { updates.add(doc._id); } }); @@ -147,31 +152,31 @@ function replaceLineageInAncestors(descendantsAndSelf, ancestors) { return updatedAncestors; } -function replaceLineageInContacts(options, moveContext) { +function replaceForSingleContact(doc, moveContext) { const { sourceId } = moveContext; - function replaceForSingleContact(doc) { - const docIsDestination = doc._id === sourceId; - const startingFromId = options.merge || !docIsDestination ? sourceId : undefined; - const replaceLineageOptions = { - replaceWith: moveContext.replacementLineage, - startingFromId, - merge: options.merge, - }; - const parentWasUpdated = lineageManipulation.replaceParentLineage(doc, replaceLineageOptions); + const docIsDestination = doc._id === sourceId; + const startingFromId = moveContext.merge || !docIsDestination ? sourceId : undefined; + const replaceLineageOptions = { + replaceWith: moveContext.replacementLineage, + startingFromId, + merge: moveContext.merge, + }; + const parentWasUpdated = lineageManipulation.replaceParentLineage(doc, replaceLineageOptions); - replaceLineageOptions.startingFromId = sourceId; - const contactWasUpdated = lineageManipulation.replaceContactLineage(doc, replaceLineageOptions); - if (parentWasUpdated || contactWasUpdated) { - return doc; - } + replaceLineageOptions.startingFromId = sourceId; + const contactWasUpdated = lineageManipulation.replaceContactLineage(doc, replaceLineageOptions); + if (parentWasUpdated || contactWasUpdated) { + return doc; } +} +function replaceLineageInContacts(options, moveContext) { function sonarQubeComplexityFiveIsTooLow(doc) { - const docIsSource = doc._id === sourceId; + const docIsSource = doc._id === moveContext.sourceId; // skip source because it will be deleted if (!options.merge || !docIsSource) { - return replaceForSingleContact(doc); + return replaceForSingleContact(doc, moveContext); } } @@ -189,8 +194,8 @@ function replaceLineageInContacts(options, moveContext) { module.exports = (db, options) => { return { HIERARCHY_ROOT: DataSource.HIERARCHY_ROOT, - move: moveHierarchy(db, { ...options, merge: false }), - merge: moveHierarchy(db, { ...options, merge: true }), + move: (sourceIds, destinationId) => moveHierarchy(db, { ...options, merge: false }, sourceIds, destinationId), + merge: (sourceIds, destinationId) => moveHierarchy(db, { ...options, merge: true }, sourceIds, destinationId), }; }; diff --git a/src/lib/hierarchy-operations/lineage-constraints.js b/src/lib/hierarchy-operations/lineage-constraints.js index a2ff9fd6..f2478aac 100644 --- a/src/lib/hierarchy-operations/lineage-constraints.js +++ b/src/lib/hierarchy-operations/lineage-constraints.js @@ -51,36 +51,36 @@ Enforce the list of allowed parents for each contact type Ensure we are not creating a circular hierarchy */ const getMovingViolations = (mapTypeToAllowedParents, sourceDoc, destinationDoc) => { - function getContactTypeError() { - const sourceContactType = getContactType(sourceDoc); - const destinationType = getContactType(destinationDoc); - const rulesForContact = mapTypeToAllowedParents[sourceContactType]; - if (!rulesForContact) { - return `cannot move contact with unknown type '${sourceContactType}'`; - } + const commonViolations = getCommonViolations(sourceDoc, destinationDoc); + const contactTypeError = getMovingContactTypeError(mapTypeToAllowedParents, sourceDoc, destinationDoc); + const circularHierarchyError = findCircularHierarchyErrors(sourceDoc, destinationDoc); + return commonViolations || contactTypeError || circularHierarchyError; +}; - const isPermittedMoveToRoot = !destinationDoc && rulesForContact.length === 0; - if (!isPermittedMoveToRoot && !rulesForContact.includes(destinationType)) { - return `contacts of type '${sourceContactType}' cannot have parent of type '${destinationType}'`; - } +function getMovingContactTypeError(mapTypeToAllowedParents, sourceDoc, destinationDoc) { + const sourceContactType = getContactType(sourceDoc); + const destinationType = getContactType(destinationDoc); + const rulesForContact = mapTypeToAllowedParents[sourceContactType]; + if (!rulesForContact) { + return `cannot move contact with unknown type '${sourceContactType}'`; } - function findCircularHierarchyErrors() { - if (!destinationDoc || !sourceDoc._id) { - return; - } + const isPermittedMoveToRoot = !destinationDoc && rulesForContact.length === 0; + if (!isPermittedMoveToRoot && !rulesForContact.includes(destinationType)) { + return `contacts of type '${sourceContactType}' cannot have parent of type '${destinationType}'`; + } +} - const parentAncestry = [destinationDoc._id, ...lineageManipulation.pluckIdsFromLineage(destinationDoc.parent)]; - if (parentAncestry.includes(sourceDoc._id)) { - return `Circular hierarchy: Cannot set parent of contact '${sourceDoc._id}' as it would create a circular hierarchy.`; - } +function findCircularHierarchyErrors(sourceDoc, destinationDoc) { + if (!destinationDoc || !sourceDoc._id) { + return; } - const commonViolations = getCommonViolations(sourceDoc, destinationDoc); - const contactTypeError = getContactTypeError(); - const circularHierarchyError = findCircularHierarchyErrors(); - return commonViolations || contactTypeError || circularHierarchyError; -}; + const parentAncestry = [destinationDoc._id, ...lineageManipulation.pluckIdsFromLineage(destinationDoc.parent)]; + if (parentAncestry.includes(sourceDoc._id)) { + return `Circular hierarchy: Cannot set parent of contact '${sourceDoc._id}' as it would create a circular hierarchy.`; + } +} const getCommonViolations = (sourceDoc, destinationDoc) => { const sourceContactType = getContactType(sourceDoc);