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
package/src/fn/upload-docs.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const minimist = require('minimist');
|
|
3
|
-
const
|
|
3
|
+
const semver = require('semver');
|
|
4
4
|
|
|
5
|
+
const api = require('../lib/api');
|
|
5
6
|
const environment = require('../lib/environment');
|
|
6
7
|
const fs = require('../lib/sync-fs');
|
|
8
|
+
const { getValidApiVersion } = require('../lib/get-api-version');
|
|
7
9
|
const log = require('../lib/log');
|
|
8
10
|
const pouch = require('../lib/db');
|
|
9
11
|
const progressBar = require('../lib/progress-bar');
|
|
12
|
+
const userPrompt = require('../lib/user-prompt');
|
|
10
13
|
|
|
11
14
|
const { info, trace, warn } = log;
|
|
12
15
|
|
|
13
16
|
const FILE_EXTENSION = '.doc.json';
|
|
14
17
|
const INITIAL_BATCH_SIZE = 100;
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
async function execute() {
|
|
17
20
|
const args = minimist(environment.extraArgs || [], { boolean: true });
|
|
18
21
|
|
|
19
22
|
const docDir = path.resolve(environment.pathToProject, args.docDirectoryPath || 'json_docs');
|
|
@@ -22,22 +25,23 @@ const execute = async () => {
|
|
|
22
25
|
return Promise.resolve();
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
if (docIdErrors.length > 0) {
|
|
28
|
-
throw new Error(`upload-docs: ${docIdErrors.join('\n')}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const totalCount = filesToUpload.length;
|
|
28
|
+
const filenamesToUpload = fs.recurseFiles(docDir).filter(name => name.endsWith(FILE_EXTENSION));
|
|
29
|
+
const totalCount = filenamesToUpload.length;
|
|
32
30
|
if (totalCount === 0) {
|
|
33
31
|
return; // nothing to upload
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
const analysis = analyseFiles(filenamesToUpload);
|
|
35
|
+
const errors = analysis.map(result => result.error).filter(Boolean);
|
|
36
|
+
if (errors.length > 0) {
|
|
37
|
+
throw new Error(`upload-docs: ${errors.join('\n')}`);
|
|
39
38
|
}
|
|
40
39
|
|
|
40
|
+
userPrompt.warnPromptAbort(`This operation will permanently write ${totalCount} docs. Are you sure you want to continue?`);
|
|
41
|
+
|
|
42
|
+
const deletedDocIds = analysis.map(result => result.delete).filter(Boolean);
|
|
43
|
+
await handleUsersAtDeletedFacilities(deletedDocIds);
|
|
44
|
+
|
|
41
45
|
const results = { ok:[], failed:{} };
|
|
42
46
|
const progress = log.level > log.LEVEL_ERROR ? progressBar.init(totalCount, '{{n}}/{{N}} docs ', ' {{%}} {{m}}:{{s}}') : null;
|
|
43
47
|
const processNextBatch = async (docFiles, batchSize) => {
|
|
@@ -93,20 +97,102 @@ const execute = async () => {
|
|
|
93
97
|
}
|
|
94
98
|
};
|
|
95
99
|
|
|
96
|
-
return processNextBatch(
|
|
97
|
-
}
|
|
100
|
+
return processNextBatch(filenamesToUpload, INITIAL_BATCH_SIZE);
|
|
101
|
+
}
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
filePaths
|
|
103
|
+
function analyseFiles(filePaths) {
|
|
104
|
+
return filePaths
|
|
101
105
|
.map(filePath => {
|
|
102
106
|
const json = fs.readJson(filePath);
|
|
103
107
|
const idFromFilename = path.basename(filePath, FILE_EXTENSION);
|
|
104
108
|
|
|
105
109
|
if (json._id !== idFromFilename) {
|
|
106
|
-
return `File '${filePath}' sets _id:'${json._id}' but the file's expected _id is '${idFromFilename}'
|
|
110
|
+
return { error: `File '${filePath}' sets _id:'${json._id}' but the file's expected _id is '${idFromFilename}'.` };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (json._deleted && json.cht_disable_linked_users) {
|
|
114
|
+
return { delete: json._id };
|
|
107
115
|
}
|
|
108
116
|
})
|
|
109
|
-
.filter(
|
|
117
|
+
.filter(Boolean);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function handleUsersAtDeletedFacilities(deletedDocIds) {
|
|
121
|
+
if (!deletedDocIds?.length) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await assertCoreVersion();
|
|
126
|
+
|
|
127
|
+
const affectedUsers = await getAffectedUsers(deletedDocIds);
|
|
128
|
+
const usernames = affectedUsers.map(userDoc => userDoc.username).join(', ');
|
|
129
|
+
if (affectedUsers.length === 0) {
|
|
130
|
+
trace('No users found needing an update.');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
userPrompt.warnPromptAbort(`This operation will update ${affectedUsers.length} user accounts: ${usernames} and cannot be undone. Are you sure you want to continue?`);
|
|
135
|
+
await updateAffectedUsers(affectedUsers);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function assertCoreVersion() {
|
|
139
|
+
const actualCoreVersion = await getValidApiVersion();
|
|
140
|
+
if (semver.lt(actualCoreVersion, '4.7.0-dev')) {
|
|
141
|
+
throw Error(`CHT Core Version 4.7.0 or newer is required to use --disable-users options. Version is ${actualCoreVersion}.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
trace(`Core version is ${actualCoreVersion}. Proceeding to disable users.`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function getAffectedUsers(deletedDocIds) {
|
|
148
|
+
const toPostApiFormat = (apiResponse) => {
|
|
149
|
+
const places = Array.isArray(apiResponse.place) ? apiResponse.place.filter(Boolean) : [apiResponse.place];
|
|
150
|
+
const placeIds = places.map(place => place?._id);
|
|
151
|
+
return {
|
|
152
|
+
username: apiResponse.username,
|
|
153
|
+
place: placeIds,
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const knownUserDocs = {};
|
|
158
|
+
for (const facilityId of deletedDocIds) {
|
|
159
|
+
const fetchedUserInfos = await api().getUsersAtPlace(facilityId);
|
|
160
|
+
for (const fetchedUserInfo of fetchedUserInfos) {
|
|
161
|
+
const userDoc = knownUserDocs[fetchedUserInfo.username] || toPostApiFormat(fetchedUserInfo);
|
|
162
|
+
removePlace(userDoc, facilityId);
|
|
163
|
+
knownUserDocs[userDoc.username] = userDoc;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return Object.values(knownUserDocs);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function removePlace(userDoc, placeId) {
|
|
171
|
+
if (Array.isArray(userDoc.place)) {
|
|
172
|
+
userDoc.place = userDoc.place
|
|
173
|
+
.filter(id => id !== placeId);
|
|
174
|
+
} else {
|
|
175
|
+
delete userDoc.place;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function updateAffectedUsers(affectedUsers) {
|
|
180
|
+
let disabledUsers = 0, updatedUsers = 0;
|
|
181
|
+
for (const userDoc of affectedUsers) {
|
|
182
|
+
const shouldDisable = !userDoc.place || userDoc.place?.length === 0;
|
|
183
|
+
if (shouldDisable) {
|
|
184
|
+
trace(`Disabling ${userDoc.username}`);
|
|
185
|
+
await api().disableUser(userDoc.username);
|
|
186
|
+
disabledUsers++;
|
|
187
|
+
} else {
|
|
188
|
+
trace(`Updating ${userDoc.username}`);
|
|
189
|
+
await api().updateUser(userDoc);
|
|
190
|
+
updatedUsers++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
info(`${disabledUsers} users disabled. ${updatedUsers} users updated.`);
|
|
195
|
+
}
|
|
110
196
|
|
|
111
197
|
module.exports = {
|
|
112
198
|
requiresInstance: true,
|
package/src/lib/api.js
CHANGED
|
@@ -39,6 +39,7 @@ const request = {
|
|
|
39
39
|
get: _request('get'),
|
|
40
40
|
post: _request('post'),
|
|
41
41
|
put: _request('put'),
|
|
42
|
+
delete: _request('delete'),
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
const logDeprecatedTransitions = (settings) => {
|
|
@@ -98,6 +99,29 @@ const api = {
|
|
|
98
99
|
.then(() => updateAppSettings(content));
|
|
99
100
|
},
|
|
100
101
|
|
|
102
|
+
async getUsersAtPlace(facilityId) {
|
|
103
|
+
const result = await request.get({
|
|
104
|
+
uri: `${environment.instanceUrl}/api/v2/users?facility_id=${facilityId}`,
|
|
105
|
+
json: true,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return result || [];
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
disableUser(username) {
|
|
112
|
+
return request.delete({
|
|
113
|
+
uri: `${environment.instanceUrl}/api/v1/users/${username}`,
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
updateUser(userDoc) {
|
|
118
|
+
return request.post({
|
|
119
|
+
uri: `${environment.instanceUrl}/api/v1/users/${userDoc.username}`,
|
|
120
|
+
json: true,
|
|
121
|
+
body: userDoc,
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
|
|
101
125
|
createUser(userData) {
|
|
102
126
|
return request.post({
|
|
103
127
|
uri: `${environment.instanceUrl}/api/v1/users`,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const lineageManipulation = require('./lineage-manipulation');
|
|
2
|
+
|
|
3
|
+
const HIERARCHY_ROOT = 'root';
|
|
4
|
+
const BATCH_SIZE = 10000;
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Fetches all of the documents associated with the "contactIds" and confirms they exist.
|
|
8
|
+
*/
|
|
9
|
+
async function getContactsByIds(db, ids) {
|
|
10
|
+
const contactDocs = await db.allDocs({
|
|
11
|
+
keys: ids,
|
|
12
|
+
include_docs: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const missingContactErrors = contactDocs.rows.filter(row => !row.doc).map(row => `Contact with id '${row.key}' could not be found.`);
|
|
16
|
+
if (missingContactErrors.length > 0) {
|
|
17
|
+
throw Error(missingContactErrors);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const contactDict = {};
|
|
21
|
+
contactDocs.rows.forEach(({ doc }) => contactDict[doc._id] = doc);
|
|
22
|
+
return contactDict;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getContact(db, id) {
|
|
26
|
+
try {
|
|
27
|
+
if (id === HIERARCHY_ROOT) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return await db.get(id);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (err.status !== 404) {
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
throw Error(`Contact with id '${id}' could not be found`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
Given a contact's id, obtain the documents of all descendant contacts
|
|
43
|
+
*/
|
|
44
|
+
async function getContactWithDescendants(db, contactId) {
|
|
45
|
+
const descendantDocs = await db.query('medic/contacts_by_depth', {
|
|
46
|
+
key: [contactId],
|
|
47
|
+
include_docs: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return descendantDocs.rows
|
|
51
|
+
.map(row => row.doc)
|
|
52
|
+
// We should not move or update tombstone documents
|
|
53
|
+
// Not relevant for 4.x cht-core versions, but needed in older versions.
|
|
54
|
+
.filter(doc => doc && doc.type !== 'tombstone');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function getReportsForContacts(db, createdByIds, createdAtId, skip) {
|
|
58
|
+
const createdByKeys = createdByIds.map(id => [`contact:${id}`]);
|
|
59
|
+
const createdAtKeys = createdAtId ? [
|
|
60
|
+
[`patient_id:${createdAtId}`],
|
|
61
|
+
[`patient_uuid:${createdAtId}`],
|
|
62
|
+
[`place_id:${createdAtId}`],
|
|
63
|
+
[`place_uuid:${createdAtId}`]
|
|
64
|
+
] : [];
|
|
65
|
+
|
|
66
|
+
const reports = await db.query('medic-client/reports_by_freetext', {
|
|
67
|
+
keys: [
|
|
68
|
+
...createdByKeys,
|
|
69
|
+
...createdAtKeys,
|
|
70
|
+
],
|
|
71
|
+
include_docs: true,
|
|
72
|
+
limit: BATCH_SIZE,
|
|
73
|
+
skip,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const docsWithId = reports.rows.map(({ doc }) => [doc._id, doc]);
|
|
77
|
+
return Array.from(new Map(docsWithId).values());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function getAncestorsOf(db, contactDoc) {
|
|
81
|
+
const ancestorIds = lineageManipulation.pluckIdsFromLineage(contactDoc.parent);
|
|
82
|
+
const ancestors = await db.allDocs({
|
|
83
|
+
keys: ancestorIds,
|
|
84
|
+
include_docs: true,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const ancestorIdsNotFound = ancestors.rows.filter(ancestor => !ancestor.doc).map(ancestor => ancestor.key);
|
|
88
|
+
if (ancestorIdsNotFound.length > 0) {
|
|
89
|
+
throw Error(`Contact '${contactDoc?.name}' (${contactDoc?._id}) has parent id(s) '${ancestorIdsNotFound.join(',')}' which could not be found.`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return ancestors.rows.map(ancestor => ancestor.doc);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
BATCH_SIZE,
|
|
97
|
+
HIERARCHY_ROOT,
|
|
98
|
+
getAncestorsOf,
|
|
99
|
+
getContactWithDescendants,
|
|
100
|
+
getContact,
|
|
101
|
+
getContactsByIds,
|
|
102
|
+
getReportsForContacts,
|
|
103
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const lineageManipulation = require('./lineage-manipulation');
|
|
2
|
+
const LineageConstraints = require('./lineage-constraints');
|
|
3
|
+
const { trace, info } = require('../log');
|
|
4
|
+
|
|
5
|
+
const JsDocs = require('./jsdocFolder');
|
|
6
|
+
const DataSource = require('./hierarchy-data-source');
|
|
7
|
+
|
|
8
|
+
async function moveHierarchy(db, options, sourceIds, destinationId) {
|
|
9
|
+
JsDocs.prepareFolder(options);
|
|
10
|
+
trace(`Fetching contact details: ${destinationId}`);
|
|
11
|
+
const constraints = await LineageConstraints(db, options);
|
|
12
|
+
const destinationDoc = await DataSource.getContact(db, destinationId);
|
|
13
|
+
const sourceDocs = await DataSource.getContactsByIds(db, sourceIds);
|
|
14
|
+
constraints.assertNoHierarchyErrors(Object.values(sourceDocs), destinationDoc);
|
|
15
|
+
|
|
16
|
+
let affectedContactCount = 0;
|
|
17
|
+
let affectedReportCount = 0;
|
|
18
|
+
const replacementLineage = lineageManipulation.createLineageFromDoc(destinationDoc);
|
|
19
|
+
for (const sourceId of sourceIds) {
|
|
20
|
+
const sourceDoc = sourceDocs[sourceId];
|
|
21
|
+
const descendantsAndSelf = await DataSource.getContactWithDescendants(db, sourceId);
|
|
22
|
+
const moveContext = {
|
|
23
|
+
sourceId,
|
|
24
|
+
destinationId,
|
|
25
|
+
descendantsAndSelf,
|
|
26
|
+
replacementLineage,
|
|
27
|
+
merge: !!options.merge,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
await constraints.assertNoPrimaryContactViolations(sourceDoc, destinationDoc, descendantsAndSelf);
|
|
31
|
+
|
|
32
|
+
if (options.merge) {
|
|
33
|
+
JsDocs.writeDoc(options, {
|
|
34
|
+
_id: sourceDoc._id,
|
|
35
|
+
_rev: sourceDoc._rev,
|
|
36
|
+
_deleted: true,
|
|
37
|
+
cht_disable_linked_users: !!options.disableUsers,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const prettyPrintDocument = doc => `'${doc.name}' (${doc._id})`;
|
|
42
|
+
trace(`Considering lineage updates to ${descendantsAndSelf.length} descendant(s) of contact ${prettyPrintDocument(sourceDoc)}.`);
|
|
43
|
+
const updatedDescendants = replaceLineageInContacts(moveContext);
|
|
44
|
+
|
|
45
|
+
const ancestors = await DataSource.getAncestorsOf(db, sourceDoc);
|
|
46
|
+
trace(`Considering primary contact updates to ${ancestors.length} ancestor(s) of contact ${prettyPrintDocument(sourceDoc)}.`);
|
|
47
|
+
const updatedAncestors = replaceLineageInAncestors(descendantsAndSelf, ancestors);
|
|
48
|
+
|
|
49
|
+
minifyLineageAndWriteToDisk(options, [...updatedDescendants, ...updatedAncestors]);
|
|
50
|
+
|
|
51
|
+
const movedReportsCount = await updateReports(db, options, moveContext);
|
|
52
|
+
trace(`${movedReportsCount} report(s) created by these affected contact(s) will be updated`);
|
|
53
|
+
|
|
54
|
+
affectedContactCount += updatedDescendants.length + updatedAncestors.length;
|
|
55
|
+
affectedReportCount += movedReportsCount;
|
|
56
|
+
|
|
57
|
+
info(`Staged updates to ${prettyPrintDocument(sourceDoc)}. ${updatedDescendants.length} contact(s) and ${movedReportsCount} report(s).`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
info(`Staged changes to lineage information for ${affectedContactCount} contact(s) and ${affectedReportCount} report(s).`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function updateReports(db, options, moveContext) {
|
|
64
|
+
const descendantIds = moveContext.descendantsAndSelf.map(contact => contact._id);
|
|
65
|
+
|
|
66
|
+
let skip = 0;
|
|
67
|
+
let reportDocsBatch;
|
|
68
|
+
do {
|
|
69
|
+
info(`Processing ${skip} to ${skip + DataSource.BATCH_SIZE} report docs`);
|
|
70
|
+
const createdAtId = options.merge && moveContext.sourceId;
|
|
71
|
+
reportDocsBatch = await DataSource.getReportsForContacts(db, descendantIds, createdAtId, skip);
|
|
72
|
+
|
|
73
|
+
const lineageUpdates = replaceLineageOfReportCreator(reportDocsBatch, moveContext);
|
|
74
|
+
const reassignUpdates = reassignReports(reportDocsBatch, moveContext);
|
|
75
|
+
const updatedReports = reportDocsBatch.filter(doc => lineageUpdates.has(doc._id) || reassignUpdates.has(doc._id));
|
|
76
|
+
|
|
77
|
+
minifyLineageAndWriteToDisk(options, updatedReports);
|
|
78
|
+
|
|
79
|
+
skip += reportDocsBatch.length;
|
|
80
|
+
} while (reportDocsBatch.length >= DataSource.BATCH_SIZE);
|
|
81
|
+
|
|
82
|
+
return skip;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function reassignReportSubjects(report, { sourceId, destinationId }) {
|
|
86
|
+
const SUBJECT_IDS = ['patient_id', 'patient_uuid', 'place_id', 'place_uuid'];
|
|
87
|
+
let updated = false;
|
|
88
|
+
for (const subjectId of SUBJECT_IDS) {
|
|
89
|
+
if (report[subjectId] === sourceId) {
|
|
90
|
+
report[subjectId] = destinationId;
|
|
91
|
+
updated = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (report.fields[subjectId] === sourceId) {
|
|
95
|
+
report.fields[subjectId] = destinationId;
|
|
96
|
+
updated = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return updated;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function reassignReports(reports, moveContext) {
|
|
104
|
+
const updated = new Set();
|
|
105
|
+
if (!moveContext.merge) {
|
|
106
|
+
return updated;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const report of reports) {
|
|
110
|
+
const isUpdated = reassignReportSubjects(report, moveContext);
|
|
111
|
+
if (isUpdated) {
|
|
112
|
+
updated.add(report._id);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return updated;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// This ensures all documents written are fully minified. Some docs in CouchDB are not minified to start with.
|
|
120
|
+
function minifyLineageAndWriteToDisk(options, docs) {
|
|
121
|
+
docs.forEach(doc => {
|
|
122
|
+
lineageManipulation.minifyLineagesInDoc(doc);
|
|
123
|
+
JsDocs.writeDoc(options, doc);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function replaceLineageOfReportCreator(reports, moveContext) {
|
|
128
|
+
const replaceContactLineage = doc => lineageManipulation.replaceContactLineage(doc, {
|
|
129
|
+
replaceWith: moveContext.replacementLineage,
|
|
130
|
+
startingFromId: moveContext.sourceId,
|
|
131
|
+
merge: moveContext.merge,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const updatedReportIds = reports
|
|
135
|
+
.filter(replaceContactLineage)
|
|
136
|
+
.map(({ _id }) => _id);
|
|
137
|
+
return new Set(updatedReportIds);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function replaceLineageInAncestors(descendantsAndSelf, ancestors) {
|
|
141
|
+
const updatedAncestors = [];
|
|
142
|
+
for (const ancestor of ancestors) {
|
|
143
|
+
const primaryContact = descendantsAndSelf.find(descendant => descendant._id === ancestor.contact?._id);
|
|
144
|
+
if (primaryContact) {
|
|
145
|
+
ancestor.contact = lineageManipulation.createLineageFromDoc(primaryContact);
|
|
146
|
+
updatedAncestors.unshift(ancestor);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return updatedAncestors;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function replaceLineageInSingleContact(doc, moveContext) {
|
|
154
|
+
const docIsSource = doc._id === moveContext.sourceId;
|
|
155
|
+
if (docIsSource && moveContext.merge) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const startingFromId = moveContext.merge || !docIsSource ? moveContext.sourceId : undefined;
|
|
160
|
+
const replaceLineageOptions = {
|
|
161
|
+
replaceWith: moveContext.replacementLineage,
|
|
162
|
+
startingFromId,
|
|
163
|
+
merge: moveContext.merge,
|
|
164
|
+
};
|
|
165
|
+
const parentWasUpdated = lineageManipulation.replaceParentLineage(doc, replaceLineageOptions);
|
|
166
|
+
|
|
167
|
+
replaceLineageOptions.startingFromId = moveContext.sourceId;
|
|
168
|
+
const contactWasUpdated = lineageManipulation.replaceContactLineage(doc, replaceLineageOptions);
|
|
169
|
+
if (parentWasUpdated || contactWasUpdated) {
|
|
170
|
+
return doc;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function replaceLineageInContacts(moveContext) {
|
|
175
|
+
return moveContext.descendantsAndSelf
|
|
176
|
+
.map(descendant => replaceLineageInSingleContact(descendant, moveContext))
|
|
177
|
+
.filter(Boolean);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = (db, options) => {
|
|
181
|
+
return {
|
|
182
|
+
HIERARCHY_ROOT: DataSource.HIERARCHY_ROOT,
|
|
183
|
+
move: (sourceIds, destinationId) => moveHierarchy(db, { ...options, merge: false }, sourceIds, destinationId),
|
|
184
|
+
merge: (sourceIds, destinationId) => moveHierarchy(db, { ...options, merge: true }, sourceIds, destinationId),
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const userPrompt = require('../user-prompt');
|
|
3
|
+
const fs = require('../sync-fs');
|
|
4
|
+
const { warn, trace } = require('../log');
|
|
5
|
+
|
|
6
|
+
function prepareFolder({ docDirectoryPath, force }) {
|
|
7
|
+
if (!fs.exists(docDirectoryPath)) {
|
|
8
|
+
fs.mkdir(docDirectoryPath);
|
|
9
|
+
} else if (!force && fs.recurseFiles(docDirectoryPath).length > 0) {
|
|
10
|
+
deleteAfterConfirmation(docDirectoryPath);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function writeDoc({ docDirectoryPath }, doc) {
|
|
15
|
+
const destinationPath = path.join(docDirectoryPath, `${doc._id}.doc.json`);
|
|
16
|
+
if (fs.exists(destinationPath)) {
|
|
17
|
+
warn(`File at ${destinationPath} already exists and is being overwritten.`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
trace(`Writing updated document to ${destinationPath}`);
|
|
21
|
+
fs.writeJson(destinationPath, doc);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function deleteAfterConfirmation(docDirectoryPath) {
|
|
25
|
+
warn(`The document folder '${docDirectoryPath}' already contains files. It is recommended you start with a clean folder. Do you want to delete the contents of this folder and continue?`);
|
|
26
|
+
if (!userPrompt.keyInYN()) {
|
|
27
|
+
throw new Error('User aborted execution.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fs.deleteFilesInFolder(docDirectoryPath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
prepareFolder,
|
|
35
|
+
writeDoc,
|
|
36
|
+
};
|
|
37
|
+
|