cht-conf 3.14.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,10 @@
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
+
1
8
  # [3.14.0](https://github.com/medic/cht-conf/compare/v3.13.0...v3.14.0) (2022-08-18)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cht-conf",
3
- "version": "3.14.0",
3
+ "version": "3.15.0",
4
4
  "description": "Configure CHT deployments",
5
5
  "main": "./src/lib/main.js",
6
6
  "engines": {
@@ -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,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,5 +1,6 @@
1
1
  const { DOMParser } = require('@xmldom/xmldom');
2
2
  const argsFormFilter = require('./args-form-filter');
3
+ const { getValidApiVersion } = require('./get-api-version');
3
4
  const environment = require('./environment');
4
5
  const fs = require('./sync-fs');
5
6
  const log = require('./log');
@@ -43,6 +44,7 @@ module.exports = async (projectDir, subDirectory, options={}) => {
43
44
  }
44
45
 
45
46
  const instanceProvided = environment.apiUrl;
47
+ const apiVersion = instanceProvided ? await getValidApiVersion() : null;
46
48
  let validationSkipped = false;
47
49
 
48
50
  const fileNames = argsFormFilter(formsDir, '.xml', options);
@@ -54,7 +56,12 @@ module.exports = async (projectDir, subDirectory, options={}) => {
54
56
  const { xformPath } = getFormFilePaths(formsDir, fileName);
55
57
  const xml = fs.read(xformPath);
56
58
 
57
- const valParams = { xformPath, xmlStr: xml, xmlDoc: domParser.parseFromString(xml) };
59
+ const valParams = {
60
+ xformPath,
61
+ xmlStr: xml,
62
+ xmlDoc: domParser.parseFromString(xml),
63
+ apiVersion,
64
+ };
58
65
  for(const validation of validations) {
59
66
  if(validation.requiresInstance && !instanceProvided) {
60
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,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
+ };