cht-conf 4.4.1 → 4.6.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/.editorconfig +15 -0
- package/CHANGELOG.md +14 -0
- package/README.md +9 -1
- package/package.json +27 -26
- package/src/bin/shell-completion.js +5 -5
- package/src/cli/supported-actions.js +2 -2
- package/src/cli/usage.js +2 -0
- package/src/fn/backup-all-forms.js +1 -1
- package/src/fn/compile-app-settings.js +46 -15
- package/src/fn/compress-pngs.js +10 -8
- package/src/fn/compress-svgs.js +5 -2
- package/src/fn/create-users.js +6 -3
- package/src/fn/csv-to-docs.js +73 -52
- package/src/fn/delete-contacts.js +2 -0
- package/src/fn/edit-contacts.js +14 -11
- package/src/fn/fetch-csvs-from-google-drive.js +3 -3
- package/src/fn/fetch-forms-from-google-drive.js +3 -3
- package/src/fn/initialise-project-layout.js +14 -8
- package/src/fn/merge-contacts.js +10 -2
- package/src/fn/move-contacts.js +2 -0
- package/src/fn/upload-custom-translations.js +29 -22
- package/src/fn/upload-docs.js +30 -13
- package/src/fn/upload-privacy-policies.js +4 -1
- package/src/fn/upload-sms-from-csv.js +11 -11
- package/src/fn/watch-project.js +212 -204
- package/src/lib/api.js +22 -22
- package/src/lib/args-form-filter.js +1 -1
- package/src/lib/attachments-from-dir.js +3 -1
- package/src/lib/check-cht-conf-dependency-version.js +4 -2
- package/src/lib/check-for-updates.js +4 -3
- package/src/lib/compilation/compile-contact-summary.js +12 -2
- package/src/lib/compilation/compile-tasks-and-targets.js +5 -2
- package/src/lib/compilation/package-lib.js +5 -2
- package/src/lib/compilation/validate-declarative-schema.js +49 -32
- package/src/lib/convert-forms.js +27 -9
- package/src/lib/exec-promise.js +27 -19
- package/src/lib/fetch-document-list.js +2 -2
- package/src/lib/generate-users-csv.js +1 -1
- package/src/lib/get-api-url.js +4 -1
- package/src/lib/google-auth.js +15 -5
- package/src/lib/hierarchy-operations/delete-hierarchy.js +4 -1
- package/src/lib/hierarchy-operations/hierarchy-data-source.js +7 -2
- package/src/lib/hierarchy-operations/index.js +16 -4
- package/src/lib/hierarchy-operations/jsdocFolder.js +4 -1
- package/src/lib/hierarchy-operations/lineage-constraints.js +12 -6
- package/src/lib/hierarchy-operations/replace-lineage.js +20 -18
- package/src/lib/insert-or-replace.js +6 -2
- package/src/lib/log.js +6 -2
- package/src/lib/main.js +4 -3
- package/src/lib/nools-utils.js +5 -3
- package/src/lib/parse-purge.js +5 -3
- package/src/lib/parse-targets.js +3 -1
- package/src/lib/polyfill.js +3 -1
- package/src/lib/progress-bar.js +26 -13
- package/src/lib/skip-fn.js +5 -3
- package/src/lib/sync-fs.js +24 -12
- package/src/lib/upload-forms.js +17 -7
- package/src/lib/validation/form/can-generate-xform.js +3 -1
- package/src/lib/validation/form/check-xpaths-exist.js +8 -3
- package/src/lib/validation/form/deprecated-appearance.js +8 -2
- package/src/lib/validation/form/has-instance-id.js +4 -1
- package/src/lib/validation/form/no-required-notes.js +4 -1
- package/src/lib/warn-upload-overwrite.js +6 -3
|
@@ -5,8 +5,8 @@ module.exports = {
|
|
|
5
5
|
requiresInstance: false,
|
|
6
6
|
execute: async () => {
|
|
7
7
|
await fetchFilesFromGoogleDrive(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
`${environment.pathToProject}/forms-on-google-drive.json`,
|
|
9
|
+
`${environment.pathToProject}/forms`,
|
|
10
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
11
11
|
}
|
|
12
12
|
};
|
|
@@ -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
|
|
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
|
|
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
|
|
68
|
-
|
|
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);
|
package/src/fn/merge-contacts.js
CHANGED
|
@@ -31,12 +31,18 @@ const parseExtraArgs = (projectDir, extraArgs = []) => {
|
|
|
31
31
|
|
|
32
32
|
if (!args.destination) {
|
|
33
33
|
usage();
|
|
34
|
-
throw Error(
|
|
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(
|
|
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
|
};
|
package/src/fn/move-contacts.js
CHANGED
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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
|
|
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)
|
|
130
|
+
if (err) {
|
|
131
|
+
return reject(err);
|
|
132
|
+
}
|
|
126
133
|
resolve(parsed);
|
|
127
134
|
});
|
|
128
135
|
});
|
package/src/fn/upload-docs.js
CHANGED
|
@@ -37,17 +37,23 @@ async function execute() {
|
|
|
37
37
|
throw new Error(`upload-docs: ${errors.join('\n')}`);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
userPrompt.warnPromptAbort(
|
|
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
|
|
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)
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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(
|
|
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 {
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
21
|
+
const valueOf = column => row[raw.cols.indexOf(column)];
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
32
|
+
return api().uploadSms(messages);
|
|
33
|
+
}, Promise.resolve());
|
|
34
34
|
}
|
|
35
35
|
};
|