cht-conf 3.10.2 → 3.12.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/.eslintignore +2 -1
- package/.releaserc.json +1 -1
- package/CHANGELOG.md +22 -1
- package/README.md +1 -1
- package/package.json +6 -3
- package/release-notes.md +1 -1
- package/src/cli/usage.js +3 -0
- package/src/fn/compile-app-settings.js +25 -8
- package/src/fn/compress-svgs.js +1 -1
- package/src/fn/convert-app-forms.js +13 -6
- package/src/fn/convert-collect-forms.js +11 -4
- package/src/fn/convert-contact-forms.js +62 -59
- package/src/fn/delete-forms.js +17 -16
- package/src/fn/move-contacts.js +2 -2
- package/src/fn/upload-app-forms.js +11 -4
- package/src/fn/upload-app-settings.js +28 -22
- package/src/fn/upload-collect-forms.js +10 -5
- package/src/fn/upload-contact-forms.js +13 -6
- package/src/fn/upload-custom-translations.js +7 -4
- package/src/fn/upload-resources.js +6 -2
- package/src/fn/validate-app-forms.js +12 -0
- package/src/fn/validate-collect-forms.js +12 -0
- package/src/fn/validate-contact-forms.js +12 -0
- package/src/fn/watch-project.js +287 -0
- package/src/lib/convert-forms.js +46 -31
- package/src/lib/environment.js +5 -2
- package/src/lib/git-exec.js +1 -1
- package/src/lib/main.js +39 -11
- package/src/lib/project-paths.js +12 -0
- package/src/lib/upload-forms.js +36 -8
- package/src/lib/validate-forms.js +57 -39
- package/src/lib/validation/form/README.md +32 -0
- package/src/lib/validation/form/can-generate-xform.js +18 -0
- package/src/lib/validation/form/has-instance-id.js +14 -0
package/src/lib/upload-forms.js
CHANGED
|
@@ -13,21 +13,41 @@ const {
|
|
|
13
13
|
readTitleFrom,
|
|
14
14
|
readIdFrom
|
|
15
15
|
} = require('./forms-utils');
|
|
16
|
-
const validateForms = require('./validate-forms');
|
|
17
16
|
|
|
18
17
|
const SUPPORTED_PROPERTIES = ['context', 'icon', 'title', 'xml2sms', 'subject_key', 'hidden_fields'];
|
|
18
|
+
const FORM_EXTENSTION = '.xml';
|
|
19
|
+
const FORM_PROPERTIES_EXTENSION = '.properties.json';
|
|
20
|
+
const FORM_MEDIA_MATCHER = /(.+)-media$/;
|
|
21
|
+
const SUPPORTED_EXTENSIONS = [FORM_EXTENSTION, FORM_PROPERTIES_EXTENSION];
|
|
22
|
+
|
|
23
|
+
const formFileMatcher = (fileName) => {
|
|
24
|
+
if (fileName.endsWith(FORM_EXTENSTION)) {
|
|
25
|
+
return fileName.slice(0, fileName.length - FORM_EXTENSTION.length);
|
|
26
|
+
}
|
|
27
|
+
if (fileName.endsWith(FORM_PROPERTIES_EXTENSION)) {
|
|
28
|
+
return fileName.slice(0, fileName.length - FORM_PROPERTIES_EXTENSION.length);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const formMediaMatcher = (formMediaDir) => {
|
|
34
|
+
const matchResult = formMediaDir.match(FORM_MEDIA_MATCHER);
|
|
35
|
+
if (matchResult) {
|
|
36
|
+
return matchResult[1]; // return the captured form name
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
};
|
|
19
40
|
|
|
20
|
-
|
|
21
|
-
await validateForms(projectDir, subDirectory, options);
|
|
41
|
+
const execute = async (projectDir, subDirectory, options) => {
|
|
22
42
|
const db = pouch();
|
|
23
43
|
if (!options) options = {};
|
|
24
44
|
const formsDir = getFormDir(projectDir, subDirectory);
|
|
25
|
-
if(!fs.exists(formsDir)) {
|
|
45
|
+
if (!fs.exists(formsDir)) {
|
|
26
46
|
log.info(`Forms dir not found: ${formsDir}`);
|
|
27
47
|
return;
|
|
28
48
|
}
|
|
29
49
|
|
|
30
|
-
const fileNames = argsFormFilter(formsDir,
|
|
50
|
+
const fileNames = argsFormFilter(formsDir, FORM_EXTENSTION, options);
|
|
31
51
|
for (const fileName of fileNames) {
|
|
32
52
|
log.info(`Preparing form for upload: ${fileName}…`);
|
|
33
53
|
|
|
@@ -35,7 +55,7 @@ module.exports = async (projectDir, subDirectory, options) => {
|
|
|
35
55
|
const baseDocId = (options.id_prefix || '') + baseFileName.replace(/-/g, ':');
|
|
36
56
|
|
|
37
57
|
const mediaDirExists = fs.exists(mediaDir);
|
|
38
|
-
if(!mediaDirExists) {
|
|
58
|
+
if (!mediaDirExists) {
|
|
39
59
|
log.info(`No media directory found at ${mediaDir} for form ${xformPath}`);
|
|
40
60
|
}
|
|
41
61
|
|
|
@@ -48,7 +68,7 @@ module.exports = async (projectDir, subDirectory, options) => {
|
|
|
48
68
|
};
|
|
49
69
|
|
|
50
70
|
const internalId = readIdFrom(xml);
|
|
51
|
-
if(internalId !== baseDocId) log.warn('DEPRECATED', 'Form:', fileName, 'Bad ID set in XML. Expected:', baseDocId, 'but saw:', internalId, ' Support for setting these values differently will be dropped. Please see https://github.com/medic/cht-core/issues/3342.');
|
|
71
|
+
if (internalId !== baseDocId) log.warn('DEPRECATED', 'Form:', fileName, 'Bad ID set in XML. Expected:', baseDocId, 'but saw:', internalId, ' Support for setting these values differently will be dropped. Please see https://github.com/medic/cht-core/issues/3342.');
|
|
52
72
|
|
|
53
73
|
const docId = `form:${baseDocId}`;
|
|
54
74
|
const doc = {
|
|
@@ -60,7 +80,7 @@ module.exports = async (projectDir, subDirectory, options) => {
|
|
|
60
80
|
context: options.default_context,
|
|
61
81
|
};
|
|
62
82
|
|
|
63
|
-
const propertiesPath = `${formsDir}/${baseFileName}
|
|
83
|
+
const propertiesPath = `${formsDir}/${baseFileName}${FORM_PROPERTIES_EXTENSION}`;
|
|
64
84
|
updateFromPropertiesFile(doc, propertiesPath);
|
|
65
85
|
|
|
66
86
|
doc._attachments = mediaDirExists ? attachmentsFromDir(mediaDir) : {};
|
|
@@ -80,6 +100,14 @@ module.exports = async (projectDir, subDirectory, options) => {
|
|
|
80
100
|
}
|
|
81
101
|
};
|
|
82
102
|
|
|
103
|
+
module.exports = {
|
|
104
|
+
SUPPORTED_EXTENSIONS,
|
|
105
|
+
FORM_MEDIA_MATCHER,
|
|
106
|
+
formFileMatcher,
|
|
107
|
+
formMediaMatcher,
|
|
108
|
+
execute
|
|
109
|
+
};
|
|
110
|
+
|
|
83
111
|
const updateFromPropertiesFile = (doc, path) => {
|
|
84
112
|
if (fs.exists(path)) {
|
|
85
113
|
|
|
@@ -1,64 +1,82 @@
|
|
|
1
|
-
const api = require('./api');
|
|
2
1
|
const argsFormFilter = require('./args-form-filter');
|
|
2
|
+
const environment = require('./environment');
|
|
3
3
|
const fs = require('./sync-fs');
|
|
4
4
|
const log = require('./log');
|
|
5
5
|
const {
|
|
6
6
|
getFormDir,
|
|
7
|
-
getFormFilePaths
|
|
8
|
-
formHasInstanceId
|
|
7
|
+
getFormFilePaths
|
|
9
8
|
} = require('./forms-utils');
|
|
10
9
|
|
|
10
|
+
const VALIDATIONS_PATH = fs.path.resolve(__dirname, './validation/form');
|
|
11
|
+
const validations = fs.readdir(VALIDATIONS_PATH)
|
|
12
|
+
.filter(name => name.endsWith('.js'))
|
|
13
|
+
.map(validationName => {
|
|
14
|
+
const validation = require(fs.path.join(VALIDATIONS_PATH, validationName));
|
|
15
|
+
validation.name = validationName;
|
|
16
|
+
if(!Object.hasOwnProperty.call(validation, 'requiresInstance')) {
|
|
17
|
+
validation.requiresInstance = true;
|
|
18
|
+
}
|
|
19
|
+
if(!Object.hasOwnProperty.call(validation, 'skipFurtherValidation')) {
|
|
20
|
+
validation.skipFurtherValidation = false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return validation;
|
|
24
|
+
})
|
|
25
|
+
.sort((a, b) => {
|
|
26
|
+
if(a.skipFurtherValidation && !b.skipFurtherValidation) {
|
|
27
|
+
return -1;
|
|
28
|
+
}
|
|
29
|
+
if(!a.skipFurtherValidation && b.skipFurtherValidation) {
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
return a.name.localeCompare(b.name);
|
|
33
|
+
});
|
|
34
|
+
|
|
11
35
|
module.exports = async (projectDir, subDirectory, options={}) => {
|
|
12
36
|
|
|
13
37
|
const formsDir = getFormDir(projectDir, subDirectory);
|
|
14
38
|
if(!formsDir) {
|
|
15
|
-
log.info(`Forms dir not found: ${
|
|
39
|
+
log.info(`Forms dir not found: ${projectDir}/forms/${subDirectory}`);
|
|
16
40
|
return;
|
|
17
41
|
}
|
|
18
42
|
|
|
19
|
-
|
|
20
|
-
let
|
|
43
|
+
const instanceProvided = environment.apiUrl;
|
|
44
|
+
let validationSkipped = false;
|
|
21
45
|
|
|
22
46
|
const fileNames = argsFormFilter(formsDir, '.xml', options);
|
|
23
|
-
|
|
47
|
+
|
|
48
|
+
let errorFound = false;
|
|
49
|
+
for(const fileName of fileNames) {
|
|
24
50
|
log.info(`Validating form: ${fileName}…`);
|
|
25
51
|
|
|
26
|
-
const { xformPath
|
|
52
|
+
const { xformPath } = getFormFilePaths(formsDir, fileName);
|
|
27
53
|
const xml = fs.read(xformPath);
|
|
28
54
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
55
|
+
const valParams = { xformPath, xmlStr: xml };
|
|
56
|
+
for(const validation of validations) {
|
|
57
|
+
if(validation.requiresInstance && !instanceProvided) {
|
|
58
|
+
validationSkipped = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const output = await validation.execute(valParams);
|
|
63
|
+
if(output.warnings) {
|
|
64
|
+
output.warnings.forEach(warnMsg => log.warn(warnMsg));
|
|
65
|
+
}
|
|
66
|
+
if(output.errors && output.errors.length) {
|
|
67
|
+
output.errors.forEach(errorMsg => log.error(errorMsg));
|
|
68
|
+
errorFound = true;
|
|
69
|
+
if(validation.skipFurtherValidation) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
39
73
|
}
|
|
40
74
|
}
|
|
41
|
-
// Once all the fails were checked raise an exception if there were errors
|
|
42
|
-
let errors = [];
|
|
43
|
-
if (!validateFormsPassed) {
|
|
44
|
-
errors.push('One or more forms appears to have errors found by the API validation endpoint.');
|
|
45
|
-
}
|
|
46
|
-
if (!idValidationsPassed) {
|
|
47
|
-
errors.push('One or more forms appears to be missing <meta><instanceID/></meta> node.');
|
|
48
|
-
}
|
|
49
|
-
if (errors.length) {
|
|
50
|
-
// the blank spaces are a trick to align the errors in the log ;)
|
|
51
|
-
throw new Error(errors.join('\n '));
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if
|
|
59
|
-
|
|
60
|
-
'no form will be checked before push');
|
|
61
|
-
validateEndpointNotFoundLogged = true; // Just log the message once
|
|
76
|
+
if(validationSkipped) {
|
|
77
|
+
log.warn('Some validations have been skipped because they require a CHT instance.');
|
|
78
|
+
}
|
|
79
|
+
if(errorFound) {
|
|
80
|
+
throw new Error('One or more forms have failed validation.');
|
|
62
81
|
}
|
|
63
|
-
return resp;
|
|
64
82
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Form validations
|
|
2
|
+
|
|
3
|
+
Form validations should be added to this directory.
|
|
4
|
+
|
|
5
|
+
Their module should look something like this:
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
module.exports = {
|
|
9
|
+
requiresInstance: false,
|
|
10
|
+
skipFurtherValidation: true,
|
|
11
|
+
execute: async ({ xformPath, xmlStr }) => {
|
|
12
|
+
...
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Module Exports
|
|
18
|
+
|
|
19
|
+
| Field | Required | Notes |
|
|
20
|
+
|-------------------------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
21
|
+
| `requiresInstance` | Optional, defaults to `true` | The validation needs the user to have provided a instance location, e.g. via `--local` or `--instance` |
|
|
22
|
+
| `skipFurtherValidation` | Optional, defaults to `false` | When `true`, additional validations will not be performed on a form that fails the current validation. The goal is to avoid unnecessary noise in the validation log for forms that are grossly invalid (e.g. XML files that are not actually xForms at all). |
|
|
23
|
+
| `execute` | Required | The function that is run when the validation is executed. The provided input argument contains the form XML string. |
|
|
24
|
+
|
|
25
|
+
## Result
|
|
26
|
+
|
|
27
|
+
The result has the following format:
|
|
28
|
+
|
|
29
|
+
| Field | Notes |
|
|
30
|
+
|----------|----------------------------------------------------------------------------------------------------|
|
|
31
|
+
| warnings | Array containing the descriptions of any warnings. |
|
|
32
|
+
| errors | Array containing the descriptions of any errors. If this is empty, the form has passed validation. |
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const api = require('../../api');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
execute: async ({ xformPath, xmlStr }) => {
|
|
5
|
+
const warnings = [];
|
|
6
|
+
const errors = [];
|
|
7
|
+
try {
|
|
8
|
+
const resp = await api().formsValidate(xmlStr);
|
|
9
|
+
if (resp.formsValidateEndpointFound === false) {
|
|
10
|
+
warnings.push('Form validation endpoint not found in your version of CHT Core, no form will be checked before push');
|
|
11
|
+
}
|
|
12
|
+
} catch (err) {
|
|
13
|
+
errors.push(`Error found while validating "${xformPath}". Validation response: ${err.message}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return { errors, warnings };
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const { formHasInstanceId } = require('../../forms-utils');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
requiresInstance: false,
|
|
5
|
+
skipFurtherValidation: true,
|
|
6
|
+
execute: async({ xformPath, xmlStr }) => {
|
|
7
|
+
const errors = [];
|
|
8
|
+
if(!formHasInstanceId(xmlStr)) {
|
|
9
|
+
errors.push(`Form at ${xformPath} appears to be missing <meta><instanceID/></meta> node. This form will not work on CHT webapp.`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return { errors, warnings: [] };
|
|
13
|
+
}
|
|
14
|
+
};
|