cht-conf 4.1.3 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/fn/edit-contacts.js +70 -50
- package/src/fn/merge-contacts.js +74 -0
- package/src/fn/move-contacts.js +13 -236
- package/src/fn/upload-docs.js +104 -18
- package/src/lib/api.js +24 -0
- package/src/lib/hierarchy-operations/hierarchy-data-source.js +103 -0
- package/src/lib/hierarchy-operations/index.js +187 -0
- package/src/lib/hierarchy-operations/jsdocFolder.js +37 -0
- package/src/lib/hierarchy-operations/lineage-constraints.js +170 -0
- package/src/lib/hierarchy-operations/lineage-manipulation.js +63 -0
- package/src/lib/hierarchy-operations/replace-lineage.js +76 -0
- package/src/lib/user-prompt.js +10 -1
- package/src/lib/lineage-constraints.js +0 -94
- package/src/lib/lineage-manipulation.js +0 -107
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const log = require('../log');
|
|
2
|
+
const { HIERARCHY_ROOT } = require('./hierarchy-data-source');
|
|
3
|
+
const { trace } = log;
|
|
4
|
+
|
|
5
|
+
const lineageManipulation = require('./lineage-manipulation');
|
|
6
|
+
|
|
7
|
+
module.exports = async (db, options) => {
|
|
8
|
+
const mapTypeToAllowedParents = await fetchAllowedParents(db);
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
assertNoPrimaryContactViolations: async (sourceDoc, destinationDoc, descendantDocs) => {
|
|
12
|
+
const invalidPrimaryContactDoc = await getPrimaryContactViolations(db, sourceDoc, destinationDoc, descendantDocs);
|
|
13
|
+
if (invalidPrimaryContactDoc) {
|
|
14
|
+
throw Error(`Cannot remove contact '${invalidPrimaryContactDoc?.name}' (${invalidPrimaryContactDoc?._id}) from the hierarchy for which they are a primary contact.`);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
assertNoHierarchyErrors: (sourceDocs, destinationDoc) => {
|
|
18
|
+
|
|
19
|
+
sourceDocs.forEach(sourceDoc => {
|
|
20
|
+
const commonViolations = getCommonViolations(sourceDoc, destinationDoc);
|
|
21
|
+
const specificViolation = options.merge ?
|
|
22
|
+
getMergeViolations(sourceDoc, destinationDoc)
|
|
23
|
+
: getMovingViolations(mapTypeToAllowedParents, sourceDoc, destinationDoc);
|
|
24
|
+
|
|
25
|
+
const hierarchyError = commonViolations || specificViolation;
|
|
26
|
+
if (hierarchyError) {
|
|
27
|
+
throw Error(`Hierarchy Constraints: ${hierarchyError}`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
It is nice that the tool can move lists of contacts as one operation, but strange things happen when two contactIds are in the same lineage.
|
|
33
|
+
For example, moving a district_hospital and moving a contact under that district_hospital to a new clinic causes multiple colliding writes to the same json file.
|
|
34
|
+
*/
|
|
35
|
+
const contactIds = sourceDocs.map(doc => doc._id);
|
|
36
|
+
sourceDocs
|
|
37
|
+
.forEach(doc => {
|
|
38
|
+
const parentIdsOfDoc = lineageManipulation.pluckIdsFromLineage(doc.parent);
|
|
39
|
+
const violatingParentId = parentIdsOfDoc.find(parentId => contactIds.includes(parentId));
|
|
40
|
+
if (violatingParentId) {
|
|
41
|
+
throw Error(`Unable to move two documents from the same lineage: '${doc._id}' and '${violatingParentId}'`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
Enforce the list of allowed parents for each contact type
|
|
50
|
+
Ensure we are not creating a circular hierarchy
|
|
51
|
+
*/
|
|
52
|
+
const getMovingViolations = (mapTypeToAllowedParents, sourceDoc, destinationDoc) => {
|
|
53
|
+
const contactTypeError = getMovingContactTypeError(mapTypeToAllowedParents, sourceDoc, destinationDoc);
|
|
54
|
+
const circularHierarchyError = findCircularHierarchyErrors(sourceDoc, destinationDoc);
|
|
55
|
+
return contactTypeError || circularHierarchyError;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function getMovingContactTypeError(mapTypeToAllowedParents, sourceDoc, destinationDoc) {
|
|
59
|
+
const sourceContactType = getContactType(sourceDoc);
|
|
60
|
+
const destinationType = getContactType(destinationDoc);
|
|
61
|
+
const rulesForContact = mapTypeToAllowedParents[sourceContactType];
|
|
62
|
+
if (!rulesForContact) {
|
|
63
|
+
return `cannot move contact with unknown type '${sourceContactType}'`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isPermittedMoveToRoot = !destinationDoc && rulesForContact.length === 0;
|
|
67
|
+
if (!isPermittedMoveToRoot && !rulesForContact.includes(destinationType)) {
|
|
68
|
+
return `contacts of type '${sourceContactType}' cannot have parent of type '${destinationType}'`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findCircularHierarchyErrors(sourceDoc, destinationDoc) {
|
|
73
|
+
if (!destinationDoc) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const parentAncestry = lineageManipulation.pluckIdsFromLineage(destinationDoc);
|
|
78
|
+
if (parentAncestry.includes(sourceDoc._id)) {
|
|
79
|
+
return `Circular hierarchy: Cannot set parent of contact '${sourceDoc._id}' as it would create a circular hierarchy.`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const getCommonViolations = (sourceDoc, destinationDoc) => {
|
|
84
|
+
if (!sourceDoc) {
|
|
85
|
+
return `source doc cannot be found`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const sourceContactType = getContactType(sourceDoc);
|
|
89
|
+
const destinationContactType = getContactType(destinationDoc);
|
|
90
|
+
if (!sourceContactType) {
|
|
91
|
+
return `source contact "${sourceDoc._id}" required attribute "type" is undefined`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (destinationDoc && !destinationContactType) {
|
|
95
|
+
return `destination contact "${destinationDoc._id}" required attribute "type" is undefined`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (sourceDoc._id === destinationDoc?._id) {
|
|
99
|
+
return `cannot move or merge contact that is itself`;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const getMergeViolations = (sourceDoc, destinationDoc) => {
|
|
104
|
+
if (!destinationDoc) {
|
|
105
|
+
return `destination doc cannot be found`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if ([sourceDoc._id, destinationDoc._id].includes(HIERARCHY_ROOT)) {
|
|
109
|
+
return `cannot merge using id: "${HIERARCHY_ROOT}"`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sourceContactType = getContactType(sourceDoc);
|
|
113
|
+
const destinationContactType = getContactType(destinationDoc);
|
|
114
|
+
if (sourceContactType !== destinationContactType) {
|
|
115
|
+
return `source and destinations must have the same type. Source is "${sourceContactType}" while destination is "${destinationContactType}".`;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
A place's primary contact must be a descendant of that place.
|
|
121
|
+
|
|
122
|
+
1. Check to see which part of the contact's lineage will be removed
|
|
123
|
+
2. For each removed part of the contact's lineage, confirm that place's primary contact isn't being removed.
|
|
124
|
+
*/
|
|
125
|
+
const getPrimaryContactViolations = async (db, contactDoc, destinationDoc, descendantDocs) => {
|
|
126
|
+
const contactsLineageIds = lineageManipulation.pluckIdsFromLineage(contactDoc?.parent);
|
|
127
|
+
const parentsLineageIds = lineageManipulation.pluckIdsFromLineage(destinationDoc);
|
|
128
|
+
|
|
129
|
+
const docIdsRemovedFromContactLineage = contactsLineageIds.filter(value => !parentsLineageIds.includes(value));
|
|
130
|
+
const docsRemovedFromContactLineage = await db.allDocs({
|
|
131
|
+
keys: docIdsRemovedFromContactLineage,
|
|
132
|
+
include_docs: true,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const primaryContactIds = docsRemovedFromContactLineage.rows
|
|
136
|
+
.map(row => row?.doc?.contact?._id)
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
|
|
139
|
+
return descendantDocs.find(descendant => primaryContactIds.some(primaryId => descendant._id === primaryId));
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const getContactType = doc => doc && (doc.type === 'contact' ? doc.contact_type : doc.type);
|
|
143
|
+
|
|
144
|
+
async function fetchAllowedParents(db) {
|
|
145
|
+
try {
|
|
146
|
+
const { settings: { contact_types } } = await db.get('settings');
|
|
147
|
+
|
|
148
|
+
if (Array.isArray(contact_types)) {
|
|
149
|
+
trace('Found app_settings.contact_types. Configurable hierarchy constraints will be enforced.');
|
|
150
|
+
const parentDict = {};
|
|
151
|
+
contact_types
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.forEach(({ id, parents }) => parentDict[id] = parents);
|
|
154
|
+
return parentDict;
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err.status !== 404) {
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
trace('Default hierarchy constraints will be enforced.');
|
|
163
|
+
return {
|
|
164
|
+
district_hospital: [],
|
|
165
|
+
health_center: ['district_hospital'],
|
|
166
|
+
clinic: ['health_center'],
|
|
167
|
+
person: ['district_hospital', 'health_center', 'clinic'],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const { replaceContactLineage, replaceParentLineage } = require('./replace-lineage');
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Function borrowed from shared-lib/lineage
|
|
5
|
+
*/
|
|
6
|
+
const minifyLineagesInDoc = doc => {
|
|
7
|
+
const minifyLineage = lineage => {
|
|
8
|
+
if (!lineage?._id) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
_id: lineage._id,
|
|
14
|
+
parent: minifyLineage(lineage.parent),
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (!doc) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if ('parent' in doc) {
|
|
23
|
+
doc.parent = minifyLineage(doc.parent);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if ('contact' in doc) {
|
|
27
|
+
doc.contact = minifyLineage(doc.contact);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (doc.type === 'data_record') {
|
|
31
|
+
delete doc.patient;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const createLineageFromDoc = doc => {
|
|
36
|
+
if (!doc) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
_id: doc._id,
|
|
42
|
+
parent: doc.parent || undefined,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
Given a lineage, return the ids therein
|
|
48
|
+
*/
|
|
49
|
+
const pluckIdsFromLineage = (lineage, results = []) => {
|
|
50
|
+
if (!lineage) {
|
|
51
|
+
return results;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return pluckIdsFromLineage(lineage.parent, [...results, lineage._id]);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
createLineageFromDoc,
|
|
59
|
+
minifyLineagesInDoc,
|
|
60
|
+
pluckIdsFromLineage,
|
|
61
|
+
replaceParentLineage,
|
|
62
|
+
replaceContactLineage,
|
|
63
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
function replaceLineage(doc, lineageAttributeName, params) {
|
|
2
|
+
// Replace the full lineage
|
|
3
|
+
if (!params.startingFromId) {
|
|
4
|
+
return replaceEntireLineage(doc, lineageAttributeName, params.replaceWith);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const selectedFunction = params.merge ? replaceLineageForMerge : replaceLineageForMove;
|
|
8
|
+
return selectedFunction(doc, lineageAttributeName, params);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function replaceLineageForMove(doc, lineageAttributeName, params) {
|
|
12
|
+
let currentElement = doc[lineageAttributeName];
|
|
13
|
+
while (currentElement) {
|
|
14
|
+
if (currentElement?._id === params.startingFromId) {
|
|
15
|
+
return replaceEntireLineage(currentElement, 'parent', params.replaceWith);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
currentElement = currentElement.parent;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function replaceLineageForMerge(doc, lineageAttributeName, params) {
|
|
25
|
+
let currentElement = doc;
|
|
26
|
+
let currentAttributeName = lineageAttributeName;
|
|
27
|
+
while (currentElement) {
|
|
28
|
+
if (currentElement[currentAttributeName]?._id === params.startingFromId) {
|
|
29
|
+
return replaceEntireLineage(currentElement, currentAttributeName, params.replaceWith);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
currentElement = currentElement[currentAttributeName];
|
|
33
|
+
currentAttributeName = 'parent';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function replaceEntireLineage(replaceInDoc, lineageAttributeName, replaceWith) {
|
|
40
|
+
if (!replaceWith) {
|
|
41
|
+
const lineageWasDeleted = !!replaceInDoc[lineageAttributeName];
|
|
42
|
+
replaceInDoc[lineageAttributeName] = undefined;
|
|
43
|
+
return lineageWasDeleted;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
replaceInDoc[lineageAttributeName] = replaceWith;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
/**
|
|
52
|
+
* Given a doc, replace the parent's lineage
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} doc A CouchDB document containing a parent lineage (eg. parent.parent._id)
|
|
55
|
+
* @param {Object} params
|
|
56
|
+
* @param {Object} params.replaceWith The new hierarchy { parent: { _id: 'parent', parent: { _id: 'grandparent' } }
|
|
57
|
+
* @param {string} params.startingFromId Only the part of the lineage "after" this id will be replaced
|
|
58
|
+
* @param {boolean} params.merge When true, startingFromId is replaced and when false, startingFromId's parent is replaced
|
|
59
|
+
*/
|
|
60
|
+
replaceParentLineage: (doc, params) => {
|
|
61
|
+
return replaceLineage(doc, 'parent', params);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Given a doc, replace the contact's lineage
|
|
66
|
+
*
|
|
67
|
+
* @param {Object} doc A CouchDB document containing a contact lineage (eg. contact.parent._id)
|
|
68
|
+
* @param {Object} params
|
|
69
|
+
* @param {Object} params.replaceWith The new hierarchy { parent: { _id: 'parent', parent: { _id: 'grandparent' } }
|
|
70
|
+
* @param {string} params.startingFromId Only the part of the lineage "after" this id will be replaced
|
|
71
|
+
* @param {boolean} params.merge When true, startingFromId is replaced and when false, startingFromId's parent is replaced
|
|
72
|
+
*/
|
|
73
|
+
replaceContactLineage: (doc, params) => {
|
|
74
|
+
return replaceLineage(doc, 'contact', params);
|
|
75
|
+
},
|
|
76
|
+
};
|
package/src/lib/user-prompt.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const environment = require('./environment');
|
|
2
2
|
const readline = require('readline-sync');
|
|
3
|
+
const { warn } = require('./log');
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -33,8 +34,16 @@ function keyInSelect(items, question, options = {}) {
|
|
|
33
34
|
return readline.keyInSelect(items, question, options);
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function warnPromptAbort(warningMessage) {
|
|
38
|
+
warn(warningMessage);
|
|
39
|
+
if (!keyInYN()) {
|
|
40
|
+
throw new Error('User aborted execution.');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
module.exports = {
|
|
37
45
|
keyInYN,
|
|
38
46
|
question,
|
|
39
|
-
keyInSelect
|
|
47
|
+
keyInSelect,
|
|
48
|
+
warnPromptAbort,
|
|
40
49
|
};
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
const log = require('./log');
|
|
2
|
-
const { trace } = log;
|
|
3
|
-
|
|
4
|
-
const { pluckIdsFromLineage } = require('./lineage-manipulation');
|
|
5
|
-
|
|
6
|
-
const lineageConstraints = async (repository, parentDoc) => {
|
|
7
|
-
let mapTypeToAllowedParents;
|
|
8
|
-
try {
|
|
9
|
-
const { settings } = await repository.get('settings');
|
|
10
|
-
const { contact_types } = settings;
|
|
11
|
-
|
|
12
|
-
if (Array.isArray(contact_types)) {
|
|
13
|
-
trace('Found app_settings.contact_types. Configurable hierarchy constraints will be enforced.');
|
|
14
|
-
mapTypeToAllowedParents = contact_types
|
|
15
|
-
.filter(rule => rule)
|
|
16
|
-
.reduce((agg, curr) => Object.assign(agg, { [curr.id]: curr.parents }), {});
|
|
17
|
-
}
|
|
18
|
-
} catch (err) {
|
|
19
|
-
if (err.name !== 'not_found') {
|
|
20
|
-
throw err;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (!mapTypeToAllowedParents) {
|
|
25
|
-
trace('Default hierarchy constraints will be enforced.');
|
|
26
|
-
mapTypeToAllowedParents = {
|
|
27
|
-
district_hospital: [],
|
|
28
|
-
health_center: ['district_hospital'],
|
|
29
|
-
clinic: ['health_center'],
|
|
30
|
-
person: ['district_hospital', 'health_center', 'clinic'],
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
getHierarchyErrors: contactDoc => getHierarchyViolations(mapTypeToAllowedParents, contactDoc, parentDoc),
|
|
36
|
-
getPrimaryContactViolations: (contactDoc, descendantDocs) => getPrimaryContactViolations(repository, contactDoc, parentDoc, descendantDocs),
|
|
37
|
-
};
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/*
|
|
41
|
-
Enforce the list of allowed parents for each contact type
|
|
42
|
-
Ensure we are not creating a circular hierarchy
|
|
43
|
-
*/
|
|
44
|
-
const getHierarchyViolations = (mapTypeToAllowedParents, contactDoc, parentDoc) => {
|
|
45
|
-
const getContactType = doc => doc && (doc.type === 'contact' ? doc.contact_type : doc.type);
|
|
46
|
-
const contactType = getContactType(contactDoc);
|
|
47
|
-
const parentType = getContactType(parentDoc);
|
|
48
|
-
if (!contactType) return 'contact required attribute "type" is undefined';
|
|
49
|
-
if (parentDoc && !parentType) return `parent contact "${parentDoc._id}" required attribute "type" is undefined`;
|
|
50
|
-
if (!mapTypeToAllowedParents) return 'hierarchy constraints are undefined';
|
|
51
|
-
|
|
52
|
-
const rulesForContact = mapTypeToAllowedParents[contactType];
|
|
53
|
-
if (!rulesForContact) return `cannot move contact with unknown type '${contactType}'`;
|
|
54
|
-
|
|
55
|
-
const isPermittedMoveToRoot = !parentDoc && rulesForContact.length === 0;
|
|
56
|
-
if (!isPermittedMoveToRoot && !rulesForContact.includes(parentType)) return `contacts of type '${contactType}' cannot have parent of type '${parentType}'`;
|
|
57
|
-
|
|
58
|
-
if (parentDoc && contactDoc._id) {
|
|
59
|
-
const parentAncestry = [parentDoc._id, ...pluckIdsFromLineage(parentDoc.parent)];
|
|
60
|
-
if (parentAncestry.includes(contactDoc._id)) {
|
|
61
|
-
return `Circular hierarchy: Cannot set parent of contact '${contactDoc._id}' as it would create a circular hierarchy.`;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/*
|
|
67
|
-
A place's primary contact must be a descendant of that place.
|
|
68
|
-
|
|
69
|
-
1. Check to see which part of the contact's lineage will be removed
|
|
70
|
-
2. For each removed part of the contact's lineage, confirm that place's primary contact isn't being removed.
|
|
71
|
-
*/
|
|
72
|
-
const getPrimaryContactViolations = async (repository, contactDoc, parentDoc, descendantDocs) => {
|
|
73
|
-
const safeGetLineageFromDoc = doc => doc ? pluckIdsFromLineage(doc.parent) : [];
|
|
74
|
-
const contactsLineageIds = safeGetLineageFromDoc(contactDoc);
|
|
75
|
-
const parentsLineageIds = safeGetLineageFromDoc(parentDoc);
|
|
76
|
-
|
|
77
|
-
if (parentDoc) {
|
|
78
|
-
parentsLineageIds.push(parentDoc._id);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const docIdsRemovedFromContactLineage = contactsLineageIds.filter(value => !parentsLineageIds.includes(value));
|
|
82
|
-
const docsRemovedFromContactLineage = await repository.allDocs({
|
|
83
|
-
keys: docIdsRemovedFromContactLineage,
|
|
84
|
-
include_docs: true,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const primaryContactIds = docsRemovedFromContactLineage.rows
|
|
88
|
-
.map(row => row.doc && row.doc.contact && row.doc.contact._id)
|
|
89
|
-
.filter(id => id);
|
|
90
|
-
|
|
91
|
-
return descendantDocs.find(descendant => primaryContactIds.some(primaryId => descendant._id === primaryId));
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
module.exports = lineageConstraints;
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
/*
|
|
3
|
-
Given a doc, replace the lineage information therein with "replaceWith"
|
|
4
|
-
|
|
5
|
-
startingFromIdInLineage (optional) - Will result in a partial replacement of the lineage. Only the part of the lineage "after" the parent
|
|
6
|
-
with _id=startingFromIdInLineage will be replaced by "replaceWith"
|
|
7
|
-
*/
|
|
8
|
-
const replaceLineage = (doc, lineageAttributeName, replaceWith, startingFromIdInLineage) => {
|
|
9
|
-
const handleReplacement = (replaceInDoc, docAttr, replaceWith) => {
|
|
10
|
-
if (!replaceWith) {
|
|
11
|
-
const lineageWasDeleted = !!replaceInDoc[docAttr];
|
|
12
|
-
replaceInDoc[docAttr] = undefined;
|
|
13
|
-
return lineageWasDeleted;
|
|
14
|
-
} else if (replaceInDoc[docAttr]) {
|
|
15
|
-
replaceInDoc[docAttr]._id = replaceWith._id;
|
|
16
|
-
replaceInDoc[docAttr].parent = replaceWith.parent;
|
|
17
|
-
} else {
|
|
18
|
-
replaceInDoc[docAttr] = replaceWith;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return true;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// Replace the full lineage
|
|
25
|
-
if (!startingFromIdInLineage) {
|
|
26
|
-
return handleReplacement(doc, lineageAttributeName, replaceWith);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Replace part of a lineage
|
|
30
|
-
let currentParent = doc[lineageAttributeName];
|
|
31
|
-
while (currentParent) {
|
|
32
|
-
if (currentParent._id === startingFromIdInLineage) {
|
|
33
|
-
return handleReplacement(currentParent, 'parent', replaceWith);
|
|
34
|
-
}
|
|
35
|
-
currentParent = currentParent.parent;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return false;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/*
|
|
42
|
-
Function borrowed from shared-lib/lineage
|
|
43
|
-
*/
|
|
44
|
-
const minifyLineagesInDoc = doc => {
|
|
45
|
-
const minifyLineage = lineage => {
|
|
46
|
-
if (!lineage || !lineage._id) {
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const result = {
|
|
51
|
-
_id: lineage._id,
|
|
52
|
-
parent: minifyLineage(lineage.parent),
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
return result;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
if (!doc) {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if ('parent' in doc) {
|
|
63
|
-
doc.parent = minifyLineage(doc.parent);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if ('contact' in doc) {
|
|
67
|
-
doc.contact = minifyLineage(doc.contact);
|
|
68
|
-
if (doc.contact && !doc.contact.parent) delete doc.contact.parent; // for unit test clarity
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (doc.type === 'data_record') {
|
|
72
|
-
delete doc.patient;
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const createLineageFromDoc = doc => {
|
|
77
|
-
if (!doc) {
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
_id: doc._id,
|
|
83
|
-
parent: doc.parent || undefined,
|
|
84
|
-
};
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/*
|
|
88
|
-
Given a lineage, return the ids therein
|
|
89
|
-
*/
|
|
90
|
-
const pluckIdsFromLineage = lineage => {
|
|
91
|
-
const result = [];
|
|
92
|
-
|
|
93
|
-
let current = lineage;
|
|
94
|
-
while (current) {
|
|
95
|
-
result.push(current._id);
|
|
96
|
-
current = current.parent;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return result;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
module.exports = {
|
|
103
|
-
createLineageFromDoc,
|
|
104
|
-
minifyLineagesInDoc,
|
|
105
|
-
pluckIdsFromLineage,
|
|
106
|
-
replaceLineage,
|
|
107
|
-
};
|