Skip to content

Commit

Permalink
Some renaming
Browse files Browse the repository at this point in the history
  • Loading branch information
kennsippell committed Nov 21, 2024
1 parent 3e6168c commit 2449dd4
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 59 deletions.
66 changes: 33 additions & 33 deletions src/fn/merge-contacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,20 @@ module.exports = {
};

const mergeContacts = async (options, db) => {
trace(`Fetching contact details: ${options.winnerId}`);
const winnerDoc = await Shared.fetch.contact(db, options.winnerId);
trace(`Fetching contact details: ${options.keptId}`);
const keptDoc = await Shared.fetch.contact(db, options.keptId);

const constraints = await lineageConstraints(db, winnerDoc);
const loserDocs = await Shared.fetch.contactList(db, options.loserIds);
await validateContacts(loserDocs, constraints);
const constraints = await lineageConstraints(db, keptDoc);
const removedDocs = await Shared.fetch.contactList(db, options.removedIds);
await validateContacts(removedDocs, constraints);

let affectedContactCount = 0, affectedReportCount = 0;
const replacementLineage = lineageManipulation.createLineageFromDoc(winnerDoc);
for (let loserId of options.loserIds) {
const contactDoc = loserDocs[loserId];
const descendantsAndSelf = await Shared.fetch.descendantsOf(db, loserId);
const replacementLineage = lineageManipulation.createLineageFromDoc(keptDoc);
for (let removedId of options.removedIds) {
const contactDoc = removedDocs[removedId];
const descendantsAndSelf = await Shared.fetch.descendantsOf(db, removedId);

const self = descendantsAndSelf.find(d => d._id === loserId);
const self = descendantsAndSelf.find(d => d._id === removedId);
Shared.writeDocumentToDisk(options, {
_id: self._id,
_rev: self._rev,
Expand All @@ -48,15 +48,15 @@ const mergeContacts = async (options, db) => {
}

trace(`Considering lineage updates to ${descendantsAndSelf.length} descendant(s) of contact ${prettyPrintDocument(contactDoc)}.`);
const updatedDescendants = replaceLineageInContacts(descendantsAndSelf, replacementLineage, loserId);
const updatedDescendants = replaceLineageInContacts(descendantsAndSelf, replacementLineage, removedId);

const ancestors = await Shared.fetch.ancestorsOf(db, contactDoc);
trace(`Considering primary contact updates to ${ancestors.length} ancestor(s) of contact ${prettyPrintDocument(contactDoc)}.`);
const updatedAncestors = Shared.replaceLineageInAncestors(descendantsAndSelf, ancestors);

minifyLineageAndWriteToDisk([...updatedDescendants, ...updatedAncestors], options);

const movedReportsCount = await moveReportsAndReassign(db, descendantsAndSelf, options, replacementLineage, loserId);
const movedReportsCount = await moveReportsAndReassign(db, descendantsAndSelf, options, replacementLineage, removedId);
trace(`${movedReportsCount} report(s) created by these affected contact(s) will be updated`);

affectedContactCount += updatedDescendants.length + updatedAncestors.length;
Expand All @@ -72,8 +72,8 @@ const mergeContacts = async (options, db) => {
Checks for any errors which this will create in the hierarchy (hierarchy schema, circular hierarchies)
Confirms the list of contacts are possible to move
*/
const validateContacts = async (loserDocs, constraints) => {
Object.values(loserDocs).forEach(doc => {
const validateContacts = async (removedDocs, constraints) => {
Object.values(removedDocs).forEach(doc => {
const hierarchyError = constraints.getMergeContactHierarchyViolations(doc);
if (hierarchyError) {
throw Error(`Hierarchy Constraints: ${hierarchyError}`);
Expand All @@ -85,23 +85,23 @@ const validateContacts = async (loserDocs, constraints) => {
const parseExtraArgs = (projectDir, extraArgs = []) => {
const args = minimist(extraArgs, { boolean: true });

const loserIds = (args.losers || args.loser || '')
const removedIds = (args.removed || '')
.split(',')
.filter(Boolean);

if (!args.winner) {
if (!args.kept) {
usage();
throw Error(`Action "merge-contacts" is missing required contact ID ${Shared.bold('--winner')}. Other contacts will be merged into this contact.`);
throw Error(`Action "merge-contacts" is missing required contact ID ${Shared.bold('--kept')}. Other contacts will be merged into this contact.`);
}

if (loserIds.length === 0) {
if (removedIds.length === 0) {
usage();
throw Error(`Action "merge-contacts" is missing required contact ID(s) ${Shared.bold('--losers')}. These contacts will be merged into the contact specified by ${Shared.bold('--winner')}`);
throw Error(`Action "merge-contacts" is missing required contact ID(s) ${Shared.bold('--removed')}. These contacts will be merged into the contact specified by ${Shared.bold('--kept')}`);
}

return {
winnerId: args.winner,
loserIds,
keptId: args.kept,
removedIds,
docDirectoryPath: path.resolve(projectDir, args.docDirectoryPath || 'json_docs'),
force: !!args.force,
};
Expand All @@ -113,43 +113,43 @@ ${Shared.bold('cht-conf\'s merge-contacts action')}
When combined with 'upload-docs' this action merges multiple contacts and all their associated data into one.
${Shared.bold('USAGE')}
cht --local merge-contacts -- --winner=<winner_id> --losers=<loser_id1>,<loser_id2>
cht --local merge-contacts -- --kept=<kept_id> --removed=<removed_id1>,<removed_id2>
${Shared.bold('OPTIONS')}
--winner=<winner_id>
--kept=<kept_id>
Specifies the ID of the contact that should have all other contact data merged into it.
--losers=<loser_id1>,<loser_id2>
A comma delimited list of IDs of contacts which will be deleted and all of their data will be merged into the winner contact.
--removed=<removed_id1>,<removed_id2>
A comma delimited list of IDs of contacts which will be deleted and all of their data will be merged into the kept contact.
--docDirectoryPath=<path to stage docs>
Specifies the folder used to store the documents representing the changes in hierarchy.
`);
};

const moveReportsAndReassign = async (db, descendantsAndSelf, writeOptions, replacementLineage, loserId) => {
const moveReportsAndReassign = async (db, descendantsAndSelf, writeOptions, replacementLineage, removedId) => {
const descendantIds = descendantsAndSelf.map(contact => contact._id);
const winnerId = writeOptions.winnerId;
const keptId = writeOptions.keptId;

let skip = 0;
let reportDocsBatch;
do {
info(`Processing ${skip} to ${skip + Shared.BATCH_SIZE} report docs`);
reportDocsBatch = await Shared.fetch.reportsCreatedByOrFor(db, descendantIds, loserId, skip);
reportDocsBatch = await Shared.fetch.reportsCreatedByOrFor(db, descendantIds, removedId, skip);

const updatedReports = replaceLineageInReports(reportDocsBatch, replacementLineage, loserId);
const updatedReports = replaceLineageInReports(reportDocsBatch, replacementLineage, removedId);

reportDocsBatch.forEach(report => {
let updated = false;
const subjectIds = ['patient_id', 'patient_uuid', 'place_id', 'place_uuid'];
for (const subjectId of subjectIds) {
if (report[subjectId] === loserId) {
report[subjectId] = winnerId;
if (report[subjectId] === removedId) {
report[subjectId] = keptId;
updated = true;
}

if (report.fields[subjectId] === loserId) {
report.fields[subjectId] = winnerId;
if (report.fields[subjectId] === removedId) {
report.fields[subjectId] = keptId;
updated = true;
}

Expand Down
18 changes: 9 additions & 9 deletions src/lib/lineage-constraints.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,23 @@ const getMoveContactHierarchyViolations = (mapTypeToAllowedParents, contactDoc,
Enforce the list of allowed parents for each contact type
Ensure we are not creating a circular hierarchy
*/
const getMergeContactHierarchyViolations = (loserDoc, winnerDoc) => {
const getMergeContactHierarchyViolations = (removedDoc, keptDoc) => {
const getContactType = doc => doc && (doc.type === 'contact' ? doc.contact_type : doc.type);
const loserContactType = getContactType(loserDoc);
const winnerContactType = getContactType(winnerDoc);
if (!loserContactType) {
const removedContactType = getContactType(removedDoc);
const keptContactType = getContactType(keptDoc);
if (!removedContactType) {
return 'contact required attribute "type" is undefined';
}

if (winnerDoc && !winnerContactType) {
return `winner contact "${winnerDoc._id}" required attribute "type" is undefined`;
if (keptDoc && !keptContactType) {
return `kept contact "${keptDoc._id}" required attribute "type" is undefined`;
}

if (loserContactType !== winnerContactType) {
return `contact "${loserDoc._id}" must have same contact type as "${winnerContactType}". Former is "${loserContactType}" while later is "${winnerContactType}".`;
if (removedContactType !== keptContactType) {
return `contact "${removedDoc._id}" must have same contact type as "${keptContactType}". Former is "${removedContactType}" while later is "${keptContactType}".`;
}

if (loserDoc._id === winnerDoc._id) {
if (removedDoc._id === keptDoc._id) {
return `Cannot merge contact with self`;
}
};
Expand Down
10 changes: 5 additions & 5 deletions src/lib/mm-shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,15 @@ const fetch = {
return reports.rows.map(row => row.doc);
},

reportsCreatedByOrFor: async (db, descendantIds, loserId, skip) => {
reportsCreatedByOrFor: async (db, descendantIds, removedId, skip) => {
// TODO is this the right way?
const reports = await db.query('medic-client/reports_by_freetext', {
keys: [
...descendantIds.map(descendantId => [`contact:${descendantId}`]),
[`patient_id:${loserId}`],
[`patient_uuid:${loserId}`],
[`place_id:${loserId}`],
[`place_uuid:${loserId}`],
[`patient_id:${removedId}`],
[`patient_uuid:${removedId}`],
[`place_id:${removedId}`],
[`place_uuid:${removedId}`],
],
include_docs: true,
limit: BATCH_SIZE,
Expand Down
80 changes: 68 additions & 12 deletions test/fn/merge-contacts.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,19 @@ describe('merge-contacts', () => {
delete result._rev;
return result;
};
const expectWrittenDocs = expected => expect(writtenDocs.map(doc => doc._id)).to.have.members(expected);

beforeEach(async () => {
pouchDb = new PouchDB(`merge-contacts-${scenarioCount++}`);

await mockHierarchy(pouchDb, {
district_1: {},
district_1: {
health_center_1: {
clinic_1: {
patient_1: {},
},
}
},
district_2: {
health_center_2: {
clinic_2: {
Expand Down Expand Up @@ -97,11 +104,19 @@ describe('merge-contacts', () => {

// action
await mergeContacts({
loserIds: ['district_2'],
winnerId: 'district_1',
removedIds: ['district_2'],
keptId: 'district_1',
}, pouchDb);

// assert
expectWrittenDocs([
'district_2', 'district_2_contact',
'health_center_2', 'health_center_2_contact',
'clinic_2', 'clinic_2_contact',
'patient_2',
'changing_subject_and_contact', 'changing_contact', 'changing_subject'
]);

expect(getWrittenDoc('district_2')).to.deep.eq({
_id: 'district_2',
_deleted: true,
Expand Down Expand Up @@ -160,26 +175,67 @@ describe('merge-contacts', () => {
});
});

it('throw if loser does not exist', async () => {
it('merge two patients', async () => {
// setup
await mockReport(pouchDb, {
id: 'pat1',
creatorId: 'clinic_1_contact',
patientId: 'patient_1'
});

await mockReport(pouchDb, {
id: 'pat2',
creatorId: 'clinic_2_contact',
patientId: 'patient_2'
});

// action
await mergeContacts({
removedIds: ['patient_2'],
keptId: 'patient_1',
}, pouchDb);

await expectWrittenDocs(['patient_2', 'pat2']);

expect(getWrittenDoc('patient_2')).to.deep.eq({
_id: 'patient_2',
_deleted: true,
});

expect(getWrittenDoc('pat2')).to.deep.eq({
_id: 'pat2',
form: 'foo',
type: 'data_record',
// still created by the user in district-2
contact: parentsToLineage('clinic_2_contact', 'clinic_2', 'health_center_2', 'district_2'),
fields: {
patient_uuid: 'patient_1'
}
});
});

xit('write to ancestors', () => {});

it('throw if removed does not exist', async () => {
const actual = mergeContacts({
loserIds: ['dne'],
winnerId: 'district_1',
removedIds: ['dne'],
keptId: 'district_1',
}, pouchDb);
await expect(actual).to.eventually.rejectedWith('could not be found');
});

it('throw if winner does not exist', async () => {
it('throw if kept does not exist', async () => {
const actual = mergeContacts({
loserIds: ['district_1'],
winnerId: 'dne',
removedIds: ['district_1'],
keptId: 'dne',
}, pouchDb);
await expect(actual).to.eventually.rejectedWith('could not be found');
});

it('throw if loser is winner', async () => {
it('throw if removed is kept', async () => {
const actual = mergeContacts({
loserIds: ['district_1', 'district_2'],
winnerId: 'district_2',
removedIds: ['district_1', 'district_2'],
keptId: 'district_2',
}, pouchDb);
await expect(actual).to.eventually.rejectedWith('merge contact with self');
});
Expand Down

0 comments on commit 2449dd4

Please sign in to comment.