cht-conf 4.3.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 CHANGED
@@ -1,3 +1,10 @@
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
+
1
8
  # [4.3.0](https://github.com/medic/cht-conf/compare/v4.2.0...v4.3.0) (2024-12-16)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cht-conf",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Configure CHT deployments",
5
5
  "main": "./src/lib/main.js",
6
6
  "engines": {
@@ -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
+ };
@@ -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,12 +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
- JsDocs.writeDoc(options, {
34
- _id: sourceDoc._id,
35
- _rev: sourceDoc._rev,
36
- _deleted: true,
37
- cht_disable_linked_users: !!options.disableUsers,
38
- });
33
+ const toDeleteUsers = options.disableUsers && constraints.isPlace(sourceDoc);
34
+ JsDocs.deleteDoc(options, sourceDoc, toDeleteUsers);
39
35
  }
40
36
 
41
37
  const prettyPrintDocument = doc => `'${doc.name}' (${doc._id})`;
@@ -177,11 +173,11 @@ function replaceLineageInContacts(moveContext) {
177
173
  .filter(Boolean);
178
174
  }
179
175
 
180
- module.exports = (db, options) => {
176
+ module.exports = (db, options = {}) => {
181
177
  return {
182
178
  HIERARCHY_ROOT: DataSource.HIERARCHY_ROOT,
183
179
  move: (sourceIds, destinationId) => moveHierarchy(db, { ...options, merge: false }, sourceIds, destinationId),
184
180
  merge: (sourceIds, destinationId) => moveHierarchy(db, { ...options, merge: true }, sourceIds, destinationId),
181
+ delete: async (sourceIds) => deleteHierarchy(db, options, sourceIds),
185
182
  };
186
183
  };
187
-
@@ -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 mapTypeToAllowedParents = await fetchAllowedParents(db);
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(mapTypeToAllowedParents, sourceDoc, destinationDoc);
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 = (mapTypeToAllowedParents, sourceDoc, destinationDoc) => {
53
- const contactTypeError = getMovingContactTypeError(mapTypeToAllowedParents, sourceDoc, destinationDoc);
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(mapTypeToAllowedParents, sourceDoc, destinationDoc) {
63
+ function getMovingContactTypeError(contactTypeInfo, sourceDoc, destinationDoc) {
59
64
  const sourceContactType = getContactType(sourceDoc);
60
65
  const destinationType = getContactType(destinationDoc);
61
- const rulesForContact = mapTypeToAllowedParents[sourceContactType];
62
- if (!rulesForContact) {
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 && rulesForContact.length === 0;
67
- if (!isPermittedMoveToRoot && !rulesForContact.includes(destinationType)) {
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 && (doc.type === 'contact' ? doc.contact_type : doc.type);
147
+ const getContactType = doc => doc?.type === 'contact' ? doc?.contact_type : doc?.type;
143
148
 
144
- async function fetchAllowedParents(db) {
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