cht-conf 4.2.0 → 4.4.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/delete-contacts.js +64 -0
- package/src/fn/merge-contacts.js +5 -0
- package/src/fn/upload-docs.js +104 -18
- package/src/lib/api.js +24 -0
- package/src/lib/hierarchy-operations/delete-hierarchy.js +46 -0
- package/src/lib/hierarchy-operations/index.js +7 -10
- package/src/lib/hierarchy-operations/jsdocFolder.js +10 -0
- package/src/lib/hierarchy-operations/lineage-constraints.js +22 -17
- package/src/lib/user-prompt.js +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [4.4.0](https://github.com/medic/cht-conf/compare/v4.3.0...v4.4.0) (2024-12-16)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **#650:** delete-contacts action ([#652](https://github.com/medic/cht-conf/issues/652)) ([129f38f](https://github.com/medic/cht-conf/commit/129f38fca83fa84717c7ba16857b315c25d33b9e)), closes [#650](https://github.com/medic/cht-conf/issues/650)
|
|
7
|
+
|
|
8
|
+
# [4.3.0](https://github.com/medic/cht-conf/compare/v4.2.0...v4.3.0) (2024-12-16)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **#648:** add --disable-users command for merge-contacts action ([#649](https://github.com/medic/cht-conf/issues/649)) ([c78d3fd](https://github.com/medic/cht-conf/commit/c78d3fd1ed1588cfefdf21384840743f5faea62d)), closes [#648](https://github.com/medic/cht-conf/issues/648)
|
|
14
|
+
|
|
1
15
|
# [4.2.0](https://github.com/medic/cht-conf/compare/v4.1.3...v4.2.0) (2024-12-11)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const minimist = require('minimist');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const environment = require('../lib/environment');
|
|
5
|
+
const pouch = require('../lib/db');
|
|
6
|
+
const { info } = require('../lib/log');
|
|
7
|
+
|
|
8
|
+
const HierarchyOperations = require('../lib/hierarchy-operations');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
requiresInstance: true,
|
|
12
|
+
execute: () => {
|
|
13
|
+
const args = parseExtraArgs(environment.pathToProject, environment.extraArgs);
|
|
14
|
+
const db = pouch();
|
|
15
|
+
const options = {
|
|
16
|
+
docDirectoryPath: args.docDirectoryPath,
|
|
17
|
+
force: args.force,
|
|
18
|
+
disableUsers: args.disableUsers,
|
|
19
|
+
};
|
|
20
|
+
return HierarchyOperations(db, options).delete(args.sourceIds);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Parses extraArgs and asserts if required parameters are not present
|
|
25
|
+
const parseExtraArgs = (projectDir, extraArgs = []) => {
|
|
26
|
+
const args = minimist(extraArgs, { boolean: true });
|
|
27
|
+
|
|
28
|
+
const sourceIds = (args.contacts || args.contact || '')
|
|
29
|
+
.split(',')
|
|
30
|
+
.filter(id => id);
|
|
31
|
+
|
|
32
|
+
if (sourceIds.length === 0) {
|
|
33
|
+
usage();
|
|
34
|
+
throw Error('Action "delete-contacts" is missing required list of contacts to be deleted');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
sourceIds,
|
|
39
|
+
disableUsers: !!args['disable-users'],
|
|
40
|
+
docDirectoryPath: path.resolve(projectDir, args.docDirectoryPath || 'json_docs'),
|
|
41
|
+
force: !!args.force,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const bold = text => `\x1b[1m${text}\x1b[0m`;
|
|
46
|
+
const usage = () => {
|
|
47
|
+
info(`
|
|
48
|
+
${bold('cht-conf\'s delete-contacts action')}
|
|
49
|
+
When combined with 'upload-docs' this action recursively deletes a contact and all of their descendant contacts and data. ${bold('This operation is permanent. It cannot be undone.')}
|
|
50
|
+
|
|
51
|
+
${bold('USAGE')}
|
|
52
|
+
cht --local delete-contacts -- --contacts=<id1>,<id2>
|
|
53
|
+
|
|
54
|
+
${bold('OPTIONS')}
|
|
55
|
+
--contacts=<id1>,<id2> (or --contact=<id1>,<id2>)
|
|
56
|
+
A comma delimited list of ids of contacts to be deleted.
|
|
57
|
+
|
|
58
|
+
--disable-users
|
|
59
|
+
When flag is present, users at any deleted place will be permanently disabled.
|
|
60
|
+
|
|
61
|
+
--docDirectoryPath=<path to stage docs>
|
|
62
|
+
Specifies the folder used to store the documents representing the changes in hierarchy.
|
|
63
|
+
`);
|
|
64
|
+
};
|
package/src/fn/merge-contacts.js
CHANGED
|
@@ -13,6 +13,7 @@ module.exports = {
|
|
|
13
13
|
const args = parseExtraArgs(environment.pathToProject, environment.extraArgs);
|
|
14
14
|
const db = pouch();
|
|
15
15
|
const options = {
|
|
16
|
+
disableUsers: args.disableUsers,
|
|
16
17
|
docDirectoryPath: args.docDirectoryPath,
|
|
17
18
|
force: args.force,
|
|
18
19
|
};
|
|
@@ -41,6 +42,7 @@ const parseExtraArgs = (projectDir, extraArgs = []) => {
|
|
|
41
42
|
return {
|
|
42
43
|
destinationId: args.destination,
|
|
43
44
|
sourceIds,
|
|
45
|
+
disableUsers: !!args['disable-users'],
|
|
44
46
|
docDirectoryPath: path.resolve(projectDir, args.docDirectoryPath || 'json_docs'),
|
|
45
47
|
force: !!args.force,
|
|
46
48
|
};
|
|
@@ -63,6 +65,9 @@ ${bold('OPTIONS')}
|
|
|
63
65
|
--sources=<source_id1>,<source_id2>
|
|
64
66
|
A comma delimited list of IDs of contacts which will be deleted. The hierarchy of contacts and reports under it will be moved to be under the destination contact.
|
|
65
67
|
|
|
68
|
+
--disable-users
|
|
69
|
+
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.
|
|
70
|
+
|
|
66
71
|
--docDirectoryPath=<path to stage docs>
|
|
67
72
|
Specifies the folder used to store the documents representing the changes in hierarchy.
|
|
68
73
|
`);
|
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,46 @@
|
|
|
1
|
+
const DataSource = require('./hierarchy-data-source');
|
|
2
|
+
const JsDocs = require('./jsdocFolder');
|
|
3
|
+
const lineageConstraints = require('./lineage-constraints');
|
|
4
|
+
const { trace, info } = require('../log');
|
|
5
|
+
|
|
6
|
+
const prettyPrintDocument = doc => `'${doc.name}' (${doc._id})`;
|
|
7
|
+
async function deleteHierarchy(db, options, sourceIds) {
|
|
8
|
+
JsDocs.prepareFolder(options);
|
|
9
|
+
|
|
10
|
+
const sourceDocs = await DataSource.getContactsByIds(db, sourceIds);
|
|
11
|
+
const constraints = await lineageConstraints(db, options);
|
|
12
|
+
for (const sourceId of sourceIds) {
|
|
13
|
+
const sourceDoc = sourceDocs[sourceId];
|
|
14
|
+
trace(`Deleting descendants and reports under: ${prettyPrintDocument(sourceDoc)}`);
|
|
15
|
+
const descendantsAndSelf = await DataSource.getContactWithDescendants(db, sourceId);
|
|
16
|
+
|
|
17
|
+
let affectedReportCount = 0;
|
|
18
|
+
for (const descendant of descendantsAndSelf) {
|
|
19
|
+
const toDeleteUsers = options.disableUsers && constraints.isPlace(descendant);
|
|
20
|
+
JsDocs.deleteDoc(options, descendant, toDeleteUsers);
|
|
21
|
+
affectedReportCount += await deleteReportsForContact(db, options, descendant);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const affectedContactCount = descendantsAndSelf.length;
|
|
25
|
+
|
|
26
|
+
info(`Staged updates to delete ${prettyPrintDocument(sourceDoc)}. ${affectedContactCount.length} contact(s) and ${affectedReportCount} report(s).`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function deleteReportsForContact(db, options, contact) {
|
|
31
|
+
let skip = 0;
|
|
32
|
+
let reportBatch;
|
|
33
|
+
do {
|
|
34
|
+
reportBatch = await DataSource.getReportsForContacts(db, [], contact._id, skip);
|
|
35
|
+
|
|
36
|
+
for (const report of reportBatch) {
|
|
37
|
+
JsDocs.deleteDoc(options, report);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
skip += reportBatch.length;
|
|
41
|
+
} while (reportBatch.length >= DataSource.BATCH_SIZE);
|
|
42
|
+
|
|
43
|
+
return skip;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = deleteHierarchy;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
const DataSource = require('./hierarchy-data-source');
|
|
2
|
+
const deleteHierarchy = require('./delete-hierarchy');
|
|
3
|
+
const JsDocs = require('./jsdocFolder');
|
|
1
4
|
const lineageManipulation = require('./lineage-manipulation');
|
|
2
5
|
const LineageConstraints = require('./lineage-constraints');
|
|
3
6
|
const { trace, info } = require('../log');
|
|
4
7
|
|
|
5
|
-
const JsDocs = require('./jsdocFolder');
|
|
6
|
-
const DataSource = require('./hierarchy-data-source');
|
|
7
|
-
|
|
8
8
|
async function moveHierarchy(db, options, sourceIds, destinationId) {
|
|
9
9
|
JsDocs.prepareFolder(options);
|
|
10
10
|
trace(`Fetching contact details: ${destinationId}`);
|
|
@@ -30,11 +30,8 @@ async function moveHierarchy(db, options, sourceIds, destinationId) {
|
|
|
30
30
|
await constraints.assertNoPrimaryContactViolations(sourceDoc, destinationDoc, descendantsAndSelf);
|
|
31
31
|
|
|
32
32
|
if (options.merge) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
_rev: sourceDoc._rev,
|
|
36
|
-
_deleted: true,
|
|
37
|
-
});
|
|
33
|
+
const toDeleteUsers = options.disableUsers && constraints.isPlace(sourceDoc);
|
|
34
|
+
JsDocs.deleteDoc(options, sourceDoc, toDeleteUsers);
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
const prettyPrintDocument = doc => `'${doc.name}' (${doc._id})`;
|
|
@@ -176,11 +173,11 @@ function replaceLineageInContacts(moveContext) {
|
|
|
176
173
|
.filter(Boolean);
|
|
177
174
|
}
|
|
178
175
|
|
|
179
|
-
module.exports = (db, options) => {
|
|
176
|
+
module.exports = (db, options = {}) => {
|
|
180
177
|
return {
|
|
181
178
|
HIERARCHY_ROOT: DataSource.HIERARCHY_ROOT,
|
|
182
179
|
move: (sourceIds, destinationId) => moveHierarchy(db, { ...options, merge: false }, sourceIds, destinationId),
|
|
183
180
|
merge: (sourceIds, destinationId) => moveHierarchy(db, { ...options, merge: true }, sourceIds, destinationId),
|
|
181
|
+
delete: async (sourceIds) => deleteHierarchy(db, options, sourceIds),
|
|
184
182
|
};
|
|
185
183
|
};
|
|
186
|
-
|
|
@@ -30,7 +30,17 @@ function deleteAfterConfirmation(docDirectoryPath) {
|
|
|
30
30
|
fs.deleteFilesInFolder(docDirectoryPath);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function deleteDoc(options, doc, disableUsers) {
|
|
34
|
+
writeDoc(options, {
|
|
35
|
+
_id: doc._id,
|
|
36
|
+
_rev: doc._rev,
|
|
37
|
+
_deleted: true,
|
|
38
|
+
cht_disable_linked_users: !!disableUsers,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
module.exports = {
|
|
43
|
+
deleteDoc,
|
|
34
44
|
prepareFolder,
|
|
35
45
|
writeDoc,
|
|
36
46
|
};
|
|
@@ -5,7 +5,7 @@ const { trace } = log;
|
|
|
5
5
|
const lineageManipulation = require('./lineage-manipulation');
|
|
6
6
|
|
|
7
7
|
module.exports = async (db, options) => {
|
|
8
|
-
const
|
|
8
|
+
const contactTypeInfo = await fetchContactTypeInfo(db);
|
|
9
9
|
|
|
10
10
|
return {
|
|
11
11
|
assertNoPrimaryContactViolations: async (sourceDoc, destinationDoc, descendantDocs) => {
|
|
@@ -20,7 +20,7 @@ module.exports = async (db, options) => {
|
|
|
20
20
|
const commonViolations = getCommonViolations(sourceDoc, destinationDoc);
|
|
21
21
|
const specificViolation = options.merge ?
|
|
22
22
|
getMergeViolations(sourceDoc, destinationDoc)
|
|
23
|
-
: getMovingViolations(
|
|
23
|
+
: getMovingViolations(contactTypeInfo, sourceDoc, destinationDoc);
|
|
24
24
|
|
|
25
25
|
const hierarchyError = commonViolations || specificViolation;
|
|
26
26
|
if (hierarchyError) {
|
|
@@ -41,7 +41,12 @@ module.exports = async (db, options) => {
|
|
|
41
41
|
throw Error(`Unable to move two documents from the same lineage: '${doc._id}' and '${violatingParentId}'`);
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
|
-
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
isPlace: (contact) => {
|
|
47
|
+
const contactType = getContactType(contact);
|
|
48
|
+
return !contactTypeInfo[contactType]?.person;
|
|
49
|
+
},
|
|
45
50
|
};
|
|
46
51
|
};
|
|
47
52
|
|
|
@@ -49,22 +54,22 @@ module.exports = async (db, options) => {
|
|
|
49
54
|
Enforce the list of allowed parents for each contact type
|
|
50
55
|
Ensure we are not creating a circular hierarchy
|
|
51
56
|
*/
|
|
52
|
-
const getMovingViolations = (
|
|
53
|
-
const contactTypeError = getMovingContactTypeError(
|
|
57
|
+
const getMovingViolations = (contactTypeInfo, sourceDoc, destinationDoc) => {
|
|
58
|
+
const contactTypeError = getMovingContactTypeError(contactTypeInfo, sourceDoc, destinationDoc);
|
|
54
59
|
const circularHierarchyError = findCircularHierarchyErrors(sourceDoc, destinationDoc);
|
|
55
60
|
return contactTypeError || circularHierarchyError;
|
|
56
61
|
};
|
|
57
62
|
|
|
58
|
-
function getMovingContactTypeError(
|
|
63
|
+
function getMovingContactTypeError(contactTypeInfo, sourceDoc, destinationDoc) {
|
|
59
64
|
const sourceContactType = getContactType(sourceDoc);
|
|
60
65
|
const destinationType = getContactType(destinationDoc);
|
|
61
|
-
const
|
|
62
|
-
if (!
|
|
66
|
+
const parentsForContactType = contactTypeInfo[sourceContactType]?.parents;
|
|
67
|
+
if (!parentsForContactType) {
|
|
63
68
|
return `cannot move contact with unknown type '${sourceContactType}'`;
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
const isPermittedMoveToRoot = !destinationDoc &&
|
|
67
|
-
if (!isPermittedMoveToRoot && !
|
|
71
|
+
const isPermittedMoveToRoot = !destinationDoc && parentsForContactType.length === 0;
|
|
72
|
+
if (!isPermittedMoveToRoot && !parentsForContactType.includes(destinationType)) {
|
|
68
73
|
return `contacts of type '${sourceContactType}' cannot have parent of type '${destinationType}'`;
|
|
69
74
|
}
|
|
70
75
|
}
|
|
@@ -139,9 +144,9 @@ const getPrimaryContactViolations = async (db, contactDoc, destinationDoc, desce
|
|
|
139
144
|
return descendantDocs.find(descendant => primaryContactIds.some(primaryId => descendant._id === primaryId));
|
|
140
145
|
};
|
|
141
146
|
|
|
142
|
-
const getContactType = doc => doc
|
|
147
|
+
const getContactType = doc => doc?.type === 'contact' ? doc?.contact_type : doc?.type;
|
|
143
148
|
|
|
144
|
-
async function
|
|
149
|
+
async function fetchContactTypeInfo(db) {
|
|
145
150
|
try {
|
|
146
151
|
const { settings: { contact_types } } = await db.get('settings');
|
|
147
152
|
|
|
@@ -150,7 +155,7 @@ async function fetchAllowedParents(db) {
|
|
|
150
155
|
const parentDict = {};
|
|
151
156
|
contact_types
|
|
152
157
|
.filter(Boolean)
|
|
153
|
-
.forEach(({ id, parents }) => parentDict[id] = parents);
|
|
158
|
+
.forEach(({ id, person, parents }) => parentDict[id] = { parents, person: !!person });
|
|
154
159
|
return parentDict;
|
|
155
160
|
}
|
|
156
161
|
} catch (err) {
|
|
@@ -161,10 +166,10 @@ async function fetchAllowedParents(db) {
|
|
|
161
166
|
|
|
162
167
|
trace('Default hierarchy constraints will be enforced.');
|
|
163
168
|
return {
|
|
164
|
-
district_hospital: [],
|
|
165
|
-
health_center: ['district_hospital'],
|
|
166
|
-
clinic: ['health_center'],
|
|
167
|
-
person: ['district_hospital', 'health_center', 'clinic'],
|
|
169
|
+
district_hospital: { parents: [] },
|
|
170
|
+
health_center: { parents: ['district_hospital'] },
|
|
171
|
+
clinic: { parents: ['health_center'] },
|
|
172
|
+
person: { parents: ['district_hospital', 'health_center', 'clinic'], person: true },
|
|
168
173
|
};
|
|
169
174
|
}
|
|
170
175
|
|
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
|
};
|