cht-conf 4.4.0 → 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 +14 -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 +15 -9
  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
@@ -3,7 +3,9 @@ const joi = require('joi');
3
3
  const { error, warn } = require('../log');
4
4
 
5
5
  const err = (filename, message) => details => {
6
- const acceptedValues = details[0].local.valids ? ` but only ${JSON.stringify(details[0].local.valids)} are allowed` : '';
6
+ const acceptedValues = details[0].local.valids
7
+ ? ` but only ${JSON.stringify(details[0].local.valids)} are allowed`
8
+ : '';
7
9
  return new Error(`Invalid schema at ${filename}${details[0].local.label}
8
10
  ${message}
9
11
  Current value of ${filename}${details[0].local.label} is ${JSON.stringify(details[0].value)}${acceptedValues}
@@ -45,8 +47,9 @@ const TargetSchema = joi.array().items(
45
47
  then: joi.function().required(),
46
48
  otherwise: joi.function().forbidden(),
47
49
  })
48
- })
49
- .error(targetError('"passesIf" is required only "type=percent" and "groupBy" is not defined. Otherwise, it is forbidden.')),
50
+ }).error(targetError(
51
+ '"passesIf" is required only "type=percent" and "groupBy" is not defined. Otherwise, it is forbidden.'
52
+ )),
50
53
  groupBy: joi.function().optional()
51
54
  .error(targetError('"groupBy" should be of type function(contact, report)')),
52
55
  passesIfGroupCount: joi.alternatives().conditional('groupBy', {
@@ -57,23 +60,25 @@ const TargetSchema = joi.array().items(
57
60
  otherwise: joi.forbidden(),
58
61
  }),
59
62
  date: joi.alternatives().try(
60
- joi.string().valid('reported', 'now'),
61
- joi.function(),
62
- )
63
+ joi.string().valid('reported', 'now'),
64
+ joi.function(),
65
+ )
63
66
  .optional()
64
- .error(targetError('"date" should be either ["reported", "now"] or "function(contact, report)" returning timestamp')),
67
+ .error(targetError(
68
+ '"date" should be either ["reported", "now"] or "function(contact, report)" returning timestamp'
69
+ )),
65
70
  emitCustom: joi.function().optional()
66
71
  .error(targetError('"emitCustom" should be a function')),
67
72
  dhis: joi.alternatives().try(
68
- DhisSchema,
69
- joi.array().items(DhisSchema),
70
- )
71
- .optional(),
73
+ DhisSchema,
74
+ joi.array().items(DhisSchema),
75
+ )
76
+ .optional(),
72
77
  visible: joi.boolean().optional(),
73
78
  idType: joi.alternatives().try(
74
- joi.string().valid('report', 'contact'),
75
- joi.function(),
76
- )
79
+ joi.string().valid('report', 'contact'),
80
+ joi.function(),
81
+ )
77
82
  .optional()
78
83
  .error(targetError('idType should be either "report" or "contact" or "function(contact, report)"')),
79
84
  aggregate: joi.boolean().optional(),
@@ -83,14 +88,20 @@ const TargetSchema = joi.array().items(
83
88
  .required();
84
89
 
85
90
  const EventSchema = idPresence => joi.object({
86
- id: joi.string().presence(idPresence),
87
- days: joi.alternatives().conditional('dueDate', { is: joi.exist(), then: joi.forbidden(), otherwise: joi.number().required() })
88
- .error(taskError('"event.days" is a required integer field only when "event.dueDate" is absent')),
89
- dueDate: joi.alternatives().conditional('days', { is: joi.exist(), then: joi.forbidden(), otherwise: joi.function().required() })
90
- .error(taskError('"event.dueDate" is required to be "function(event, contact, report)" only when "event.days" is absent')),
91
- start: joi.number().min(0).required(),
92
- end: joi.number().min(0).required(),
93
- });
91
+ id: joi.string().presence(idPresence),
92
+ days: joi.alternatives().conditional(
93
+ 'dueDate',
94
+ { is: joi.exist(), then: joi.forbidden(), otherwise: joi.number().required() }
95
+ ).error(taskError('"event.days" is a required integer field only when "event.dueDate" is absent')),
96
+ dueDate: joi.alternatives().conditional(
97
+ 'days',
98
+ { is: joi.exist(), then: joi.forbidden(), otherwise: joi.function().required() }
99
+ ).error(taskError(
100
+ '"event.dueDate" is required to be "function(event, contact, report)" only when "event.days" is absent'
101
+ )),
102
+ start: joi.number().min(0).required(),
103
+ end: joi.number().min(0).required(),
104
+ });
94
105
 
95
106
  const TaskSchema = joi.array().items(
96
107
  joi.object({
@@ -103,7 +114,7 @@ const TaskSchema = joi.array().items(
103
114
  appliesToType: joi.array().items(joi.string()).optional().min(1),
104
115
  contactLabel:
105
116
  joi.alternatives().try( joi.string().min(1), joi.function() ).optional()
106
- .error(taskError('"contactLabel" should either be a non-empty string or function(contact, report)')),
117
+ .error(taskError('"contactLabel" should either be a non-empty string or function(contact, report)')),
107
118
  resolvedIf: joi
108
119
  .alternatives()
109
120
  .conditional('actions', {
@@ -111,12 +122,10 @@ const TaskSchema = joi.array().items(
111
122
  then: joi.function().optional(),
112
123
  otherwise: joi.function().required()
113
124
  })
114
- .error(
115
- taskError(
116
- 'ERROR: Schema error in actions array: Actions with property "type" which value is different than "report", ' +
117
- 'should define property "resolvedIf" as: function(contact, report) { ... }.'
118
- )
119
- ),
125
+ .error(taskError(
126
+ 'ERROR: Schema error in actions array: Actions with property "type" which value is different than "report", ' +
127
+ 'should define property "resolvedIf" as: function(contact, report) { ... }.'
128
+ )),
120
129
  events: joi.alternatives().conditional('events', {
121
130
  is: joi.array().length(1),
122
131
  then: joi.array().items(EventSchema('optional')).min(1).required(),
@@ -130,11 +139,18 @@ const TaskSchema = joi.array().items(
130
139
  joi.function(),
131
140
  )
132
141
  .optional()
133
- .error(taskError('"priority" should be an object with optional fields "level" and "label" or a function which returns the same')),
142
+ .error(taskError(
143
+ '"priority" should be an object with optional fields "level" and "label" or a function which returns the same'
144
+ )),
134
145
  actions: joi.array().items(
135
146
  joi.object({
136
147
  type: joi.string().valid('report', 'contact').optional(),
137
- form: joi.alternatives().conditional('type', { is: 'contact', then: joi.forbidden(), otherwise: joi.string().min(1).required() }),
148
+ form: joi
149
+ .alternatives()
150
+ .conditional(
151
+ 'type',
152
+ { is: 'contact', then: joi.forbidden(), otherwise: joi.string().min(1).required() }
153
+ ),
138
154
  label: joi.string().min(1).optional(),
139
155
  modifyContent: joi.function().optional()
140
156
  .error(taskError('"actions.modifyContent" should be "function (content, contact, report)')),
@@ -181,7 +197,8 @@ const validate = (filename, fileContent, schema) => {
181
197
  const formatJoiError = (desc, detail) => {
182
198
  const { context } = detail;
183
199
  if (detail.type === 'array.unique') {
184
- return `${desc}${context.label} contains duplicate value for the "${context.path}" field: "${context.value[context.path]}"`;
200
+ const fieldValue = context.value[context.path];
201
+ return `${desc}${context.label} contains duplicate value for the "${context.path}" field: "${fieldValue}"`;
185
202
  }
186
203
 
187
204
  let result = detail.message;
@@ -31,7 +31,9 @@ const formFileMatcher = (fileName) => {
31
31
  };
32
32
 
33
33
  const execute = async (projectDir, subDirectory, options) => {
34
- if (!options) options = {};
34
+ if (!options) {
35
+ options = {};
36
+ }
35
37
 
36
38
  const formsDir = getFormDir(projectDir, subDirectory);
37
39
 
@@ -43,7 +45,7 @@ const execute = async (projectDir, subDirectory, options) => {
43
45
  const filesToConvert = argsFormFilter(formsDir, FORM_EXTENSION, options)
44
46
  .filter(name => formFileMatcher(name));
45
47
 
46
- for (let xls of filesToConvert) {
48
+ for (const xls of filesToConvert) {
47
49
  const originalSourcePath = `${formsDir}/${xls}`;
48
50
  let sourcePath;
49
51
 
@@ -51,7 +53,9 @@ const execute = async (projectDir, subDirectory, options) => {
51
53
  const temporaryPath = `${fs.mkdtemp()}/${options.force_data_node}.xlsx`;
52
54
  fs.copy(originalSourcePath, temporaryPath);
53
55
  sourcePath = temporaryPath;
54
- } else sourcePath = originalSourcePath;
56
+ } else {
57
+ sourcePath = originalSourcePath;
58
+ }
55
59
 
56
60
  const targetPath = `${fs.withoutExtension(originalSourcePath)}.xml`;
57
61
 
@@ -76,8 +80,14 @@ const xls2xform = (sourcePath, targetPath) =>
76
80
  if (executableAvailable()) {
77
81
  if (e.includes('unrecognized arguments: --skip_validate')) {
78
82
  throw new Error('Your xls2xform installation appears to be out of date.' + UPDATE_INSTRUCTIONS);
79
- } else throw e;
80
- } else throw new Error('There was a problem executing xls2xform. It may not be installed.' + INSTALLATION_INSTRUCTIONS);
83
+ } else {
84
+ throw e;
85
+ }
86
+ } else {
87
+ throw new Error(
88
+ 'There was a problem executing xls2xform. It may not be installed.' + INSTALLATION_INSTRUCTIONS
89
+ );
90
+ }
81
91
  });
82
92
 
83
93
  // here we fix the form content in arcane ways. Seeing as we have out own fork
@@ -103,14 +113,18 @@ const fixXml = (path, hiddenFields, transformer, enketo) => {
103
113
 
104
114
  // Enketo _may_ not work with forms which define a default language - see
105
115
  // https://github.com/medic/cht-core/issues/3174
106
- if (enketo) xml = xml.replace(/ default="true\(\)"/g, '');
116
+ if (enketo) {
117
+ xml = xml.replace(/ default="true\(\)"/g, '');
118
+ }
107
119
 
108
120
  if (hiddenFields) {
109
121
  const r = new RegExp(`<(${hiddenFields.join('|')})(/?)>`, 'g');
110
122
  xml = xml.replace(r, '<$1 tag="hidden"$2>');
111
123
  }
112
124
 
113
- if (transformer) xml = transformer(xml, path);
125
+ if (transformer) {
126
+ xml = transformer(xml, path);
127
+ }
114
128
 
115
129
 
116
130
  // Check for deprecations
@@ -122,8 +136,12 @@ const fixXml = (path, hiddenFields, transformer, enketo) => {
122
136
  };
123
137
 
124
138
  function getHiddenFields(propsJson) {
125
- if (!fs.exists(propsJson)) return [];
126
- else return fs.readJson(propsJson).hidden_fields;
139
+ if (!fs.exists(propsJson)) {
140
+ return [];
141
+ }
142
+ else {
143
+ return fs.readJson(propsJson).hidden_fields;
144
+ }
127
145
  }
128
146
 
129
147
  function executableAvailable() {
@@ -6,26 +6,34 @@ const log = require('../lib/log');
6
6
 
7
7
  module.exports = (args, logLevel=log.level) => new Promise((resolve, reject) => {
8
8
 
9
- // Include stdout at log.LEVEL_WARN because xls2xform outputs warnings on stdout
9
+ // Include stdout at log.LEVEL_WARN because xls2xform outputs warnings on stdout
10
10
 
11
- let stdio;
12
- if (logLevel >= log.LEVEL_WARN) {
13
- stdio = [ 'ignore', 'pipe', 'pipe' ];
14
- } else if (logLevel >= log.LEVEL_ERROR) {
15
- stdio = [ 'ignore', 'ignore', 'pipe' ];
16
- } else {
17
- stdio = [ 'ignore', 'ignore', 'ignore' ];
18
- }
11
+ let stdio;
12
+ if (logLevel >= log.LEVEL_WARN) {
13
+ stdio = [ 'ignore', 'pipe', 'pipe' ];
14
+ } else if (logLevel >= log.LEVEL_ERROR) {
15
+ stdio = [ 'ignore', 'ignore', 'pipe' ];
16
+ } else {
17
+ stdio = [ 'ignore', 'ignore', 'ignore' ];
18
+ }
19
19
 
20
- const sub = exec(
21
- args.join(' '),
22
- { stdio:stdio },
23
- (err, stdout, stderr) => {
24
- if(err) reject(stderr);
25
- else resolve(stdout);
26
- });
20
+ const sub = exec(
21
+ args.join(' '),
22
+ { stdio:stdio },
23
+ (err, stdout, stderr) => {
24
+ if(err) {
25
+ reject(stderr);
26
+ }
27
+ else {
28
+ resolve(stdout);
29
+ }
30
+ });
27
31
 
28
- if(logLevel >= log.LEVEL_WARN) sub.stdout.pipe(process.stdout);
29
- if(logLevel >= log.LEVEL_ERROR) sub.stderr.pipe(process.stderr);
32
+ if(logLevel >= log.LEVEL_WARN) {
33
+ sub.stdout.pipe(process.stdout);
34
+ }
35
+ if(logLevel >= log.LEVEL_ERROR) {
36
+ sub.stderr.pipe(process.stderr);
37
+ }
30
38
 
31
- });
39
+ });
@@ -9,8 +9,8 @@ const findErrors = (rows) => {
9
9
 
10
10
  rows.forEach(row => {
11
11
  if (!row.doc) {
12
- errors.docNotFound.push(`Document with id '${row.key}' could not be found.`);
13
- return;
12
+ errors.docNotFound.push(`Document with id '${row.key}' could not be found.`);
13
+ return;
14
14
  }
15
15
 
16
16
  if (!VALID_DOC_TYPES.includes(row.doc.type)) {
@@ -3,7 +3,7 @@ const fs = require('./sync-fs');
3
3
  const info = require('./log').info;
4
4
 
5
5
  module.exports = (data, csvPath) => {
6
- const fields = ['username', 'password', 'roles', 'contact', 'phone', 'place'];
6
+ const fields = ['username', 'password', 'roles', 'contact', 'phone', 'place', 'fullname', 'email'];
7
7
  const json2csvParser = new Json2csvParser({
8
8
  fields,
9
9
  doubleQuote: '\'',
@@ -25,7 +25,10 @@ const getApiUrl = (cmdArgs, env = {}) => {
25
25
  // See ./archiving-db.js
26
26
  instanceUrl = parseLocalUrl(env.COUCH_URL);
27
27
  if (instanceUrl.hostname !== 'localhost') {
28
- throw Error(`--local was specified but COUCH_URL env var is set to '${instanceUrl.hostname}'. Please use --url for remote servers.`);
28
+ throw Error(
29
+ `--local was specified but COUCH_URL env var is set to '${instanceUrl.hostname}'. `
30
+ + `Please use --url for remote servers.`
31
+ );
29
32
  }
30
33
  } else if (cmdArgs.instance) {
31
34
  const password = userPrompt.question(`${emoji.key} Password: `, { hideEchoBack: true });
@@ -11,11 +11,15 @@ const TOKENS_FILE = './.gdrive.session.json';
11
11
  module.exports = () => {
12
12
  const client = oauthClient();
13
13
 
14
- if(!fs.exists(TOKENS_FILE)) return newSessionFor(client);
14
+ if(!fs.exists(TOKENS_FILE)) {
15
+ return newSessionFor(client);
16
+ }
15
17
 
16
18
  client.setCredentials(fs.readJson(TOKENS_FILE));
17
19
 
18
- if(client.credentials.expiry_date < Date.now()) return newSessionFor(client);
20
+ if(client.credentials.expiry_date < Date.now()) {
21
+ return newSessionFor(client);
22
+ }
19
23
 
20
24
  return Promise.resolve(client);
21
25
  };
@@ -30,7 +34,9 @@ function newSessionFor(client) {
30
34
 
31
35
  return new Promise((resolve, reject) => {
32
36
  client.getToken(accessCode, function (err, tokens) {
33
- if(err) return reject(err);
37
+ if(err) {
38
+ return reject(err);
39
+ }
34
40
 
35
41
  fs.writeJson(TOKENS_FILE, tokens);
36
42
 
@@ -61,7 +67,9 @@ function oauthClient() {
61
67
  checkRequired(clientSecret, 'client_secret', 'GOOGLE_CLIENT_SECRET') |
62
68
  checkRequired(redirectUri, 'redirect_uris', 'GOOGLE_REDIRECT_URI');
63
69
 
64
- if(missingConfig) throw new Error('Missing required config for google drive access. Please check warnings.');
70
+ if(missingConfig) {
71
+ throw new Error('Missing required config for google drive access. Please check warnings.');
72
+ }
65
73
 
66
74
  return new google.auth.OAuth2(clientId, clientSecret, redirectUri);
67
75
  }
@@ -72,7 +80,9 @@ function openBrowserAt(url) {
72
80
  }
73
81
 
74
82
  function checkRequired(value, jsonKey, envVar) {
75
- if(value) return;
83
+ if(value) {
84
+ return;
85
+ }
76
86
 
77
87
  warn(`Missing .${jsonKey} in ${SECRETS_FILE} or env var ${envVar}!`);
78
88
  return true;
@@ -23,7 +23,10 @@ async function deleteHierarchy(db, options, sourceIds) {
23
23
 
24
24
  const affectedContactCount = descendantsAndSelf.length;
25
25
 
26
- info(`Staged updates to delete ${prettyPrintDocument(sourceDoc)}. ${affectedContactCount.length} contact(s) and ${affectedReportCount} report(s).`);
26
+ info(
27
+ `Staged updates to delete ${prettyPrintDocument(sourceDoc)}. ${affectedContactCount.length} contact(s) `
28
+ + `and ${affectedReportCount} report(s).`
29
+ );
27
30
  }
28
31
  }
29
32
 
@@ -12,7 +12,9 @@ async function getContactsByIds(db, ids) {
12
12
  include_docs: true,
13
13
  });
14
14
 
15
- const missingContactErrors = contactDocs.rows.filter(row => !row.doc).map(row => `Contact with id '${row.key}' could not be found.`);
15
+ const missingContactErrors = contactDocs.rows
16
+ .filter(row => !row.doc)
17
+ .map(row => `Contact with id '${row.key}' could not be found.`);
16
18
  if (missingContactErrors.length > 0) {
17
19
  throw Error(missingContactErrors);
18
20
  }
@@ -86,7 +88,10 @@ async function getAncestorsOf(db, contactDoc) {
86
88
 
87
89
  const ancestorIdsNotFound = ancestors.rows.filter(ancestor => !ancestor.doc).map(ancestor => ancestor.key);
88
90
  if (ancestorIdsNotFound.length > 0) {
89
- throw Error(`Contact '${contactDoc?.name}' (${contactDoc?._id}) has parent id(s) '${ancestorIdsNotFound.join(',')}' which could not be found.`);
91
+ throw Error(
92
+ `Contact '${contactDoc?.name}' (${contactDoc?._id}) has parent id(s) `
93
+ + `'${ancestorIdsNotFound.join(',')}' which could not be found.`
94
+ );
90
95
  }
91
96
 
92
97
  return ancestors.rows.map(ancestor => ancestor.doc);
@@ -35,11 +35,17 @@ async function moveHierarchy(db, options, sourceIds, destinationId) {
35
35
  }
36
36
 
37
37
  const prettyPrintDocument = doc => `'${doc.name}' (${doc._id})`;
38
- trace(`Considering lineage updates to ${descendantsAndSelf.length} descendant(s) of contact ${prettyPrintDocument(sourceDoc)}.`);
38
+ trace(
39
+ `Considering lineage updates to ${descendantsAndSelf.length} descendant(s) of contact `
40
+ + `${prettyPrintDocument(sourceDoc)}.`
41
+ );
39
42
  const updatedDescendants = replaceLineageInContacts(moveContext);
40
43
 
41
44
  const ancestors = await DataSource.getAncestorsOf(db, sourceDoc);
42
- trace(`Considering primary contact updates to ${ancestors.length} ancestor(s) of contact ${prettyPrintDocument(sourceDoc)}.`);
45
+ trace(
46
+ `Considering primary contact updates to ${ancestors.length} ancestor(s) of contact `
47
+ + `${prettyPrintDocument(sourceDoc)}.`
48
+ );
43
49
  const updatedAncestors = replaceLineageInAncestors(descendantsAndSelf, ancestors);
44
50
 
45
51
  minifyLineageAndWriteToDisk(options, [...updatedDescendants, ...updatedAncestors]);
@@ -50,10 +56,16 @@ async function moveHierarchy(db, options, sourceIds, destinationId) {
50
56
  affectedContactCount += updatedDescendants.length + updatedAncestors.length;
51
57
  affectedReportCount += movedReportsCount;
52
58
 
53
- info(`Staged updates to ${prettyPrintDocument(sourceDoc)}. ${updatedDescendants.length} contact(s) and ${movedReportsCount} report(s).`);
59
+ info(
60
+ `Staged updates to ${prettyPrintDocument(sourceDoc)}. ${updatedDescendants.length} contact(s) and `
61
+ + `${movedReportsCount} report(s).`
62
+ );
54
63
  }
55
64
 
56
- info(`Staged changes to lineage information for ${affectedContactCount} contact(s) and ${affectedReportCount} report(s).`);
65
+ info(
66
+ `Staged changes to lineage information for ${affectedContactCount} contact(s) and `
67
+ + `${affectedReportCount} report(s).`
68
+ );
57
69
  }
58
70
 
59
71
  async function updateReports(db, options, moveContext) {
@@ -22,7 +22,10 @@ function writeDoc({ docDirectoryPath }, doc) {
22
22
  }
23
23
 
24
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?`);
25
+ warn(
26
+ `The document folder '${docDirectoryPath}' already contains files. `
27
+ + 'It is recommended you start with a clean folder. Do you want to delete the contents of this folder and continue?'
28
+ );
26
29
  if (!userPrompt.keyInYN()) {
27
30
  throw new Error('User aborted execution.');
28
31
  }
@@ -11,7 +11,10 @@ module.exports = async (db, options) => {
11
11
  assertNoPrimaryContactViolations: async (sourceDoc, destinationDoc, descendantDocs) => {
12
12
  const invalidPrimaryContactDoc = await getPrimaryContactViolations(db, sourceDoc, destinationDoc, descendantDocs);
13
13
  if (invalidPrimaryContactDoc) {
14
- throw Error(`Cannot remove contact '${invalidPrimaryContactDoc?.name}' (${invalidPrimaryContactDoc?._id}) from the hierarchy for which they are a primary contact.`);
14
+ throw Error(
15
+ `Cannot remove contact '${invalidPrimaryContactDoc?.name}' (${invalidPrimaryContactDoc?._id}) from the `
16
+ + 'hierarchy for which they are a primary contact.'
17
+ );
15
18
  }
16
19
  },
17
20
  assertNoHierarchyErrors: (sourceDocs, destinationDoc) => {
@@ -29,9 +32,10 @@ module.exports = async (db, options) => {
29
32
  });
30
33
 
31
34
  /*
32
- It is nice that the tool can move lists of contacts as one operation, but strange things happen when two contactIds are in the same lineage.
33
- For example, moving a district_hospital and moving a contact under that district_hospital to a new clinic causes multiple colliding writes to the same json file.
34
- */
35
+ * It is nice that the tool can move lists of contacts as one operation, but strange things happen when two
36
+ * contactIds are in the same lineage. For example, moving a district_hospital and moving a contact under that
37
+ * district_hospital to a new clinic causes multiple colliding writes to the same json file.
38
+ */
35
39
  const contactIds = sourceDocs.map(doc => doc._id);
36
40
  sourceDocs
37
41
  .forEach(doc => {
@@ -81,7 +85,8 @@ function findCircularHierarchyErrors(sourceDoc, destinationDoc) {
81
85
 
82
86
  const parentAncestry = lineageManipulation.pluckIdsFromLineage(destinationDoc);
83
87
  if (parentAncestry.includes(sourceDoc._id)) {
84
- return `Circular hierarchy: Cannot set parent of contact '${sourceDoc._id}' as it would create a circular hierarchy.`;
88
+ return `Circular hierarchy: Cannot set parent of contact '${sourceDoc._id}' `
89
+ + `as it would create a circular hierarchy.`;
85
90
  }
86
91
  }
87
92
 
@@ -117,7 +122,8 @@ const getMergeViolations = (sourceDoc, destinationDoc) => {
117
122
  const sourceContactType = getContactType(sourceDoc);
118
123
  const destinationContactType = getContactType(destinationDoc);
119
124
  if (sourceContactType !== destinationContactType) {
120
- return `source and destinations must have the same type. Source is "${sourceContactType}" while destination is "${destinationContactType}".`;
125
+ return `source and destinations must have the same type. `
126
+ + `Source is "${sourceContactType}" while destination is "${destinationContactType}".`;
121
127
  }
122
128
  };
123
129
 
@@ -48,28 +48,30 @@ function replaceEntireLineage(replaceInDoc, lineageAttributeName, replaceWith) {
48
48
  }
49
49
 
50
50
  module.exports = {
51
- /**
52
- * Given a doc, replace the parent's lineage
53
- *
54
- * @param {Object} doc A CouchDB document containing a parent lineage (eg. parent.parent._id)
55
- * @param {Object} params
56
- * @param {Object} params.replaceWith The new hierarchy { parent: { _id: 'parent', parent: { _id: 'grandparent' } }
57
- * @param {string} params.startingFromId Only the part of the lineage "after" this id will be replaced
58
- * @param {boolean} params.merge When true, startingFromId is replaced and when false, startingFromId's parent is replaced
59
- */
51
+ /**
52
+ * Given a doc, replace the parent's lineage
53
+ *
54
+ * @param {Object} doc A CouchDB document containing a parent lineage (eg. parent.parent._id)
55
+ * @param {Object} params
56
+ * @param {Object} params.replaceWith The new hierarchy { parent: { _id: 'parent', parent: { _id: 'grandparent' } }
57
+ * @param {string} params.startingFromId Only the part of the lineage "after" this id will be replaced
58
+ * @param {boolean} params.merge When true, startingFromId is replaced and when false, startingFromId's parent is
59
+ * replaced
60
+ */
60
61
  replaceParentLineage: (doc, params) => {
61
62
  return replaceLineage(doc, 'parent', params);
62
63
  },
63
64
 
64
- /**
65
- * Given a doc, replace the contact's lineage
66
- *
67
- * @param {Object} doc A CouchDB document containing a contact lineage (eg. contact.parent._id)
68
- * @param {Object} params
69
- * @param {Object} params.replaceWith The new hierarchy { parent: { _id: 'parent', parent: { _id: 'grandparent' } }
70
- * @param {string} params.startingFromId Only the part of the lineage "after" this id will be replaced
71
- * @param {boolean} params.merge When true, startingFromId is replaced and when false, startingFromId's parent is replaced
72
- */
65
+ /**
66
+ * Given a doc, replace the contact's lineage
67
+ *
68
+ * @param {Object} doc A CouchDB document containing a contact lineage (eg. contact.parent._id)
69
+ * @param {Object} params
70
+ * @param {Object} params.replaceWith The new hierarchy { parent: { _id: 'parent', parent: { _id: 'grandparent' } }
71
+ * @param {string} params.startingFromId Only the part of the lineage "after" this id will be replaced
72
+ * @param {boolean} params.merge When true, startingFromId is replaced and when false, startingFromId's parent is
73
+ * replaced
74
+ */
73
75
  replaceContactLineage: (doc, params) => {
74
76
  return replaceLineage(doc, 'contact', params);
75
77
  },
@@ -2,7 +2,11 @@ module.exports = (db, doc) =>
2
2
  db.get(doc._id)
3
3
  .then(existingDoc => doc._rev = existingDoc._rev)
4
4
  .catch(e => {
5
- if(e.status === 404) return;
6
- else throw e;
5
+ if(e.status === 404) {
6
+ return;
7
+ }
8
+ else {
9
+ throw e;
10
+ }
7
11
  })
8
12
  .then(() => db.put(doc));
package/src/lib/log.js CHANGED
@@ -30,7 +30,11 @@ function logAtLevel(color, level, ...args) {
30
30
  }
31
31
 
32
32
  const redactUrls = s => {
33
- if(s instanceof Error) s = s.stack || s.message || s;
34
- if(s && typeof s !== 'string') s = JSON.stringify(s, null, 2);
33
+ if(s instanceof Error) {
34
+ s = s.stack || s.message || s;
35
+ }
36
+ if(s && typeof s !== 'string') {
37
+ s = JSON.stringify(s, null, 2);
38
+ }
35
39
  return s && redactBasicAuth(s);
36
40
  };
package/src/lib/main.js CHANGED
@@ -168,11 +168,12 @@ module.exports = async (argv, env) => {
168
168
  await api().available();
169
169
  }
170
170
 
171
- const productionUrlMatch = environment.instanceUrl && environment.instanceUrl.match(/^https:\/\/(?:[^@]*@)?(.*)\.(app|dev)\.medicmobile\.org(?:$|\/)/);
171
+ const productionUrlMatch = environment.instanceUrl
172
+ && environment.instanceUrl.match(/^https:\/\/(?:[^@]*@)?(.*)\.(app|dev)\.medicmobile\.org(?:$|\/)/);
172
173
  const expectedOptions = ['alpha', projectName];
173
174
  if (productionUrlMatch && !expectedOptions.includes(productionUrlMatch[1])) {
174
175
  warn(`Attempting to use project for \x1b[31m${projectName}\x1b[33m`,
175
- `against non-matching instance: \x1b[31m${redactBasicAuth(environment.instanceUrl)}\x1b[33m`);
176
+ `against non-matching instance: \x1b[31m${redactBasicAuth(environment.instanceUrl)}\x1b[33m`);
176
177
  if (!userPrompt.keyInYN()) {
177
178
  throw new Error('User aborted execution.');
178
179
  }
@@ -189,7 +190,7 @@ module.exports = async (argv, env) => {
189
190
  await checkForUpdates({ nonFatal: true });
190
191
  }
191
192
 
192
- for (let action of actions) {
193
+ for (const action of actions) {
193
194
  info(`Starting action: ${action.name}…`);
194
195
  await executeAction(action);
195
196
  info(`${action.name} complete.`);
@@ -2,18 +2,20 @@ const minify = js =>
2
2
  js.split('\n')
3
3
  .map(s =>
4
4
  s.trim()
5
- .replace(/\s*\/\/.*/, '') // single-line comments (like this one)
5
+ .replace(/\s*\/\/.*/, '') // single-line comments (like this one)
6
6
  ).join('')
7
- .replace(/\s*\/\*(?:(?!\*\/).)*\*\/\s*/g, '') /* this kind of comment */
8
- .replace(/function \(/g, 'function(') // different node versions do function.toString() differently :\
7
+ .replace(/\s*\/\*(?:(?!\*\/).)*\*\/\s*/g, '') /* this kind of comment */
8
+ .replace(/function \(/g, 'function(') // different node versions do function.toString() differently :\
9
9
  ;
10
10
 
11
+ /* eslint-disable max-len */
11
12
  const addBoilerplateToCode = code => `define Target { _id: null, contact: null, deleted: null, type: null, pass: null, date: null, groupBy: null }
12
13
  define Contact { contact: null, reports: null, tasks: null }
13
14
  define Task { _id: null, deleted: null, doc: null, contact: null, icon: null, date: null, readyStart: null, readyEnd: null, title: null, fields: null, resolved: null, priority: null, priorityLabel: null, reports: null, actions: null }
14
15
  rule GenerateEvents {
15
16
  when { c: Contact } then { ${code} }
16
17
  }`;
18
+ /* eslint-enable max-len */
17
19
 
18
20
  module.exports = {
19
21
  addBoilerplateToCode,
@@ -14,7 +14,10 @@ module.exports = (projectDir) => {
14
14
  const purgingExists = fs.exists(purgingPath);
15
15
 
16
16
  if (purgeExists && purgingExists) {
17
- throwError(`Purge is defined at both ${purgePath} and ${purgingPath}. Using 'purging.js' is deprecated. Create a 'purge.js' file instead.`);
17
+ throwError(
18
+ `Purge is defined at both ${purgePath} and ${purgingPath}. `
19
+ + `Using 'purging.js' is deprecated. Create a 'purge.js' file instead.`
20
+ );
18
21
  }
19
22
 
20
23
  let purgeConfig;