cht-conf 4.4.1 → 4.5.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.
Files changed (63) hide show
  1. package/.editorconfig +15 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +9 -1
  4. package/package.json +27 -26
  5. package/src/bin/shell-completion.js +5 -5
  6. package/src/cli/supported-actions.js +2 -2
  7. package/src/cli/usage.js +2 -0
  8. package/src/fn/backup-all-forms.js +1 -1
  9. package/src/fn/compile-app-settings.js +41 -14
  10. package/src/fn/compress-pngs.js +10 -8
  11. package/src/fn/compress-svgs.js +5 -2
  12. package/src/fn/create-users.js +6 -3
  13. package/src/fn/csv-to-docs.js +73 -52
  14. package/src/fn/delete-contacts.js +2 -0
  15. package/src/fn/edit-contacts.js +14 -11
  16. package/src/fn/fetch-csvs-from-google-drive.js +3 -3
  17. package/src/fn/fetch-forms-from-google-drive.js +3 -3
  18. package/src/fn/initialise-project-layout.js +14 -8
  19. package/src/fn/merge-contacts.js +10 -2
  20. package/src/fn/move-contacts.js +2 -0
  21. package/src/fn/upload-custom-translations.js +29 -22
  22. package/src/fn/upload-docs.js +30 -13
  23. package/src/fn/upload-privacy-policies.js +4 -1
  24. package/src/fn/upload-sms-from-csv.js +11 -11
  25. package/src/fn/watch-project.js +212 -204
  26. package/src/lib/api.js +22 -22
  27. package/src/lib/args-form-filter.js +1 -1
  28. package/src/lib/attachments-from-dir.js +3 -1
  29. package/src/lib/check-cht-conf-dependency-version.js +4 -2
  30. package/src/lib/check-for-updates.js +4 -3
  31. package/src/lib/compilation/compile-contact-summary.js +12 -2
  32. package/src/lib/compilation/compile-tasks-and-targets.js +5 -2
  33. package/src/lib/compilation/package-lib.js +5 -2
  34. package/src/lib/compilation/validate-declarative-schema.js +49 -32
  35. package/src/lib/convert-forms.js +27 -9
  36. package/src/lib/exec-promise.js +27 -19
  37. package/src/lib/fetch-document-list.js +2 -2
  38. package/src/lib/generate-users-csv.js +1 -1
  39. package/src/lib/get-api-url.js +4 -1
  40. package/src/lib/google-auth.js +15 -5
  41. package/src/lib/hierarchy-operations/delete-hierarchy.js +4 -1
  42. package/src/lib/hierarchy-operations/hierarchy-data-source.js +7 -2
  43. package/src/lib/hierarchy-operations/index.js +16 -4
  44. package/src/lib/hierarchy-operations/jsdocFolder.js +4 -1
  45. package/src/lib/hierarchy-operations/lineage-constraints.js +12 -6
  46. package/src/lib/hierarchy-operations/replace-lineage.js +20 -18
  47. package/src/lib/insert-or-replace.js +6 -2
  48. package/src/lib/log.js +6 -2
  49. package/src/lib/main.js +4 -3
  50. package/src/lib/nools-utils.js +5 -3
  51. package/src/lib/parse-purge.js +4 -1
  52. package/src/lib/parse-targets.js +3 -1
  53. package/src/lib/polyfill.js +3 -1
  54. package/src/lib/progress-bar.js +26 -13
  55. package/src/lib/skip-fn.js +5 -3
  56. package/src/lib/sync-fs.js +24 -12
  57. package/src/lib/upload-forms.js +17 -7
  58. package/src/lib/validation/form/can-generate-xform.js +3 -1
  59. package/src/lib/validation/form/check-xpaths-exist.js +8 -3
  60. package/src/lib/validation/form/deprecated-appearance.js +4 -2
  61. package/src/lib/validation/form/has-instance-id.js +4 -1
  62. package/src/lib/validation/form/no-required-notes.js +4 -1
  63. package/src/lib/warn-upload-overwrite.js +6 -3
@@ -9,14 +9,14 @@ const LAYOUT = {
9
9
  fields: [],
10
10
  cards: [],
11
11
  context: {}
12
- };`,
12
+ };\n`,
13
13
  'privacy-policies.json': {},
14
14
  'privacy-policies': {},
15
15
  'resources.json': {},
16
16
  'harness.defaults.json': {},
17
17
  resources: {},
18
- 'targets.js': 'module.exports = [];',
19
- 'tasks.js': 'module.exports = [];',
18
+ 'targets.js': 'module.exports = [];\n',
19
+ 'tasks.js': 'module.exports = [];\n',
20
20
  '.eslintrc': `{
21
21
  "env": {
22
22
  "node": true,
@@ -25,7 +25,7 @@ const LAYOUT = {
25
25
  "parserOptions": {
26
26
  "ecmaVersion": 2022
27
27
  }
28
- }`,
28
+ }\n`,
29
29
  forms: {
30
30
  app: {},
31
31
  collect: {},
@@ -48,7 +48,7 @@ const LAYOUT = {
48
48
  function createRecursively(dir, layout) {
49
49
  fs.mkdir(dir);
50
50
 
51
- for (const k in layout) {
51
+ for (const k of Object.keys(layout)) {
52
52
  const path = `${dir}/${k}`;
53
53
 
54
54
  const val = layout[k];
@@ -58,14 +58,20 @@ function createRecursively(dir, layout) {
58
58
  } else {
59
59
  createRecursively(path, val);
60
60
  }
61
- } else fs.write(path, val);
61
+ } else {
62
+ fs.write(path, val);
63
+ }
62
64
  }
63
65
  }
64
66
 
65
67
  function execute() {
66
68
  const { extraArgs } = environment;
67
- if(extraArgs && extraArgs.length) extraArgs.forEach(createProject);
68
- else createProject('.');
69
+ if(extraArgs?.length) {
70
+ extraArgs.forEach(createProject);
71
+ }
72
+ else {
73
+ createProject('.');
74
+ }
69
75
 
70
76
  function createProject(root) {
71
77
  const dir = path.join(environment.pathToProject, root);
@@ -31,12 +31,18 @@ const parseExtraArgs = (projectDir, extraArgs = []) => {
31
31
 
32
32
  if (!args.destination) {
33
33
  usage();
34
- throw Error(`Action "merge-contacts" is missing required contact ID ${bold('--destination')}. Other contacts will be merged into this contact.`);
34
+ throw Error(
35
+ `Action "merge-contacts" is missing required contact ID ${bold('--destination')}. `
36
+ + 'Other contacts will be merged into this contact.'
37
+ );
35
38
  }
36
39
 
37
40
  if (sourceIds.length === 0) {
38
41
  usage();
39
- throw Error(`Action "merge-contacts" is missing required contact ID(s) ${bold('--sources')}. These contacts will be merged into the contact specified by ${bold('--destination')}`);
42
+ throw Error(
43
+ `Action "merge-contacts" is missing required contact ID(s) ${bold('--sources')}. `
44
+ + `These contacts will be merged into the contact specified by ${bold('--destination')}`
45
+ );
40
46
  }
41
47
 
42
48
  return {
@@ -50,6 +56,7 @@ const parseExtraArgs = (projectDir, extraArgs = []) => {
50
56
 
51
57
  const bold = text => `\x1b[1m${text}\x1b[0m`;
52
58
  const usage = () => {
59
+ /* eslint-disable max-len */
53
60
  info(`
54
61
  ${bold('cht-conf\'s merge-contacts action')}
55
62
  When combined with 'upload-docs' this action moves all of the contacts and reports under ${bold('sources')} to be under ${bold('destination')}.
@@ -71,4 +78,5 @@ ${bold('OPTIONS')}
71
78
  --docDirectoryPath=<path to stage docs>
72
79
  Specifies the folder used to store the documents representing the changes in hierarchy.
73
80
  `);
81
+ /* eslint-enable max-len */
74
82
  };
@@ -48,6 +48,7 @@ const parseExtraArgs = (projectDir, extraArgs = []) => {
48
48
 
49
49
  const bold = text => `\x1b[1m${text}\x1b[0m`;
50
50
  const usage = () => {
51
+ /* eslint-disable max-len */
51
52
  info(`
52
53
  ${bold('cht-conf\'s move-contacts action')}
53
54
  When combined with 'upload-docs' this action effectively moves a contact from one place in the hierarchy to another.
@@ -65,4 +66,5 @@ ${bold('OPTIONS')}
65
66
  --docDirectoryPath=<path to stage docs>
66
67
  Specifies the folder used to store the documents representing the changes in hierarchy.
67
68
  `);
69
+ /* eslint-enable max-len */
68
70
  };
@@ -40,23 +40,23 @@ const execute = async () => {
40
40
  }
41
41
  for (const error of err.errors) {
42
42
  switch (error.error) {
43
- case 'cannot-access-dir':
44
- return log.warn('Could not find custom translations dir:', dir);
45
- case 'missed-placeholder':
46
- case 'wrong-placeholder':
47
- placeholderErrorsFound++;
48
- log.error(error.message);
49
- break;
50
- case 'empty-message':
51
- emptiesFound++;
52
- log.warn(error.message);
53
- break;
54
- case 'wrong-messageformat':
55
- formatErrorsFound++;
56
- log.error(error.message);
57
- break;
58
- default: // 'wrong-file-name', ...
59
- log.error(error.message);
43
+ case 'cannot-access-dir':
44
+ return log.warn('Could not find custom translations dir:', dir);
45
+ case 'missed-placeholder':
46
+ case 'wrong-placeholder':
47
+ placeholderErrorsFound++;
48
+ log.error(error.message);
49
+ break;
50
+ case 'empty-message':
51
+ emptiesFound++;
52
+ log.warn(error.message);
53
+ break;
54
+ case 'wrong-messageformat':
55
+ formatErrorsFound++;
56
+ log.error(error.message);
57
+ break;
58
+ default: // 'wrong-file-name', ...
59
+ log.error(error.message);
60
60
  }
61
61
  }
62
62
  if (emptiesFound > 0) {
@@ -74,11 +74,14 @@ const execute = async () => {
74
74
  }
75
75
  }
76
76
 
77
- for (let fileName of fileNames) {
77
+ for (const fileName of fileNames) {
78
78
  const id = idFor(fileName);
79
79
  const languageCode = id.substring('messages-'.length);
80
80
  if (!isLanguageCodeValid(languageCode)) {
81
- throw new Error(`The language code '${languageCode}' is not valid. It must begin with a letter(a-z, A-Z), followed by any number of hyphens, underscores, letters, or numbers.`);
81
+ throw new Error(
82
+ `The language code '${languageCode}' is not valid. It must begin with a letter(a-z, A-Z), `
83
+ + 'followed by any number of hyphens, underscores, letters, or numbers.'
84
+ );
82
85
  }
83
86
 
84
87
  let languageName = iso639.getName(languageCode);
@@ -86,7 +89,7 @@ const execute = async () => {
86
89
  log.warn(`'${languageCode}' is not a recognized ISO 639 language code, please ask admin to set the name`);
87
90
  languageName = 'TODO: please ask admin to set this in settings UI';
88
91
  } else {
89
- let languageNativeName = iso639.getNativeName(languageCode);
92
+ const languageNativeName = iso639.getNativeName(languageCode);
90
93
  if (languageNativeName !== languageName) {
91
94
  languageName = `${languageNativeName} (${languageName})`;
92
95
  }
@@ -101,7 +104,9 @@ const execute = async () => {
101
104
  if (e.status === 404) {
102
105
  doc = await newDocFor(fileName, db, languageName, languageCode);
103
106
  }
104
- else throw e;
107
+ else {
108
+ throw e;
109
+ }
105
110
  }
106
111
 
107
112
  overwriteProperties(doc, translations);
@@ -122,7 +127,9 @@ const execute = async () => {
122
127
  function parse(filePath, options) {
123
128
  return new Promise((resolve, reject) => {
124
129
  properties.parse(filePath, options, (err, parsed) => {
125
- if (err) return reject(err);
130
+ if (err) {
131
+ return reject(err);
132
+ }
126
133
  resolve(parsed);
127
134
  });
128
135
  });
@@ -37,17 +37,23 @@ async function execute() {
37
37
  throw new Error(`upload-docs: ${errors.join('\n')}`);
38
38
  }
39
39
 
40
- userPrompt.warnPromptAbort(`This operation will permanently write ${totalCount} docs. Are you sure you want to continue?`);
40
+ userPrompt.warnPromptAbort(
41
+ `This operation will permanently write ${totalCount} docs. Are you sure you want to continue?`
42
+ );
41
43
 
42
44
  const deletedDocIds = analysis.map(result => result.delete).filter(Boolean);
43
45
  await handleUsersAtDeletedFacilities(deletedDocIds);
44
46
 
45
47
  const results = { ok:[], failed:{} };
46
- const progress = log.level > log.LEVEL_ERROR ? progressBar.init(totalCount, '{{n}}/{{N}} docs ', ' {{%}} {{m}}:{{s}}') : null;
48
+ const progress = log.level > log.LEVEL_ERROR
49
+ ? progressBar.init(totalCount, '{{n}}/{{N}} docs ', ' {{%}} {{m}}:{{s}}')
50
+ : null;
47
51
  const processNextBatch = async (docFiles, batchSize) => {
48
52
  const now = new Date();
49
53
  if(!docFiles.length) {
50
- if(progress) progress.done();
54
+ if(progress) {
55
+ progress.done();
56
+ }
51
57
 
52
58
  const reportFile = `upload-docs.${now.getTime()}.log.json`;
53
59
  fs.writeJson(reportFile, results);
@@ -57,11 +63,11 @@ async function execute() {
57
63
  }
58
64
 
59
65
  const docs = docFiles.slice(0, batchSize)
60
- .map(file => {
61
- const doc = fs.readJson(file);
62
- doc.imported_date = now.toISOString();
63
- return doc;
64
- });
66
+ .map(file => {
67
+ const doc = fs.readJson(file);
68
+ doc.imported_date = now.toISOString();
69
+ return doc;
70
+ });
65
71
 
66
72
  trace('');
67
73
  trace(`Attempting to upload batch of ${docs.length} docs…`);
@@ -88,7 +94,10 @@ async function execute() {
88
94
  trace('Server connection timed out. Decreasing batch size…');
89
95
  return processNextBatch(docFiles, batchSize / 2);
90
96
  } else {
91
- warn('Server connection timed out for batch size of 1 document. We will continue to retry, but you might want to cancel the job if this continues.');
97
+ warn(
98
+ 'Server connection timed out for batch size of 1 document. '
99
+ + 'We will continue to retry, but you might want to cancel the job if this continues.'
100
+ );
92
101
  return processNextBatch(docFiles, 1);
93
102
  }
94
103
  } else {
@@ -107,7 +116,9 @@ function analyseFiles(filePaths) {
107
116
  const idFromFilename = path.basename(filePath, FILE_EXTENSION);
108
117
 
109
118
  if (json._id !== idFromFilename) {
110
- return { error: `File '${filePath}' sets _id:'${json._id}' but the file's expected _id is '${idFromFilename}'.` };
119
+ return {
120
+ error: `File '${filePath}' sets _id:'${json._id}' but the file's expected _id is '${idFromFilename}'.`
121
+ };
111
122
  }
112
123
 
113
124
  if (json._deleted && json.cht_disable_linked_users) {
@@ -131,14 +142,19 @@ async function handleUsersAtDeletedFacilities(deletedDocIds) {
131
142
  return;
132
143
  }
133
144
 
134
- userPrompt.warnPromptAbort(`This operation will update ${affectedUsers.length} user accounts: ${usernames} and cannot be undone. Are you sure you want to continue?`);
145
+ userPrompt.warnPromptAbort(
146
+ `This operation will update ${affectedUsers.length} user accounts: ${usernames} and cannot be undone. `
147
+ + 'Are you sure you want to continue?'
148
+ );
135
149
  await updateAffectedUsers(affectedUsers);
136
150
  }
137
151
 
138
152
  async function assertCoreVersion() {
139
153
  const actualCoreVersion = await getValidApiVersion();
140
154
  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}.`);
155
+ throw Error(
156
+ `CHT Core Version 4.7.0 or newer is required to use --disable-users options. Version is ${actualCoreVersion}.`
157
+ );
142
158
  }
143
159
 
144
160
  trace(`Core version is ${actualCoreVersion}. Proceeding to disable users.`);
@@ -177,7 +193,8 @@ function removePlace(userDoc, placeId) {
177
193
  }
178
194
 
179
195
  async function updateAffectedUsers(affectedUsers) {
180
- let disabledUsers = 0, updatedUsers = 0;
196
+ let disabledUsers = 0;
197
+ let updatedUsers = 0;
181
198
  for (const userDoc of affectedUsers) {
182
199
  const shouldDisable = !userDoc.place || userDoc.place?.length === 0;
183
200
  if (shouldDisable) {
@@ -28,7 +28,10 @@ module.exports = {
28
28
  return;
29
29
  }
30
30
  if (attachmentFile && attachmentFile.content_type !== 'text/html') {
31
- log.warn(`Privacy policies attachment files must be of type text/html. Found ${attachmentFileName} of type ${attachmentFile.content_type}. Skipping.`);
31
+ log.warn(
32
+ 'Privacy policies attachment files must be of type text/html. '
33
+ + `Found ${attachmentFileName} of type ${attachmentFile.content_type}. Skipping.`
34
+ );
32
35
  delete privacyPoliciesMapping[languageCode];
33
36
  return;
34
37
  }
@@ -18,18 +18,18 @@ module.exports = {
18
18
  const raw = fs.readCsv(csvFile);
19
19
 
20
20
  const messages = raw.rows.map(row => {
21
- const valueOf = column => row[raw.cols.indexOf(column)];
21
+ const valueOf = column => row[raw.cols.indexOf(column)];
22
22
 
23
- return {
24
- id: uuid(),
25
- from: valueOf('from'),
26
- content: valueOf('message'),
27
- sms_sent: valueOf('sent_timestamp') || Date.now(),
28
- sms_received: Date.now(),
29
- };
30
- });
23
+ return {
24
+ id: uuid(),
25
+ from: valueOf('from'),
26
+ content: valueOf('message'),
27
+ sms_sent: valueOf('sent_timestamp') || Date.now(),
28
+ sms_received: Date.now(),
29
+ };
30
+ });
31
31
 
32
- return api().uploadSms(messages);
33
- }, Promise.resolve());
32
+ return api().uploadSms(messages);
33
+ }, Promise.resolve());
34
34
  }
35
35
  };