cht-conf 3.12.0 → 3.15.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,24 @@
1
+ # [3.15.0](https://github.com/medic/cht-conf/compare/v3.14.0...v3.15.0) (2022-08-19)
2
+
3
+
4
+ ### Features
5
+
6
+ * **#500:** Validate deprecated appearances in forms ([#501](https://github.com/medic/cht-conf/issues/501)) ([1e52c0c](https://github.com/medic/cht-conf/commit/1e52c0c769fbd27ee6a764358ef2a0463ae3023f)), closes [#500](https://github.com/medic/cht-conf/issues/500)
7
+
8
+ # [3.14.0](https://github.com/medic/cht-conf/compare/v3.13.0...v3.14.0) (2022-08-18)
9
+
10
+
11
+ ### Features
12
+
13
+ * **#494:** validate notes are not required in forms ([#496](https://github.com/medic/cht-conf/issues/496)) ([a5a8e09](https://github.com/medic/cht-conf/commit/a5a8e096a478373a43e63f0f606a2919bade8982)), closes [#494](https://github.com/medic/cht-conf/issues/494)
14
+
15
+ # [3.13.0](https://github.com/medic/cht-conf/compare/v3.12.0...v3.13.0) (2022-07-12)
16
+
17
+
18
+ ### Features
19
+
20
+ * **#479:** validate simple XPaths in forms ([#486](https://github.com/medic/cht-conf/issues/486)) ([9df4a63](https://github.com/medic/cht-conf/commit/9df4a630e4bf69d6f6d102c6ae538f9e0bf10282)), closes [#479](https://github.com/medic/cht-conf/issues/479) [#479](https://github.com/medic/cht-conf/issues/479)
21
+
1
22
  # [3.12.0](https://github.com/medic/cht-conf/compare/v3.11.1...v3.12.0) (2022-06-23)
2
23
 
3
24
 
package/README.md CHANGED
@@ -300,7 +300,6 @@ Execute `npm test` to run static analysis checks and the test suite. Requires Do
300
300
  1. Get the pull request reviewed and approved.
301
301
  1. When doing the squash and merge, make sure that your commit message is clear and readable and follows the strict format described in the commit format section below. If the commit message does not comply, automatic release will fail.
302
302
  1. In case you are planning to merge the pull request with a merge commit, make sure that every commit in your branch respects the format.
303
- 1. Announce the release on the [CHT forum](https://forum.communityhealthtoolkit.org), under the "Product - Releases" category.
304
303
 
305
304
  ### Commit format
306
305
  The commit format should follow this [conventional-changelog angular preset](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). Examples are provided below.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cht-conf",
3
- "version": "3.12.0",
3
+ "version": "3.15.0",
4
4
  "description": "Configure CHT deployments",
5
5
  "main": "./src/lib/main.js",
6
6
  "engines": {
@@ -11,7 +11,7 @@
11
11
  "eslint": "eslint 'src/**/*.js' test/*.js 'test/**/*.js'",
12
12
  "docker-start-couchdb": "npm run docker-stop-couchdb && docker run -d -p 6984:5984 --rm --name cht-conf-couchdb couchdb:2.3.1 && sh test/scripts/wait_for_response_code.sh 6984 200 CouchDB",
13
13
  "docker-stop-couchdb": "docker stop cht-conf-couchdb || true",
14
- "test": "npm run eslint && npm run docker-start-couchdb && npm run clean && mkdir -p build/test && cp -r test/data build/test/data && cd build/test && nyc --reporter=html mocha --forbid-only ../../test/**/*.spec.js && cd ../.. && npm run docker-stop-couchdb",
14
+ "test": "npm run eslint && npm run docker-start-couchdb && npm run clean && mkdir -p build/test && cp -r test/data build/test/data && cd build/test && nyc --reporter=html mocha --forbid-only \"../../test/**/*.spec.js\" && cd ../.. && npm run docker-stop-couchdb",
15
15
  "semantic-release": "semantic-release"
16
16
  },
17
17
  "bin": {
@@ -38,6 +38,7 @@
38
38
  "@hapi/joi": "^16.1.8",
39
39
  "@medic/translation-checker": "^1.0.1",
40
40
  "@parcel/watcher": "^2.0.5",
41
+ "@xmldom/xmldom": "^0.8.2",
41
42
  "canonical-json": "0.0.4",
42
43
  "csv-parse": "^4.16.0",
43
44
  "dom-compare": "^0.6.0",
@@ -67,7 +68,7 @@
67
68
  "terser-webpack-plugin": "^1.4.3",
68
69
  "uuid": "^8.3.2",
69
70
  "webpack": "^4.46.0",
70
- "xmldom": "^0.6.0"
71
+ "xpath": "0.0.32"
71
72
  },
72
73
  "devDependencies": {
73
74
  "@commitlint/cli": "^13.2.0",
@@ -5,10 +5,15 @@ const fs = require('../lib/sync-fs');
5
5
  const { info } = require('../lib/log');
6
6
 
7
7
  const LAYOUT = {
8
- 'contact-summary.js': '',
8
+ 'contact-summary.templated.js': `module.exports = {
9
+ fields: [],
10
+ cards: [],
11
+ context: {}
12
+ };`,
9
13
  'privacy-policies.json': {},
10
14
  'privacy-policies': {},
11
15
  'resources.json': {},
16
+ 'harness.defaults.json': {},
12
17
  resources: {},
13
18
  'targets.js': 'module.exports = [];',
14
19
  'tasks.js': 'module.exports = [];',
@@ -32,6 +37,12 @@ const LAYOUT = {
32
37
  'forms.json': {},
33
38
  'schedules.json': [],
34
39
  },
40
+ test: {
41
+ forms: {},
42
+ 'contact-summary': {},
43
+ tasks: {},
44
+ targets: {}
45
+ }
35
46
  };
36
47
 
37
48
  function createRecursively(dir, layout) {
@@ -2,7 +2,7 @@ const semver = require('semver');
2
2
 
3
3
  const environment = require('../lib/environment');
4
4
  const pouch = require('../lib/db');
5
- const getApiVersion = require('../lib/get-api-version');
5
+ const { getValidApiVersion } = require('../lib/get-api-version');
6
6
  const iso639 = require('iso-639-1');
7
7
  const log = require('../lib/log');
8
8
  const properties = require('properties');
@@ -168,9 +168,9 @@ function idFor(fileName) {
168
168
  }
169
169
 
170
170
  async function genericTranslationsStructure(db) {
171
- const version = await getApiVersion();
171
+ const version = await getValidApiVersion();
172
172
 
173
- if (semver.valid(version)) {
173
+ if (version) {
174
174
  return semver.gte(version, '3.4.0');
175
175
  }
176
176
 
@@ -1,5 +1,14 @@
1
+ const xpath = require('xpath');
1
2
  const fs = require('./sync-fs');
2
3
 
4
+ const XPATH_MODEL = '/h:html/h:head/model';
5
+
6
+ const getNode = (currentNode, path) =>
7
+ xpath.parse(path).select1({ node: currentNode, allowAnyNamespaceForNoPrefix: true });
8
+
9
+ const getNodes = (currentNode, path) =>
10
+ xpath.parse(path).select({ node: currentNode, allowAnyNamespaceForNoPrefix: true });
11
+
3
12
  module.exports = {
4
13
  /**
5
14
  * Get the full path of the form, or null if the path doesn't exist.
@@ -29,24 +38,53 @@ module.exports = {
29
38
  };
30
39
  },
31
40
 
32
- // This isn't really how to parse XML, but we have fairly good control over the
33
- // input and this code is working so far. This may break with changes to the
34
- // formatting of output from xls2xform.
41
+ /**
42
+ * Returns the node from the form XML specified by the given XPath.
43
+ * @param {Element} currentNode the current node in the form XML document
44
+ * @param {string} path the XPath expression
45
+ * @returns {Element} the selected node or `undefined` if not found
46
+ */
47
+ getNode,
48
+
49
+ /**
50
+ * Returns the nodes from the form XML specified by the given XPath.
51
+ * @param {Element} currentNode the current node in the form XML document
52
+ * @param {string} path the XPath expression
53
+ * @returns {Element} the selected nodes or an empty array if none are found
54
+ */
55
+ getNodes,
56
+
57
+ /**
58
+ * Returns the `bind` nodes for the given form XML.
59
+ * @param {Document} xmlDoc the form XML document
60
+ * @returns {Element}
61
+ */
62
+ getBindNodes: xmlDoc => getNodes(xmlDoc, `${XPATH_MODEL}/bind`),
63
+
64
+ /**
65
+ * Returns the primary (first) `instance` node for the given form XML.
66
+ * @param {Document} xmlDoc the form XML document
67
+ * @returns {Element}
68
+ */
69
+ getPrimaryInstanceNode: xmlDoc => getNode(xmlDoc, `${XPATH_MODEL}/instance`),
35
70
 
36
71
  /**
37
72
  * Check whether the XForm has the <instanceID/> tag.
38
- * @param {string} xml the XML string
73
+ * @param {string} xmlDoc the XML document
39
74
  * @returns {boolean}
40
75
  */
41
- formHasInstanceId: xml => xml.includes('<instanceID/>'),
76
+ formHasInstanceId: xmlDoc => getNode(xmlDoc, `//meta/instanceID`) !== undefined,
42
77
 
78
+ // This isn't really how to parse XML, but we have fairly good control over the
79
+ // input and this code is working so far. This may break with changes to the
80
+ // formatting of output from xls2xform.
43
81
  /**
44
82
  * Get the title string inside the <h:title> tag
45
83
  * @param {string} xml the XML string
46
84
  * @returns {string}
47
85
  */
48
86
  readTitleFrom: xml =>
49
- xml.substring(xml.indexOf('<h:title>') + 9, xml.indexOf('</h:title>')),
87
+ xml.substring(xml.indexOf('<h:title>') + 9, xml.indexOf('</h:title>')),
50
88
 
51
89
  /**
52
90
  * Get the ID of the form
@@ -54,7 +92,7 @@ module.exports = {
54
92
  * @returns {string}
55
93
  */
56
94
  readIdFrom: xml =>
57
- xml.match(/<model>[^]*<\/model>/)[0]
58
- .match(/<instance>[^]*<\/instance>/)[0]
59
- .match(/id="([^"]*)"/)[1],
95
+ xml.match(/<model>[^]*<\/model>/)[0]
96
+ .match(/<instance>[^]*<\/instance>/)[0]
97
+ .match(/id="([^"]*)"/)[1],
60
98
  };
@@ -1,3 +1,5 @@
1
+ const semver = require('semver');
2
+
1
3
  const api = require('./api');
2
4
  const db = require('./db');
3
5
 
@@ -15,4 +17,8 @@ const getApiVersion = async () => {
15
17
  return version;
16
18
  };
17
19
 
18
- module.exports = getApiVersion;
20
+ const getValidApiVersion = async () => {
21
+ return semver.valid(await getApiVersion());
22
+ };
23
+
24
+ module.exports = { getValidApiVersion };
@@ -1,4 +1,6 @@
1
+ const { DOMParser } = require('@xmldom/xmldom');
1
2
  const argsFormFilter = require('./args-form-filter');
3
+ const { getValidApiVersion } = require('./get-api-version');
2
4
  const environment = require('./environment');
3
5
  const fs = require('./sync-fs');
4
6
  const log = require('./log');
@@ -7,6 +9,7 @@ const {
7
9
  getFormFilePaths
8
10
  } = require('./forms-utils');
9
11
 
12
+ const domParser = new DOMParser();
10
13
  const VALIDATIONS_PATH = fs.path.resolve(__dirname, './validation/form');
11
14
  const validations = fs.readdir(VALIDATIONS_PATH)
12
15
  .filter(name => name.endsWith('.js'))
@@ -41,6 +44,7 @@ module.exports = async (projectDir, subDirectory, options={}) => {
41
44
  }
42
45
 
43
46
  const instanceProvided = environment.apiUrl;
47
+ const apiVersion = instanceProvided ? await getValidApiVersion() : null;
44
48
  let validationSkipped = false;
45
49
 
46
50
  const fileNames = argsFormFilter(formsDir, '.xml', options);
@@ -52,7 +56,12 @@ module.exports = async (projectDir, subDirectory, options={}) => {
52
56
  const { xformPath } = getFormFilePaths(formsDir, fileName);
53
57
  const xml = fs.read(xformPath);
54
58
 
55
- const valParams = { xformPath, xmlStr: xml };
59
+ const valParams = {
60
+ xformPath,
61
+ xmlStr: xml,
62
+ xmlDoc: domParser.parseFromString(xml),
63
+ apiVersion,
64
+ };
56
65
  for(const validation of validations) {
57
66
  if(validation.requiresInstance && !instanceProvided) {
58
67
  validationSkipped = true;
@@ -8,7 +8,7 @@ Their module should look something like this:
8
8
  module.exports = {
9
9
  requiresInstance: false,
10
10
  skipFurtherValidation: true,
11
- execute: async ({ xformPath, xmlStr }) => {
11
+ execute: async ({ xformPath, xmlStr, xmlDoc, apiVersion }) => {
12
12
  ...
13
13
  }
14
14
  };
@@ -16,11 +16,11 @@ module.exports = {
16
16
 
17
17
  ## Module Exports
18
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. |
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(opts)` | Required | The function that is run when the validation is executed. The available ops are:<br>* `xformPath` - the file path to the form<br>* `xmlStr` - string value of the form XML<br>* `xmlDoc` - Document value of the form XML<br>* `apiVersion` - the version of the CHT instance (if connection information was provided) or `null` |
24
24
 
25
25
  ## Result
26
26
 
@@ -0,0 +1,121 @@
1
+ /*
2
+ An XPath expression that evaluates to an empty result (e.g. no node is found in the document that matches the
3
+ expression) is technically "valid" (in the sense that it is a properly formatted XPath expression).
4
+
5
+ However, if there is a "simple" XPath expression (one that just references a node via an absolute/relative path and does
6
+ not contain any dynamic processing) in a form that does not evaluate to an actual node in the form, that is almost
7
+ certainly an error (since it could never return any other result). This validation checks the simple XPaths in a form's
8
+ calculate, constraint, readonly, relevant, and required expressions and returns an error for any simple XPaths that
9
+ point to a non-existant node. More complex dynamic XPath expressions are not validated (since their results could vary
10
+ at runtime depending on the data in the form).
11
+
12
+ https://github.com/medic/cht-conf/issues/479
13
+ */
14
+
15
+ const { getNode, getBindNodes, getPrimaryInstanceNode } = require('../../forms-utils');
16
+
17
+ // Characters used in simple XPaths
18
+ const SUPPORTED_CHAR = '[/\\w.-]';
19
+ // Special characters used in complex XPaths
20
+ const UNSUPPORTED_CHAR = '[*@:[\\]]';
21
+ const XPATH_CHAR = `(${SUPPORTED_CHAR}|${UNSUPPORTED_CHAR})`;
22
+ // Some XPaths lookup nodes in separate instances (e.g. instance('contact-summary')/context/pregnancy_uuid)
23
+ const INSTANCE = `instance\\([\\w-'"]+\\)`;
24
+ // Look ahead and make sure there are an even number of quotes following the match (this means that the match itself is not in quotes).
25
+ const LOOK_AHEADS_FOR_EVEN_QUOTES = `(?=([^"]*"[^"]*")*[^"]*$)(?=([^']*'[^']*')*[^']*$)`;
26
+ // Matches on all possible XPaths (simple and complex) not in quotes. May start with an instance reference. Must include a slash.
27
+ const XPATH_PATTERN = new RegExp(`(${INSTANCE}|)${XPATH_CHAR}*\\/${XPATH_CHAR}+${LOOK_AHEADS_FOR_EVEN_QUOTES}`, 'g');
28
+ // Matches on XPaths containing complex calculations (note that '//' indicates a deep lookup, which we do not validate)
29
+ const UNSUPPORTED_XPATH_PATTERN = new RegExp(`\\/\\/|${UNSUPPORTED_CHAR}|${INSTANCE}`, 'g');
30
+
31
+ const extractSimpleXpaths = (expression) => {
32
+ /*
33
+ "Simple" XPaths look a lot like subsections of complex XPaths. To ensure that we never accidentally extract part of a
34
+ complex XPath, we first just match on all possible XPaths (simple and complex). Then we do a second pass to filter the
35
+ list down to just the XPaths that have a supported beginning section (since that is a cheep check). Finally, we filter
36
+ out any complex XPaths (with unsupported characters).
37
+ */
38
+ return (expression.match(XPATH_PATTERN) || [])
39
+ .filter(xpath => xpath.startsWith('/') || xpath.startsWith('./') || xpath.startsWith('../'))
40
+ .filter(xpath => !xpath.match(UNSUPPORTED_XPATH_PATTERN));
41
+ };
42
+
43
+ const isValidXpath = (nodeset, xpathToValidate, instance) => {
44
+ if(xpathToValidate.startsWith('/')) {
45
+ // Absolute XPath. Evaluate it relative to the instance
46
+ return getNode(instance, xpathToValidate.substring(1));
47
+ }
48
+ // Relative XPath. Evaluate it relative to the current node
49
+ const currentNode = getNode(instance, nodeset.substring(1));
50
+ if(!currentNode) {
51
+ throw new Error(`Could not find model node referenced by bind nodeset: ${nodeset}`);
52
+ }
53
+ return getNode(currentNode, xpathToValidate);
54
+ };
55
+
56
+ const extractInvalidXPaths = (nodeset, expression, instance) => {
57
+ return extractSimpleXpaths(expression)
58
+ .filter(xpath => !isValidXpath(nodeset, xpath, instance));
59
+ };
60
+
61
+ const getField = (bind, instance) => {
62
+ const nodeset = bind.getAttribute('nodeset');
63
+ const getInvalidXPaths = (expression) => extractInvalidXPaths(nodeset, expression, instance);
64
+ return {
65
+ nodeset,
66
+ calculate: getInvalidXPaths(bind.getAttribute('calculate')),
67
+ constraint: getInvalidXPaths(bind.getAttribute('constraint')),
68
+ readonly: getInvalidXPaths(bind.getAttribute('readonly')),
69
+ relevant: getInvalidXPaths(bind.getAttribute('relevant')),
70
+ required: getInvalidXPaths(bind.getAttribute('required')),
71
+ };
72
+ };
73
+
74
+ const keepFieldsWithXPaths = (field) => {
75
+ return field.calculate.length ||
76
+ field.constraint.length ||
77
+ field.readonly.length ||
78
+ field.relevant.length ||
79
+ field.required.length;
80
+ };
81
+
82
+ const getFieldsWithInvalidXPaths = (xmlDoc) => {
83
+ const instance = getPrimaryInstanceNode(xmlDoc);
84
+ if(!instance) {
85
+ throw new Error('No instance found in form XML.');
86
+ }
87
+ return getBindNodes(xmlDoc)
88
+ .map(bind => getField(bind, instance))
89
+ .filter(field => keepFieldsWithXPaths(field));
90
+ };
91
+
92
+ module.exports = {
93
+ requiresInstance: false,
94
+ skipFurtherValidation: false,
95
+ execute: async({ xformPath, xmlDoc }) => {
96
+ const errors = [];
97
+ try {
98
+ const fields = getFieldsWithInvalidXPaths(xmlDoc);
99
+ if(fields.length) {
100
+ errors.push(`Form at ${xformPath} contains invalid XPath expressions (absolute or relative paths that refer to a non-existant node):`);
101
+
102
+ fields.forEach(field => {
103
+ const recordError = (expressionName) => {
104
+ const xpaths = field[expressionName];
105
+ if(xpaths.length) {
106
+ errors.push(` - ${expressionName} for ${field.nodeset} contains [${xpaths.join(', ')}]`);
107
+ }
108
+ };
109
+ recordError('calculate');
110
+ recordError('constraint');
111
+ recordError('readonly');
112
+ recordError('relevant');
113
+ recordError('required');
114
+ });
115
+ }
116
+ } catch(e) {
117
+ errors.push(`Error encountered while validating XPaths in form at ${xformPath}: ${e.message}`);
118
+ }
119
+ return { errors, warnings: [] };
120
+ }
121
+ };
@@ -0,0 +1,87 @@
1
+ const semver = require('semver');
2
+ const { getNodes } = require('../../forms-utils');
3
+
4
+ // Based on https://forum.getodk.org/t/spec-proposal-harmonize-compact-compact-n-horizontal-horizontal-compact-appearances/15565/32
5
+ // Note the order of evaluation is important
6
+ const getDeprecatedAppearances = (apiVersion) => [
7
+ {
8
+ match: appearance => appearance.match(/(?:^|\s)horizontal-compact(?:$|\s)/),
9
+ getReplacement: () => 'columns-pack',
10
+ versionDeprecated: '4.0.0'
11
+ },
12
+ {
13
+ match: appearance => appearance.match(/(?:^|\s)horizontal(?:$|\s)/),
14
+ getReplacement: () => 'columns',
15
+ versionDeprecated: '4.0.0'
16
+ },
17
+ {
18
+ match: appearance => appearance.match(/(?:^|\s)compact-(\d{1,2})(?:$|\s)/),
19
+ getReplacement: (match) => `columns-${match[1].trim()} no-buttons`,
20
+ versionDeprecated: '4.0.0'
21
+ },
22
+ {
23
+ match: appearance => appearance.match(/(?:^|\s)compact(?:$|\s)/),
24
+ getReplacement: () => 'columns-pack no-buttons',
25
+ versionDeprecated: '4.0.0'
26
+ },
27
+ ].filter(deprecatedAppearance => semver.gte(apiVersion, deprecatedAppearance.versionDeprecated));
28
+
29
+ const getElementsWithAppearance = (xmlDoc) => getNodes(xmlDoc, '/h:html/h:body//*[@appearance]');
30
+
31
+ const unpackDeprecationAttributes = element => ({
32
+ ref: element.getAttribute('ref'),
33
+ appearance: element.getAttribute('appearance'),
34
+ });
35
+
36
+ const hasAppearance = ({ appearance }) => appearance;
37
+
38
+ const populateDeprecatedAppearance = deprecatedAppearances => ({ ref, appearance }) => ({
39
+ ref,
40
+ appearance,
41
+ // Only warning for the first deprecated appearance found for an element. This seems acceptable.
42
+ deprecatedAppearance: deprecatedAppearances.find(depAppearance => depAppearance.match(appearance))
43
+ });
44
+
45
+ const hasDeprecatedAppearance = ({ deprecatedAppearance }) => deprecatedAppearance;
46
+
47
+ const createWarning = ({ ref, appearance, deprecatedAppearance }) => {
48
+ const match = deprecatedAppearance.match(appearance);
49
+ const originalAppearance = match[0].trim();
50
+ const newAppearance = deprecatedAppearance.getReplacement(match);
51
+ return ` - ${ref}: replace [${originalAppearance}] with [${newAppearance}]`;
52
+ };
53
+
54
+ const populateWarningHeader = (xformPath, warnings) => {
55
+ if (warnings.length) {
56
+ warnings.unshift(`Form at ${xformPath} contains fields with the deprecated \`horizontal\`/\`compact\` appearance. ` +
57
+ 'These have been deprecated in favor of the `columns` appearance. Please update the following fields:');
58
+ }
59
+ };
60
+
61
+ const validateDeprecatedAppearances = async({ xformPath, xmlDoc, apiVersion }) => {
62
+ if (!apiVersion) {
63
+ return { warnings: [], errors: [], };
64
+ }
65
+
66
+ const deprecatedAppearances = getDeprecatedAppearances(apiVersion);
67
+ if (!deprecatedAppearances.length) {
68
+ return { warnings: [], errors: [], };
69
+ }
70
+
71
+ const warnings = getElementsWithAppearance(xmlDoc)
72
+ .map(unpackDeprecationAttributes)
73
+ .filter(hasAppearance)
74
+ .map(populateDeprecatedAppearance(deprecatedAppearances))
75
+ .filter(hasDeprecatedAppearance)
76
+ .map(createWarning);
77
+
78
+ populateWarningHeader(xformPath, warnings);
79
+
80
+ return { errors: [], warnings };
81
+ };
82
+
83
+ module.exports = {
84
+ requiresInstance: true,
85
+ skipFurtherValidation: false,
86
+ execute: validateDeprecatedAppearances
87
+ };
@@ -3,9 +3,9 @@ const { formHasInstanceId } = require('../../forms-utils');
3
3
  module.exports = {
4
4
  requiresInstance: false,
5
5
  skipFurtherValidation: true,
6
- execute: async({ xformPath, xmlStr }) => {
6
+ execute: async({ xformPath, xmlDoc }) => {
7
7
  const errors = [];
8
- if(!formHasInstanceId(xmlStr)) {
8
+ if(!formHasInstanceId(xmlDoc)) {
9
9
  errors.push(`Form at ${xformPath} appears to be missing <meta><instanceID/></meta> node. This form will not work on CHT webapp.`);
10
10
  }
11
11
 
@@ -0,0 +1,23 @@
1
+ const { getBindNodes } = require('../../forms-utils');
2
+
3
+ module.exports = {
4
+ requiresInstance: false,
5
+ skipFurtherValidation: false,
6
+ execute: async({ xformPath, xmlDoc }) => {
7
+ const errors = [];
8
+
9
+ const requiredNotes = getBindNodes(xmlDoc)
10
+ .filter(bind => bind.getAttribute('readonly') === 'true()') // Notes will not be conditionally readonly
11
+ .filter(bind => {
12
+ const required = bind.getAttribute('required');
13
+ return required && required !== 'false()';
14
+ })
15
+ .filter(bind => bind.getAttribute('type') === 'string')
16
+ .filter(bind => !bind.getAttribute('calculate'))
17
+ .map(bind => bind.getAttribute('nodeset'));
18
+ if(requiredNotes.length) {
19
+ errors.push(`Form at ${xformPath} contains the following note fields with 'required' expressions: [${requiredNotes.join(', ')}]`);
20
+ }
21
+ return { errors, warnings: [] };
22
+ }
23
+ };
@@ -8,7 +8,7 @@ const path = require('path');
8
8
  const log = require('./log');
9
9
  const environment = require('./environment');
10
10
  const { compare, GroupingReporter } = require('dom-compare');
11
- const DOMParser = require('xmldom').DOMParser;
11
+ const DOMParser = require('@xmldom/xmldom').DOMParser;
12
12
 
13
13
  const question = 'You are trying to modify a configuration that has been modified since your last upload. Do you want to?';
14
14
  const responseChoicesWithoutDiff = [