cht-conf 6.0.2 → 6.1.1
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/delete-contacts.js +1 -1
- package/src/fn/merge-contacts.js +5 -0
- package/src/lib/api.js +15 -12
- package/src/lib/hierarchy-operations/delete-hierarchy.js +1 -1
- package/src/lib/hierarchy-operations/hierarchy-data-source.js +36 -22
- package/src/lib/hierarchy-operations/index.js +94 -29
- package/src/lib/hierarchy-operations/lineage-constraints.js +46 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [6.1.1](https://github.com/medic/cht-conf/compare/v6.1.0...v6.1.1) (2026-04-10)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **#799:** batch contact IDs in move-contacts to avoid URI length errors ([#802](https://github.com/medic/cht-conf/issues/802)) ([08e92a5](https://github.com/medic/cht-conf/commit/08e92a568196bdcb3e68076a760fbd8a5b7ff461)), closes [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799) [#799](https://github.com/medic/cht-conf/issues/799)
|
|
7
|
+
|
|
8
|
+
# [6.1.0](https://github.com/medic/cht-conf/compare/v6.0.2...v6.1.0) (2026-04-02)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **#653:** add --merge-primary-contacts flag for merge-contacts action ([#654](https://github.com/medic/cht-conf/issues/654)) ([def533d](https://github.com/medic/cht-conf/commit/def533dbca20abca8303aea7f314921c6a1c166b)), closes [#653](https://github.com/medic/cht-conf/issues/653)
|
|
14
|
+
|
|
1
15
|
## [6.0.2](https://github.com/medic/cht-conf/compare/v6.0.1...v6.0.2) (2026-01-16)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -57,7 +57,7 @@ ${bold('OPTIONS')}
|
|
|
57
57
|
A comma delimited list of ids of contacts to be deleted.
|
|
58
58
|
|
|
59
59
|
--disable-users
|
|
60
|
-
When flag is present, users at any deleted place will be permanently disabled.
|
|
60
|
+
When flag is present, users at any deleted place will be updated and may be permanently disabled. Supported by CHT Core 4.7 and above.
|
|
61
61
|
|
|
62
62
|
--docDirectoryPath=<path to stage docs>
|
|
63
63
|
Specifies the folder used to store the documents representing the changes in hierarchy.
|
package/src/fn/merge-contacts.js
CHANGED
|
@@ -15,6 +15,7 @@ module.exports = {
|
|
|
15
15
|
const options = {
|
|
16
16
|
disableUsers: args.disableUsers,
|
|
17
17
|
docDirectoryPath: args.docDirectoryPath,
|
|
18
|
+
mergePrimaryContacts: args.mergePrimaryContacts,
|
|
18
19
|
force: args.force,
|
|
19
20
|
};
|
|
20
21
|
return HierarchyOperations(db, options).merge(args.sourceIds, args.destinationId);
|
|
@@ -50,6 +51,7 @@ const parseExtraArgs = (projectDir, extraArgs = []) => {
|
|
|
50
51
|
sourceIds,
|
|
51
52
|
disableUsers: !!args['disable-users'],
|
|
52
53
|
docDirectoryPath: path.resolve(projectDir, args.docDirectoryPath || 'json_docs'),
|
|
54
|
+
mergePrimaryContacts: !!args['merge-primary-contacts'],
|
|
53
55
|
force: !!args.force,
|
|
54
56
|
};
|
|
55
57
|
};
|
|
@@ -75,6 +77,9 @@ ${bold('OPTIONS')}
|
|
|
75
77
|
--disable-users
|
|
76
78
|
When flag is present, users at any deleted place will be updated and may be permanently disabled. Supported by CHT Core 4.7 and above.
|
|
77
79
|
|
|
80
|
+
--merge-primary-contacts
|
|
81
|
+
When flag is present, the primary contacts for all the top-level places will also be merged into a single resulting contact.
|
|
82
|
+
|
|
78
83
|
--docDirectoryPath=<path to stage docs>
|
|
79
84
|
Specifies the folder used to store the documents representing the changes in hierarchy.
|
|
80
85
|
`);
|
package/src/lib/api.js
CHANGED
|
@@ -256,20 +256,23 @@ const api = {
|
|
|
256
256
|
}
|
|
257
257
|
},
|
|
258
258
|
|
|
259
|
-
async
|
|
259
|
+
async getReportsByFreetext (createdByIds, limit, bookmark) {
|
|
260
260
|
const queryString = createdByIds.map(id => `exact_match:"contact:${id}"`).join(' OR ');
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
261
|
+
const body = {
|
|
262
|
+
// sorting by this field ensures that the output are same with the clouseau views
|
|
263
|
+
// https://github.com/medic/cht-core/pull/9541
|
|
264
|
+
sort: 'reported_date',
|
|
265
|
+
q: queryString,
|
|
266
|
+
include_docs: true,
|
|
267
|
+
limit,
|
|
268
|
+
};
|
|
269
|
+
if (bookmark) {
|
|
270
|
+
body.bookmark = bookmark;
|
|
271
|
+
}
|
|
272
|
+
const res = await request.post(`${environment.apiUrl}/_design/medic/_nouveau/reports_by_freetext`, {
|
|
273
|
+
body, json: true
|
|
271
274
|
});
|
|
272
|
-
return res.hits.map(item => item.doc);
|
|
275
|
+
return { docs: res.hits.map(item => item.doc), bookmark: res.bookmark };
|
|
273
276
|
}
|
|
274
277
|
};
|
|
275
278
|
|
|
@@ -34,7 +34,7 @@ async function deleteReportsForContact(db, options, contact) {
|
|
|
34
34
|
let skip = 0;
|
|
35
35
|
let reportBatch;
|
|
36
36
|
do {
|
|
37
|
-
reportBatch = await DataSource.
|
|
37
|
+
reportBatch = await DataSource.fetchReportsBySubject(db, [contact._id], skip);
|
|
38
38
|
|
|
39
39
|
for (const report of reportBatch) {
|
|
40
40
|
JsDocs.deleteDoc(options, report);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const lineageManipulation = require('./lineage-manipulation');
|
|
2
|
-
const {getValidApiVersion} = require('../get-api-version');
|
|
2
|
+
const { getValidApiVersion } = require('../get-api-version');
|
|
3
3
|
const semver = require('semver');
|
|
4
4
|
const api = require('../api');
|
|
5
5
|
|
|
6
6
|
const HIERARCHY_ROOT = 'root';
|
|
7
7
|
const BATCH_SIZE = 10000;
|
|
8
|
+
const QUERY_IDS_BATCH_SIZE = 100;
|
|
8
9
|
const NOUVEAU_MIN_VERSION = '5.0.0';
|
|
10
|
+
const SUBJECT_IDS = ['patient_id', 'patient_uuid', 'place_id', 'place_uuid'];
|
|
9
11
|
|
|
10
12
|
/*
|
|
11
13
|
Fetches all of the documents associated with the "contactIds" and confirms they exist.
|
|
@@ -70,37 +72,46 @@ const getFromDbView = async (db, view, keys, skip) => {
|
|
|
70
72
|
return res.rows.map(row => row.doc);
|
|
71
73
|
};
|
|
72
74
|
|
|
73
|
-
const
|
|
75
|
+
const useNouveauSearch = async () => {
|
|
76
|
+
const coreVersion = await getValidApiVersion();
|
|
77
|
+
return coreVersion && semver.gte(coreVersion, NOUVEAU_MIN_VERSION);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const fetchReportsByCreator = async (db, createdByIds, cursor, useNouveau) => {
|
|
74
81
|
if (createdByIds.length === 0) {
|
|
75
|
-
return [];
|
|
82
|
+
return { docs: [], cursor: null };
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return await api().getReportsByCreatedByIds(createdByIds, BATCH_SIZE, skip);
|
|
85
|
+
if (useNouveau) {
|
|
86
|
+
return await fetchAllReportsFromNouveau(createdByIds);
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
const skip = cursor || 0;
|
|
83
90
|
const createdByKeys = createdByIds.map(id => [`contact:${id}`]);
|
|
84
|
-
|
|
91
|
+
const docs = await getFromDbView(db, 'medic-client/reports_by_freetext', createdByKeys, skip);
|
|
92
|
+
return { docs, cursor: skip + docs.length };
|
|
85
93
|
};
|
|
86
94
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
const fetchAllReportsFromNouveau = async (createdByIds) => {
|
|
96
|
+
const allDocs = [];
|
|
97
|
+
for (let i = 0; i < createdByIds.length; i += QUERY_IDS_BATCH_SIZE) {
|
|
98
|
+
const idsBatch = createdByIds.slice(i, i + QUERY_IDS_BATCH_SIZE);
|
|
99
|
+
let bookmark = null;
|
|
100
|
+
let batch;
|
|
101
|
+
do {
|
|
102
|
+
batch = await api().getReportsByFreetext(idsBatch, BATCH_SIZE, bookmark);
|
|
103
|
+
allDocs.push(...batch.docs);
|
|
104
|
+
bookmark = batch.bookmark;
|
|
105
|
+
} while (batch.docs.length >= BATCH_SIZE);
|
|
90
106
|
}
|
|
91
|
-
return
|
|
107
|
+
return { docs: allDocs, cursor: null };
|
|
92
108
|
};
|
|
93
109
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const allRows = [...creatorReports, ...subjectReports];
|
|
101
|
-
|
|
102
|
-
const docsWithId = allRows.map(( doc ) => [doc._id, doc]);
|
|
103
|
-
return Array.from(new Map(docsWithId).values());
|
|
110
|
+
const fetchReportsBySubject = async (db, createdAtIds, skip) => {
|
|
111
|
+
if (!createdAtIds || createdAtIds.length <= 0) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
return await getFromDbView(db, 'medic-client/reports_by_subject', createdAtIds, skip);
|
|
104
115
|
};
|
|
105
116
|
|
|
106
117
|
async function getAncestorsOf(db, contactDoc) {
|
|
@@ -124,9 +135,12 @@ async function getAncestorsOf(db, contactDoc) {
|
|
|
124
135
|
module.exports = {
|
|
125
136
|
BATCH_SIZE,
|
|
126
137
|
HIERARCHY_ROOT,
|
|
138
|
+
SUBJECT_IDS,
|
|
127
139
|
getAncestorsOf,
|
|
128
140
|
getContactWithDescendants,
|
|
129
141
|
getContact,
|
|
130
142
|
getContactsByIds,
|
|
131
|
-
|
|
143
|
+
useNouveauSearch,
|
|
144
|
+
fetchReportsByCreator,
|
|
145
|
+
fetchReportsBySubject,
|
|
132
146
|
};
|
|
@@ -19,27 +19,27 @@ async function moveHierarchy(db, options, sourceIds, destinationId) {
|
|
|
19
19
|
for (const sourceId of sourceIds) {
|
|
20
20
|
const sourceDoc = sourceDocs[sourceId];
|
|
21
21
|
const descendantsAndSelf = await DataSource.getContactWithDescendants(db, sourceId);
|
|
22
|
+
|
|
22
23
|
const moveContext = {
|
|
23
24
|
sourceId,
|
|
24
25
|
destinationId,
|
|
25
26
|
descendantsAndSelf,
|
|
26
27
|
replacementLineage,
|
|
27
28
|
merge: !!options.merge,
|
|
29
|
+
disableUsers: !!options.disableUsers,
|
|
30
|
+
mergePrimaryContacts: !!options.mergePrimaryContacts,
|
|
31
|
+
sourcePrimaryContactId: getPrimaryContactId(sourceDoc),
|
|
32
|
+
destinationPrimaryContactId: getPrimaryContactId(destinationDoc),
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
await constraints.assertNoPrimaryContactViolations(sourceDoc, destinationDoc, descendantsAndSelf);
|
|
31
36
|
|
|
32
|
-
if (options.merge) {
|
|
33
|
-
const toDeleteUsers = options.disableUsers && constraints.isPlace(sourceDoc);
|
|
34
|
-
JsDocs.deleteDoc(options, sourceDoc, toDeleteUsers);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
37
|
const prettyPrintDocument = doc => `'${doc.name}' (${doc._id})`;
|
|
38
38
|
trace(
|
|
39
39
|
`Considering lineage updates to ${descendantsAndSelf.length} descendant(s) of contact `
|
|
40
40
|
+ `${prettyPrintDocument(sourceDoc)}.`
|
|
41
41
|
);
|
|
42
|
-
const updatedDescendants =
|
|
42
|
+
const updatedDescendants = updateContacts(moveContext, constraints);
|
|
43
43
|
|
|
44
44
|
const ancestors = await DataSource.getAncestorsOf(db, sourceDoc);
|
|
45
45
|
trace(
|
|
@@ -68,46 +68,94 @@ async function moveHierarchy(db, options, sourceIds, destinationId) {
|
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function getPrimaryContactId(doc) {
|
|
72
|
+
return typeof doc?.contact === 'string' ? doc.contact : doc?.contact?._id;
|
|
73
|
+
}
|
|
74
|
+
|
|
71
75
|
async function updateReports(db, options, moveContext) {
|
|
72
76
|
const descendantIds = moveContext.descendantsAndSelf.map(contact => contact._id);
|
|
77
|
+
const createdAtIds = getReportsCreatedAtIds(moveContext);
|
|
78
|
+
|
|
79
|
+
let totalCount = 0;
|
|
80
|
+
const useNouveau = await DataSource.useNouveauSearch();
|
|
81
|
+
|
|
82
|
+
let cursor = null;
|
|
83
|
+
let result;
|
|
84
|
+
do {
|
|
85
|
+
info(`Processing creator reports ${totalCount} to ${totalCount + DataSource.BATCH_SIZE}`);
|
|
86
|
+
result = await DataSource.fetchReportsByCreator(db, descendantIds, cursor, useNouveau);
|
|
87
|
+
processAndWriteReportBatch(result.docs, options, moveContext);
|
|
88
|
+
cursor = result.cursor;
|
|
89
|
+
totalCount += result.docs.length;
|
|
90
|
+
} while (result.cursor && result.docs.length >= DataSource.BATCH_SIZE);
|
|
73
91
|
|
|
74
92
|
let skip = 0;
|
|
75
|
-
let
|
|
93
|
+
let batch;
|
|
76
94
|
do {
|
|
77
|
-
info(`Processing ${skip} to ${skip + DataSource.BATCH_SIZE}
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
info(`Processing subject reports ${skip} to ${skip + DataSource.BATCH_SIZE}`);
|
|
96
|
+
batch = await DataSource.fetchReportsBySubject(db, createdAtIds, skip);
|
|
97
|
+
processAndWriteReportBatch(batch, options, moveContext);
|
|
98
|
+
skip += batch.length;
|
|
99
|
+
totalCount += batch.length;
|
|
100
|
+
} while (batch.length >= DataSource.BATCH_SIZE);
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const updatedReports = reportDocsBatch.filter(doc => lineageUpdates.has(doc._id) || reassignUpdates.has(doc._id));
|
|
102
|
+
return totalCount;
|
|
103
|
+
}
|
|
84
104
|
|
|
85
|
-
|
|
105
|
+
function processAndWriteReportBatch(batch, options, moveContext) {
|
|
106
|
+
const lineageUpdates = replaceLineageOfReportCreator(batch, moveContext);
|
|
107
|
+
const reassignUpdates = reassignReports(batch, moveContext);
|
|
108
|
+
const updatedReports = batch.filter(doc => lineageUpdates.has(doc._id) || reassignUpdates.has(doc._id));
|
|
109
|
+
minifyLineageAndWriteToDisk(options, updatedReports);
|
|
110
|
+
}
|
|
86
111
|
|
|
87
|
-
|
|
88
|
-
|
|
112
|
+
function getReportsCreatedAtIds(moveContext) {
|
|
113
|
+
const result = [];
|
|
114
|
+
if (moveContext.merge) {
|
|
115
|
+
result.push(moveContext.sourceId);
|
|
116
|
+
}
|
|
89
117
|
|
|
90
|
-
|
|
118
|
+
if (moveContext.mergePrimaryContacts && moveContext.sourcePrimaryContactId) {
|
|
119
|
+
result.push(moveContext.sourcePrimaryContactId);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
91
123
|
}
|
|
92
124
|
|
|
93
|
-
function reassignReportSubjects(report,
|
|
94
|
-
const SUBJECT_IDS = ['patient_id', 'patient_uuid', 'place_id', 'place_uuid'];
|
|
125
|
+
function reassignReportSubjects(report, moveContext) {
|
|
95
126
|
let updated = false;
|
|
96
|
-
for (const subjectId of SUBJECT_IDS) {
|
|
97
|
-
|
|
98
|
-
report[subjectId] = destinationId;
|
|
99
|
-
updated = true;
|
|
100
|
-
}
|
|
127
|
+
for (const subjectId of DataSource.SUBJECT_IDS) {
|
|
128
|
+
updated = updated || reassignSingleReport(report, subjectId, moveContext.sourceId, moveContext.destinationId);
|
|
101
129
|
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
if (
|
|
131
|
+
moveContext.mergePrimaryContacts &&
|
|
132
|
+
moveContext.sourcePrimaryContactId &&
|
|
133
|
+
moveContext.destinationPrimaryContactId
|
|
134
|
+
) {
|
|
135
|
+
updated = updated || reassignSingleReport(
|
|
136
|
+
report, subjectId, moveContext.sourcePrimaryContactId, moveContext.destinationPrimaryContactId
|
|
137
|
+
);
|
|
105
138
|
}
|
|
106
139
|
}
|
|
107
140
|
|
|
108
141
|
return updated;
|
|
109
142
|
}
|
|
110
143
|
|
|
144
|
+
function reassignSingleReport(report, subjectId, matchId, resultingId) {
|
|
145
|
+
let result = false;
|
|
146
|
+
if (report[subjectId] === matchId) {
|
|
147
|
+
report[subjectId] = resultingId;
|
|
148
|
+
result = true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (report.fields[subjectId] === matchId) {
|
|
152
|
+
report.fields[subjectId] = resultingId;
|
|
153
|
+
result = true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
111
159
|
function reassignReports(reports, moveContext) {
|
|
112
160
|
const updated = new Set();
|
|
113
161
|
if (!moveContext.merge) {
|
|
@@ -179,9 +227,26 @@ function replaceLineageInSingleContact(doc, moveContext) {
|
|
|
179
227
|
}
|
|
180
228
|
}
|
|
181
229
|
|
|
182
|
-
function
|
|
230
|
+
function updateContacts(moveContext, constraints) {
|
|
183
231
|
return moveContext.descendantsAndSelf
|
|
184
|
-
.map(descendant =>
|
|
232
|
+
.map(descendant => {
|
|
233
|
+
const deleteSource = moveContext.merge && descendant._id === moveContext.sourceId;
|
|
234
|
+
const deletePrimaryContact = moveContext.mergePrimaryContacts
|
|
235
|
+
&& descendant._id === moveContext.sourcePrimaryContactId
|
|
236
|
+
&& moveContext.destinationPrimaryContactId;
|
|
237
|
+
|
|
238
|
+
if (deleteSource || deletePrimaryContact) {
|
|
239
|
+
const toDeleteUsers = moveContext.disableUsers && constraints.isPlace(descendant);
|
|
240
|
+
return {
|
|
241
|
+
_id: descendant._id,
|
|
242
|
+
_rev: descendant._rev,
|
|
243
|
+
_deleted: true,
|
|
244
|
+
cht_disable_linked_users: !!toDeleteUsers,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return replaceLineageInSingleContact(descendant, moveContext);
|
|
249
|
+
})
|
|
185
250
|
.filter(Boolean);
|
|
186
251
|
}
|
|
187
252
|
|
|
@@ -9,13 +9,8 @@ module.exports = async (db, options) => {
|
|
|
9
9
|
|
|
10
10
|
return {
|
|
11
11
|
assertNoPrimaryContactViolations: async (sourceDoc, destinationDoc, descendantDocs) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
throw Error(
|
|
15
|
-
`Cannot remove contact '${invalidPrimaryContactDoc?.name}' (${invalidPrimaryContactDoc?._id}) from the `
|
|
16
|
-
+ 'hierarchy for which they are a primary contact.'
|
|
17
|
-
);
|
|
18
|
-
}
|
|
12
|
+
await assertOnPrimaryContactRemoval(db, sourceDoc, destinationDoc, descendantDocs);
|
|
13
|
+
await assertSourcePrimaryContactType(db, contactTypeInfo, sourceDoc);
|
|
19
14
|
},
|
|
20
15
|
assertNoHierarchyErrors: (sourceDocs, destinationDoc) => {
|
|
21
16
|
|
|
@@ -30,7 +25,7 @@ module.exports = async (db, options) => {
|
|
|
30
25
|
throw Error(`Hierarchy Constraints: ${hierarchyError}`);
|
|
31
26
|
}
|
|
32
27
|
});
|
|
33
|
-
|
|
28
|
+
|
|
34
29
|
/*
|
|
35
30
|
* It is nice that the tool can move lists of contacts as one operation, but strange things happen when two
|
|
36
31
|
* contactIds are in the same lineage. For example, moving a district_hospital and moving a contact under that
|
|
@@ -47,13 +42,16 @@ module.exports = async (db, options) => {
|
|
|
47
42
|
});
|
|
48
43
|
},
|
|
49
44
|
|
|
50
|
-
isPlace: (contact) =>
|
|
51
|
-
const contactType = getContactType(contact);
|
|
52
|
-
return !contactTypeInfo[contactType]?.person;
|
|
53
|
-
},
|
|
45
|
+
isPlace: (contact) => isPlace(contactTypeInfo, contact),
|
|
54
46
|
};
|
|
55
47
|
};
|
|
56
48
|
|
|
49
|
+
function isPlace(contactTypeInfo, contact) {
|
|
50
|
+
const contactType = getContactType(contact);
|
|
51
|
+
const isPerson = contactTypeInfo[contactType]?.person || false;
|
|
52
|
+
return !isPerson;
|
|
53
|
+
}
|
|
54
|
+
|
|
57
55
|
/*
|
|
58
56
|
Enforce the list of allowed parents for each contact type
|
|
59
57
|
Ensure we are not creating a circular hierarchy
|
|
@@ -133,24 +131,50 @@ A place's primary contact must be a descendant of that place.
|
|
|
133
131
|
1. Check to see which part of the contact's lineage will be removed
|
|
134
132
|
2. For each removed part of the contact's lineage, confirm that place's primary contact isn't being removed.
|
|
135
133
|
*/
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
const
|
|
134
|
+
async function assertOnPrimaryContactRemoval(db, sourceDoc, destinationDoc, descendantDocs) {
|
|
135
|
+
const sourceLineageIds = lineageManipulation.pluckIdsFromLineage(sourceDoc?.parent);
|
|
136
|
+
const destinationLineageIds = lineageManipulation.pluckIdsFromLineage(destinationDoc);
|
|
139
137
|
|
|
140
|
-
const docIdsRemovedFromContactLineage =
|
|
138
|
+
const docIdsRemovedFromContactLineage = sourceLineageIds.filter(value => !destinationLineageIds.includes(value));
|
|
141
139
|
const docsRemovedFromContactLineage = await db.allDocs({
|
|
142
140
|
keys: docIdsRemovedFromContactLineage,
|
|
143
141
|
include_docs: true,
|
|
144
142
|
});
|
|
145
143
|
|
|
146
|
-
const primaryContactIds = docsRemovedFromContactLineage.rows
|
|
144
|
+
const primaryContactIds = new Set(docsRemovedFromContactLineage.rows
|
|
147
145
|
.map(row => row?.doc?.contact?._id)
|
|
148
|
-
.filter(Boolean);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
146
|
+
.filter(Boolean));
|
|
147
|
+
|
|
148
|
+
const invalidPrimaryContactDoc = descendantDocs.find(
|
|
149
|
+
descendant => primaryContactIds.has(descendant._id)
|
|
150
|
+
);
|
|
151
|
+
if (invalidPrimaryContactDoc) {
|
|
152
|
+
throw new Error('Cannot remove contact '+
|
|
153
|
+
`'${invalidPrimaryContactDoc?.name}' (${invalidPrimaryContactDoc?._id}) ` +
|
|
154
|
+
'from the hierarchy for which they are a primary contact.');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function assertSourcePrimaryContactType(db, contactTypeInfo, sourceDoc) {
|
|
159
|
+
const sourcePrimaryContactId = getPrimaryContactId(sourceDoc);
|
|
160
|
+
if (!sourcePrimaryContactId) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const sourcePrimaryContactDoc = await db.get(sourcePrimaryContactId);
|
|
165
|
+
const primaryContactIsPlace = isPlace(contactTypeInfo, sourcePrimaryContactDoc);
|
|
166
|
+
if (primaryContactIsPlace) {
|
|
167
|
+
throw new Error(`Source "${sourceDoc._id}" has primary contact "${sourcePrimaryContactId}" which is of type place`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getContactType(doc) {
|
|
172
|
+
return doc?.type === 'contact' ? doc?.contact_type : doc?.type;
|
|
173
|
+
}
|
|
152
174
|
|
|
153
|
-
|
|
175
|
+
function getPrimaryContactId(doc) {
|
|
176
|
+
return typeof doc?.contact === 'string' ? doc.contact : doc?.contact?._id;
|
|
177
|
+
}
|
|
154
178
|
|
|
155
179
|
async function fetchContactTypeInfo(db) {
|
|
156
180
|
try {
|