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
package/.editorconfig
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# top-most EditorConfig file
|
|
2
|
+
root = true
|
|
3
|
+
|
|
4
|
+
# Unix-style newlines with a newline ending every file
|
|
5
|
+
[*]
|
|
6
|
+
end_of_line = lf
|
|
7
|
+
insert_final_newline = true
|
|
8
|
+
trim_trailing_whitespace = true
|
|
9
|
+
charset = utf-8
|
|
10
|
+
indent_style = space
|
|
11
|
+
indent_size = 2
|
|
12
|
+
|
|
13
|
+
# Markdown files
|
|
14
|
+
[*.md]
|
|
15
|
+
trim_trailing_whitespace = false
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [4.6.0](https://github.com/medic/cht-conf/compare/v4.5.0...v4.6.0) (2025-04-23)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **#502:** warn when using deprecated `db-object` appearance ([#682](https://github.com/medic/cht-conf/issues/682)) ([ca6691b](https://github.com/medic/cht-conf/commit/ca6691b1f3285d1a59093fc3a6bb2c980dcb78e0)), closes [#502](https://github.com/medic/cht-conf/issues/502)
|
|
7
|
+
|
|
8
|
+
# [4.5.0](https://github.com/medic/cht-conf/compare/v4.4.1...v4.5.0) (2025-04-15)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **#675:** support `duplicate_check` contact form property ([#677](https://github.com/medic/cht-conf/issues/677)) ([503bb7d](https://github.com/medic/cht-conf/commit/503bb7d94dc9ee87078f183e43e5d1141123a05c)), closes [#675](https://github.com/medic/cht-conf/issues/675)
|
|
14
|
+
|
|
1
15
|
## [4.4.1](https://github.com/medic/cht-conf/compare/v4.4.0...v4.4.1) (2025-02-03)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -112,6 +112,14 @@ CHT Conf supports authentication using a session token by adding `--session-toke
|
|
|
112
112
|
|
|
113
113
|
cht --url=https://example.com:12345 --session-token=*my_token*
|
|
114
114
|
|
|
115
|
+
The `my_token` can be obtained by doing a POST request to `/_session` [endpoint](https://docs.couchdb.org/en/stable/api/server/authn.html#cookie-authentication) with `name` and `password` as form parameters.
|
|
116
|
+
|
|
117
|
+
For example, if your CHT instance is `my.cht.com`, you could use this `curl` call to specify your user `medic` and your password `secret123` to retrieve the header with the `AuthSession` value which is the token:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
curl -v -H 'Content-Type: application/json' -d '{"name":"medic","password":"secret123"}' https://my.cht.com/_session 2>&1 | grep AuthSession
|
|
121
|
+
< set-cookie: AuthSession=bWVkaWM6NjdBRTM4MkE6EguRnzpSiK0t8wFaOQ_jgkZE8UWcgNWgpyStzbbHreI; Version=1; Expires=Fri, 13-Feb-2026 18:21:30 GMT; Max-Age=31536000; Path=/; HttpOnly
|
|
122
|
+
```
|
|
115
123
|
### Into an archive to be uploaded later
|
|
116
124
|
|
|
117
125
|
cht --archive
|
|
@@ -357,7 +365,7 @@ Builds brought to you courtesy of GitHub actions.
|
|
|
357
365
|
|
|
358
366
|
# Copyright
|
|
359
367
|
|
|
360
|
-
Copyright 2013-
|
|
368
|
+
Copyright 2013-2025 Medic Mobile, Inc. <hello@medic.org>
|
|
361
369
|
|
|
362
370
|
# License
|
|
363
371
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cht-conf",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "Configure CHT deployments",
|
|
5
5
|
"main": "./src/lib/main.js",
|
|
6
6
|
"engines": {
|
|
@@ -35,20 +35,20 @@
|
|
|
35
35
|
"homepage": "https://github.com/medic/cht-conf#readme",
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@medic/translation-checker": "^1.1.0",
|
|
38
|
-
"@parcel/watcher": "^2.1
|
|
38
|
+
"@parcel/watcher": "^2.5.1",
|
|
39
39
|
"@xmldom/xmldom": "^0.8.10",
|
|
40
40
|
"async-retry": "^1.3.3",
|
|
41
41
|
"canonical-json": "0.0.4",
|
|
42
|
-
"csv-parse": "^5.
|
|
42
|
+
"csv-parse": "^5.6.0",
|
|
43
43
|
"dom-compare": "^0.6.0",
|
|
44
|
-
"eslint": "^8.57.
|
|
45
|
-
"eslint-webpack-plugin": "^2.
|
|
46
|
-
"googleapis": "^
|
|
47
|
-
"iso-639-1": "^3.1.
|
|
44
|
+
"eslint": "^8.57.1",
|
|
45
|
+
"eslint-webpack-plugin": "^4.2.0",
|
|
46
|
+
"googleapis": "^144.0.0",
|
|
47
|
+
"iso-639-1": "^3.1.5",
|
|
48
48
|
"joi": "^17.13.3",
|
|
49
49
|
"json-diff": "^1.0.6",
|
|
50
50
|
"json-stringify-safe": "^5.0.1",
|
|
51
|
-
"json2csv": "^
|
|
51
|
+
"json2csv": "^5.0.7",
|
|
52
52
|
"mime-types": "^2.1.35",
|
|
53
53
|
"minimist": "^1.2.8",
|
|
54
54
|
"mkdirp": "^3.0.1",
|
|
@@ -64,34 +64,35 @@
|
|
|
64
64
|
"redact-basic-auth": "^1.0.1",
|
|
65
65
|
"request": "^2.88.2",
|
|
66
66
|
"request-promise-native": "^1.0.9",
|
|
67
|
-
"semver": "^7.
|
|
67
|
+
"semver": "^7.7.1",
|
|
68
68
|
"svgo": "^3.3.2",
|
|
69
|
-
"terser-webpack-plugin": "^5.3.
|
|
70
|
-
"uuid": "^
|
|
71
|
-
"webpack": "^5.
|
|
72
|
-
"xpath": "0.0.
|
|
69
|
+
"terser-webpack-plugin": "^5.3.11",
|
|
70
|
+
"uuid": "^11.1.0",
|
|
71
|
+
"webpack": "^5.98.0",
|
|
72
|
+
"xpath": "0.0.34"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@commitlint/cli": "^19.
|
|
76
|
-
"@commitlint/config-conventional": "^19.
|
|
75
|
+
"@commitlint/cli": "^19.7.1",
|
|
76
|
+
"@commitlint/config-conventional": "^19.7.1",
|
|
77
|
+
"@medic/eslint-config": "^1.1.0",
|
|
77
78
|
"@semantic-release/changelog": "^6.0.3",
|
|
78
79
|
"@semantic-release/git": "^10.0.1",
|
|
79
|
-
"body-parser": "^1.20.
|
|
80
|
-
"chai": "^4.
|
|
81
|
-
"chai-as-promised": "^7.1.
|
|
82
|
-
"chai-exclude": "^2.1.
|
|
80
|
+
"body-parser": "^1.20.3",
|
|
81
|
+
"chai": "^4.5.0",
|
|
82
|
+
"chai-as-promised": "^7.1.2",
|
|
83
|
+
"chai-exclude": "^2.1.1",
|
|
83
84
|
"chai-shallow-deep-equal": "^1.4.6",
|
|
84
85
|
"chai-xml": "^0.4.1",
|
|
85
|
-
"eslint-plugin-n": "^17.
|
|
86
|
-
"express": "^4.
|
|
86
|
+
"eslint-plugin-n": "^17.15.1",
|
|
87
|
+
"express": "^4.21.2",
|
|
87
88
|
"express-pouchdb": "^4.2.0",
|
|
88
|
-
"fs-extra": "11.
|
|
89
|
-
"husky": "^9.1.
|
|
90
|
-
"mocha": "^
|
|
91
|
-
"nyc": "^17.
|
|
89
|
+
"fs-extra": "11.3.0",
|
|
90
|
+
"husky": "^9.1.7",
|
|
91
|
+
"mocha": "^11.1.0",
|
|
92
|
+
"nyc": "^17.1.0",
|
|
92
93
|
"pouchdb-adapter-memory": "^7.2.2",
|
|
93
94
|
"rewire": "^7.0.0",
|
|
94
95
|
"semantic-release": "^22.0.0",
|
|
95
|
-
"sinon": "^
|
|
96
|
+
"sinon": "^19.0.2"
|
|
96
97
|
}
|
|
97
98
|
}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
/* eslint-disable no-console */
|
|
4
4
|
|
|
5
5
|
const options = [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
'--instance', '--local', '--url', '--user',
|
|
7
|
+
'--help', '--shell-completion', '--supported-actions',
|
|
8
|
+
'--version', '--accept-self-signed-certs', '--skip-dependency-check',
|
|
9
|
+
'--skip-git-check', '--skip-translation-check', '--force',
|
|
10
|
+
...require('../cli/supported-actions'),
|
|
11
11
|
];
|
|
12
12
|
|
|
13
13
|
console.log(...options);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
|
|
3
3
|
const actions = fs.readdirSync(`${__dirname}/../fn`)
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
.filter(name => name.endsWith('.js'))
|
|
5
|
+
.map(name => name.substring(0, name.length - 3));
|
|
6
6
|
|
|
7
7
|
module.exports = actions;
|
package/src/cli/usage.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const supportedActions = require('./supported-actions');
|
|
2
2
|
|
|
3
3
|
module.exports = () => {
|
|
4
|
+
/* eslint-disable max-len */
|
|
4
5
|
console.log(`
|
|
5
6
|
${bold('NAME')}
|
|
6
7
|
cht - Configure your CHT instances
|
|
@@ -69,6 +70,7 @@ ${bold('OPTIONS')}
|
|
|
69
70
|
--force
|
|
70
71
|
CAN BE DANGEROUS! Passes yes to all commands and any where that would prompt to overwrite changes will overwrite automatically.
|
|
71
72
|
`);
|
|
73
|
+
/* eslint-enable max-len */
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
const bold = text => `\x1b[1m${text}\x1b[0m`;
|
|
@@ -16,10 +16,14 @@ const JSON_FILE_MATCHER = /^(.+)(\.json)$/;
|
|
|
16
16
|
const configFileMatcher = (fileName) => {
|
|
17
17
|
const jsFileMatchResult = fileName.match(JS_FILE_MATCHER);
|
|
18
18
|
// the first element is always the whole matched string, then our first file name group
|
|
19
|
-
if (jsFileMatchResult)
|
|
19
|
+
if (jsFileMatchResult) {
|
|
20
|
+
return jsFileMatchResult[1];
|
|
21
|
+
}
|
|
20
22
|
|
|
21
23
|
const jsonFileMatchResult = fileName.match(JSON_FILE_MATCHER);
|
|
22
|
-
if (jsonFileMatchResult)
|
|
24
|
+
if (jsonFileMatchResult) {
|
|
25
|
+
return jsonFileMatchResult[1];
|
|
26
|
+
}
|
|
23
27
|
|
|
24
28
|
return null;
|
|
25
29
|
};
|
|
@@ -48,7 +52,9 @@ const compileAppSettingsForProject = async (projectDir, options) => {
|
|
|
48
52
|
const oldTaskSchedulesPath = path.join(projectDir, 'tasks.json');
|
|
49
53
|
if (fs.exists(oldTaskSchedulesPath)) {
|
|
50
54
|
if (fs.exists(taskSchedulesPath)) {
|
|
51
|
-
throw new Error(
|
|
55
|
+
throw new Error(
|
|
56
|
+
`You have both ${taskSchedulesPath} and ${oldTaskSchedulesPath}. Please remove one to continue!`
|
|
57
|
+
);
|
|
52
58
|
}
|
|
53
59
|
warn(`tasks.json file is deprecated. Please rename ${oldTaskSchedulesPath} to ${taskSchedulesPath}`);
|
|
54
60
|
taskSchedulesPath = oldTaskSchedulesPath;
|
|
@@ -66,7 +72,10 @@ const compileAppSettingsForProject = async (projectDir, options) => {
|
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
if (!fs.exists(baseSettingsPath) && !fs.exists(appSettingsPath)) {
|
|
69
|
-
throw new Error(
|
|
75
|
+
throw new Error(
|
|
76
|
+
'No configuration defined please create a base_settings.json file in app_settings folder ' +
|
|
77
|
+
'with the desired configuration'
|
|
78
|
+
);
|
|
70
79
|
}
|
|
71
80
|
if (fs.exists(baseSettingsPath)) {
|
|
72
81
|
// using modular config so should override anything already defined in app_settings.json
|
|
@@ -105,10 +114,13 @@ const compileAppSettingsForProject = async (projectDir, options) => {
|
|
|
105
114
|
appSettings.assetlinks = assetlinks;
|
|
106
115
|
}
|
|
107
116
|
} else {
|
|
108
|
-
warn(
|
|
109
|
-
|
|
117
|
+
warn(
|
|
118
|
+
`app_settings.json file should not be edited directly.
|
|
119
|
+
Please create a base_settings.json file in app_settings folder and move any manually defined configurations there.`
|
|
120
|
+
);
|
|
110
121
|
appSettings = fs.readJson(appSettingsPath);
|
|
111
122
|
}
|
|
123
|
+
|
|
112
124
|
appSettings.contact_summary = await compilation.compileContactSummary(projectDir, options);
|
|
113
125
|
const compiledTasksAndTargets = await compilation.compileTasksAndTargets(projectDir, options);
|
|
114
126
|
appSettings.tasks = {
|
|
@@ -122,13 +134,18 @@ const compileAppSettingsForProject = async (projectDir, options) => {
|
|
|
122
134
|
if (purgeConfig) {
|
|
123
135
|
appSettings.purge = purgeConfig;
|
|
124
136
|
}
|
|
125
|
-
|
|
137
|
+
else{
|
|
138
|
+
warn('Setting purge configuration to empty object as purge.js and purging.js files were not found.');
|
|
139
|
+
appSettings.purge = {};
|
|
140
|
+
}
|
|
126
141
|
return appSettings;
|
|
127
142
|
};
|
|
128
143
|
|
|
129
144
|
function applyTransforms(app_settings, inherited) {
|
|
130
145
|
function doDelete(target, rules) {
|
|
131
|
-
if (!Array.isArray(rules))
|
|
146
|
+
if (!Array.isArray(rules)) {
|
|
147
|
+
throw new Error('.delete should be an array');
|
|
148
|
+
}
|
|
132
149
|
|
|
133
150
|
rules.forEach(k => {
|
|
134
151
|
const parts = k.split('.');
|
|
@@ -142,7 +159,9 @@ function applyTransforms(app_settings, inherited) {
|
|
|
142
159
|
}
|
|
143
160
|
|
|
144
161
|
function doReplace(target, rules) {
|
|
145
|
-
if (typeof rules !== 'object')
|
|
162
|
+
if (typeof rules !== 'object') {
|
|
163
|
+
throw new Error('.replace should be an object');
|
|
164
|
+
}
|
|
146
165
|
|
|
147
166
|
Object.keys(rules)
|
|
148
167
|
.forEach(k => {
|
|
@@ -159,14 +178,22 @@ function applyTransforms(app_settings, inherited) {
|
|
|
159
178
|
function doMerge(target, source) {
|
|
160
179
|
Object.keys(target)
|
|
161
180
|
.forEach(k => {
|
|
162
|
-
if (Array.isArray(source[k]))
|
|
163
|
-
|
|
164
|
-
|
|
181
|
+
if (Array.isArray(source[k])) {
|
|
182
|
+
target[k] = target[k].concat(source[k]);
|
|
183
|
+
}
|
|
184
|
+
else if (typeof source[k] === 'object') {
|
|
185
|
+
doMerge(target[k], source[k]);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
source[k] = target[k];
|
|
189
|
+
}
|
|
165
190
|
});
|
|
166
191
|
}
|
|
167
192
|
|
|
168
193
|
function doFilter(target, rules) {
|
|
169
|
-
if (typeof rules !== 'object')
|
|
194
|
+
if (typeof rules !== 'object') {
|
|
195
|
+
throw new Error('.filter should be an object');
|
|
196
|
+
}
|
|
170
197
|
|
|
171
198
|
Object.keys(rules)
|
|
172
199
|
.forEach(k => {
|
|
@@ -177,11 +204,15 @@ function applyTransforms(app_settings, inherited) {
|
|
|
177
204
|
parts.shift();
|
|
178
205
|
}
|
|
179
206
|
|
|
180
|
-
if (!Array.isArray(rules[k]))
|
|
207
|
+
if (!Array.isArray(rules[k])) {
|
|
208
|
+
throw new Error('.filter values must be arrays!');
|
|
209
|
+
}
|
|
181
210
|
|
|
182
211
|
Object.keys(t[parts[0]])
|
|
183
212
|
.forEach(tK => {
|
|
184
|
-
if (!rules[k].includes(tK))
|
|
213
|
+
if (!rules[k].includes(tK)) {
|
|
214
|
+
delete t[parts[0]][tK];
|
|
215
|
+
}
|
|
185
216
|
});
|
|
186
217
|
});
|
|
187
218
|
}
|
package/src/fn/compress-pngs.js
CHANGED
|
@@ -12,12 +12,14 @@ module.exports = {
|
|
|
12
12
|
promiseChain
|
|
13
13
|
.then(() => info('Compressing PNG:', png, '…'))
|
|
14
14
|
.then(() =>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
exec(['pngout-medic', `'${png}'`])
|
|
16
|
+
.then(() => trace('Compressed', png))
|
|
17
|
+
.catch(e => {
|
|
18
|
+
if(e.status === 2) {
|
|
19
|
+
info('Unable to compress further.');
|
|
20
|
+
} else {
|
|
21
|
+
throw e;
|
|
22
|
+
}
|
|
23
|
+
})),
|
|
24
|
+
Promise.resolve())
|
|
23
25
|
};
|
package/src/fn/compress-svgs.js
CHANGED
|
@@ -15,6 +15,9 @@ module.exports = {
|
|
|
15
15
|
.then(() => svgo.optimize(fs.read(path), { path }))
|
|
16
16
|
.then(result => fs.write(path, result.data))
|
|
17
17
|
.then(() => trace('Compressed', path))
|
|
18
|
-
.then(() => warn(
|
|
19
|
-
|
|
18
|
+
.then(() => warn(
|
|
19
|
+
'Make sure you compare the content of your SVG files before merging to the default branch ' +
|
|
20
|
+
'- sometimes optimising can change their contents!'
|
|
21
|
+
)),
|
|
22
|
+
Promise.resolve())
|
|
20
23
|
};
|
package/src/fn/create-users.js
CHANGED
|
@@ -67,10 +67,13 @@ const execute = async () => {
|
|
|
67
67
|
|
|
68
68
|
const users = parseUsersData(fs.read(csvPath));
|
|
69
69
|
const warnings = [];
|
|
70
|
-
for (
|
|
70
|
+
for (const user of users) {
|
|
71
71
|
const userInfo = await getUserInfo(user);
|
|
72
72
|
if (userInfo && userInfo.warn) {
|
|
73
|
-
warnings.push(
|
|
73
|
+
warnings.push(
|
|
74
|
+
`The user "${user.username}" would replicate ${userInfo.total_docs}, `
|
|
75
|
+
+ `which is above the recommended limit of ${userInfo.limit}.`
|
|
76
|
+
);
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
if (warnings.length) {
|
|
@@ -81,7 +84,7 @@ const execute = async () => {
|
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
for (
|
|
87
|
+
for (const user of users) {
|
|
85
88
|
info(`Creating user ${user.username}`);
|
|
86
89
|
await api().createUser(user);
|
|
87
90
|
}
|
package/src/fn/csv-to-docs.js
CHANGED
|
@@ -56,23 +56,23 @@ const execute = () => {
|
|
|
56
56
|
return fs.recurseFiles(csvDir)
|
|
57
57
|
.filter(name => name.endsWith('.csv'))
|
|
58
58
|
.reduce((promiseChain, csv) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
promiseChain
|
|
60
|
+
.then(() => {
|
|
61
|
+
info('Processing CSV file:', csv, '…');
|
|
62
|
+
|
|
63
|
+
const nameParts = fs.path.basename(csv).split('.');
|
|
64
|
+
const prefix = nameParts[0];
|
|
65
|
+
switch(prefix) {
|
|
66
|
+
case 'contact': return processContacts('contact', csv);
|
|
67
|
+
case 'person': return processPersons(csv);
|
|
68
|
+
case 'place': return processPlaces(csv);
|
|
69
|
+
case 'report': return processReports(nameParts[1], csv);
|
|
70
|
+
case 'users' : return processUsers(csv);
|
|
71
|
+
default: throw new Error(`Unrecognised CSV type ${prefix} for file ${csv}`);
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
.then(docs => addToModel(csv, docs)),
|
|
75
|
+
Promise.resolve())
|
|
76
76
|
|
|
77
77
|
.then(() => model.references.forEach(updateRef))
|
|
78
78
|
.then(() => model.exclusions.forEach(removeExcludedField))
|
|
@@ -132,16 +132,20 @@ const execute = () => {
|
|
|
132
132
|
for(let i=0; i<cols.length; ++i) {
|
|
133
133
|
const { col, val, reference, excluded } = parseColumn(cols[i], row[i]);
|
|
134
134
|
setCol(doc, col, val);
|
|
135
|
-
if(reference)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
135
|
+
if(reference) {
|
|
136
|
+
model.references.push({
|
|
137
|
+
doc: doc,
|
|
138
|
+
matcher: reference,
|
|
139
|
+
colVal: val,
|
|
140
|
+
targetProperty: col,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if(excluded) {
|
|
144
|
+
model.exclusions.push({
|
|
145
|
+
doc: doc,
|
|
146
|
+
propertyName: col,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
return withId(doc);
|
|
@@ -157,25 +161,36 @@ const execute = () => {
|
|
|
157
161
|
function setCol(doc, col, val) {
|
|
158
162
|
const colParts = col.split('.');
|
|
159
163
|
|
|
160
|
-
if(RESERVED_COL_NAMES.includes(colParts[0]))
|
|
164
|
+
if(RESERVED_COL_NAMES.includes(colParts[0])) {
|
|
161
165
|
throw new Error(`Cannot set property defined by column '${col}' - this property name is protected.`);
|
|
166
|
+
}
|
|
162
167
|
|
|
163
168
|
while(colParts.length > 1) {
|
|
164
169
|
col = colParts.shift();
|
|
165
|
-
if(!doc[col])
|
|
170
|
+
if(!doc[col]) {
|
|
171
|
+
doc[col] = {};
|
|
172
|
+
}
|
|
166
173
|
doc = doc[col];
|
|
167
174
|
}
|
|
168
175
|
doc[colParts[0]] = val;
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
function parseTimestamp(t) {
|
|
172
|
-
if(isIntegerString(t))
|
|
173
|
-
|
|
179
|
+
if(isIntegerString(t)) {
|
|
180
|
+
return int(t);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
return Date.parse(t);
|
|
184
|
+
}
|
|
174
185
|
}
|
|
175
186
|
|
|
176
187
|
function parseBool(b) {
|
|
177
|
-
if(isIntegerString(b))
|
|
178
|
-
|
|
188
|
+
if(isIntegerString(b)) {
|
|
189
|
+
return b !== '0';
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
return b.toLowerCase() === 'true';
|
|
193
|
+
}
|
|
179
194
|
}
|
|
180
195
|
|
|
181
196
|
function calcRelTimestampInDays(b) {
|
|
@@ -206,7 +221,9 @@ function matchesType(type, doc) {
|
|
|
206
221
|
|
|
207
222
|
function matchesWhereClause(where, doc, colVal) {
|
|
208
223
|
const whereMatch = where.match(/^([\w-]+)=COL_VAL$/);
|
|
209
|
-
if(!whereMatch)
|
|
224
|
+
if(!whereMatch) {
|
|
225
|
+
throw new Error(`Cannot interpret WHERE clause: ${where}`);
|
|
226
|
+
}
|
|
210
227
|
|
|
211
228
|
const [, propertyName] = whereMatch;
|
|
212
229
|
|
|
@@ -218,7 +235,9 @@ function removeExcludedField(exclusion) {
|
|
|
218
235
|
}
|
|
219
236
|
|
|
220
237
|
function parseColumn(rawCol, rawVal) {
|
|
221
|
-
let val
|
|
238
|
+
let val;
|
|
239
|
+
let reference;
|
|
240
|
+
let excluded = false;
|
|
222
241
|
|
|
223
242
|
const parts = rawCol.split(/[:>]/);
|
|
224
243
|
const col = parts[0];
|
|
@@ -228,26 +247,28 @@ function parseColumn(rawCol, rawVal) {
|
|
|
228
247
|
} else if(parts.length === 2) {
|
|
229
248
|
const type = parts[1];
|
|
230
249
|
switch(type) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
250
|
+
case 'date': val = new Date(rawVal); break;
|
|
251
|
+
case 'rel-date': val = (new Date(calcRelTimestampInDays(rawVal))).toISOString().substring(0, 10); break;
|
|
252
|
+
case 'timestamp': val = parseTimestamp(rawVal); break;
|
|
253
|
+
case 'rel-timestamp': val = calcRelTimestampInMilliseconds(rawVal); break;
|
|
254
|
+
case 'int': val = int(rawVal); break;
|
|
255
|
+
case 'bool': val = parseBool(rawVal); break;
|
|
256
|
+
case 'string': val = rawVal; break;
|
|
257
|
+
case 'float': val = Number.parseFloat(rawVal); break;
|
|
258
|
+
case 'excluded': val = rawVal; excluded = true; break;
|
|
259
|
+
default: {
|
|
260
|
+
if(isReference(type)) {
|
|
261
|
+
val = rawVal;
|
|
262
|
+
reference = type;
|
|
263
|
+
} else {
|
|
264
|
+
throw new Error(`Unrecognised column type: ${type} for ${rawCol}`);
|
|
247
265
|
}
|
|
248
266
|
}
|
|
267
|
+
}
|
|
249
268
|
} else {
|
|
250
|
-
throw new Error(
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Wrong number of parts in column definition: ${rawCol} (should be 1, 2 or 4, but found ${parts.length}).`
|
|
271
|
+
);
|
|
251
272
|
}
|
|
252
273
|
return { col:col, val:val, reference:reference, excluded:excluded };
|
|
253
274
|
}
|
|
@@ -44,6 +44,7 @@ const parseExtraArgs = (projectDir, extraArgs = []) => {
|
|
|
44
44
|
|
|
45
45
|
const bold = text => `\x1b[1m${text}\x1b[0m`;
|
|
46
46
|
const usage = () => {
|
|
47
|
+
/* eslint-disable max-len */
|
|
47
48
|
info(`
|
|
48
49
|
${bold('cht-conf\'s delete-contacts action')}
|
|
49
50
|
When combined with 'upload-docs' this action recursively deletes a contact and all of their descendant contacts and data. ${bold('This operation is permanent. It cannot be undone.')}
|
|
@@ -61,4 +62,5 @@ ${bold('OPTIONS')}
|
|
|
61
62
|
--docDirectoryPath=<path to stage docs>
|
|
62
63
|
Specifies the folder used to store the documents representing the changes in hierarchy.
|
|
63
64
|
`);
|
|
65
|
+
/* eslint-enable max-len */
|
|
64
66
|
};
|
package/src/fn/edit-contacts.js
CHANGED
|
@@ -17,7 +17,10 @@ const jsonDocPath = (directoryPath, docID) => `${directoryPath}/${docID}.doc.jso
|
|
|
17
17
|
|
|
18
18
|
// check to see if we should write/overwrite the file
|
|
19
19
|
const overwriteFileCheck = (doc, args, overwriteAllFiles) => {
|
|
20
|
-
return args.updateOfflineDocs
|
|
20
|
+
return args.updateOfflineDocs
|
|
21
|
+
|| environment.force
|
|
22
|
+
|| overwriteAllFiles
|
|
23
|
+
|| !fs.exists(jsonDocPath(args.docDirectoryPath, doc._id));
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
const saveJsonDoc = (doc, args) => {
|
|
@@ -62,19 +65,19 @@ const execute = () => {
|
|
|
62
65
|
.filter(name => name.endsWith('.csv'))
|
|
63
66
|
.reduce((promiseChain, csv) =>
|
|
64
67
|
promiseChain
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
.then(() => {
|
|
69
|
+
info('Processing CSV file:', csv, '…');
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
const nameParts = fs.path.basename(csv).split('.');
|
|
72
|
+
const prefix = nameParts[0];
|
|
73
|
+
switch(prefix) {
|
|
71
74
|
case 'contact': return processDocuments('contact', csv, getIDs(csv, prefix), db, args);
|
|
72
75
|
case 'users': return processDocuments('user', csv, getIDs(csv, prefix), db, args);
|
|
73
76
|
default: throw new Error(`Unrecognised CSV type ${prefix} for file ${csv}`);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
.then(docs => addToModel(csv, docs)),
|
|
80
|
+
Promise.resolve())
|
|
78
81
|
|
|
79
82
|
.then(() => model.exclusions.forEach(toDocs.removeExcludedField))
|
|
80
83
|
.then(() => writeDocs(model.docs, args));
|
|
@@ -98,7 +101,7 @@ function getIDs(csv, docType) {
|
|
|
98
101
|
const { rows, cols } = fs.readCsv(csv);
|
|
99
102
|
const index = cols.indexOf(DOCUMENT_ID);
|
|
100
103
|
if (index === -1){
|
|
101
|
-
|
|
104
|
+
throw Error('missing "documentID" column.');
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
const idPrefix = docType === 'contact' ? '' : 'org.couchdb.user:';
|
|
@@ -5,8 +5,8 @@ module.exports = {
|
|
|
5
5
|
requiresInstance: false,
|
|
6
6
|
execute: async () => {
|
|
7
7
|
await fetchFilesFromGoogleDrive(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
`${environment.pathToProject}/csvs-on-google-drive.json`,
|
|
9
|
+
`${environment.pathToProject}/csv`,
|
|
10
|
+
'text/csv');
|
|
11
11
|
}
|
|
12
12
|
};
|