cht-conf 4.8.0 → 4.8.1
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 +7 -0
- package/README.md +1 -0
- package/package.json +2 -1
- package/src/fn/upload-database-indexes.js +178 -0
- package/src/lib/db.js +1 -0
- package/src/lib/main.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [4.8.1](https://github.com/medic/cht-conf/compare/v4.8.0...v4.8.1) (2025-05-07)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **#680:** add command for uploading db indexes ([#681](https://github.com/medic/cht-conf/issues/681)) ([6170b57](https://github.com/medic/cht-conf/commit/6170b576faa4cc72d09350ba72eaedb57155d6c4)), closes [#680](https://github.com/medic/cht-conf/issues/680)
|
|
7
|
+
|
|
1
8
|
# [4.8.0](https://github.com/medic/cht-conf/compare/v4.7.0...v4.8.0) (2025-04-25)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cht-conf",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.1",
|
|
4
4
|
"description": "Configure CHT deployments",
|
|
5
5
|
"main": "./src/lib/main.js",
|
|
6
6
|
"engines": {
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"pluralize": "^8.0.0",
|
|
57
57
|
"pouchdb-adapter-http": "^7.2.2",
|
|
58
58
|
"pouchdb-core": "^7.2.2",
|
|
59
|
+
"pouchdb-find": "^7.2.2",
|
|
59
60
|
"pouchdb-mapreduce": "^7.2.2",
|
|
60
61
|
"pouchdb-session-authentication": "^1.4.0",
|
|
61
62
|
"properties": "^1.2.1",
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const environment = require('../lib/environment');
|
|
2
|
+
const fs = require('../lib/sync-fs');
|
|
3
|
+
const pouch = require('../lib/db');
|
|
4
|
+
const log = require('../lib/log');
|
|
5
|
+
const warnUploadOverwrite = require('../lib/warn-upload-overwrite');
|
|
6
|
+
const insertOrReplace = require('../lib/insert-or-replace');
|
|
7
|
+
const jsonDiff = require('json-diff');
|
|
8
|
+
const { info } = require('../lib/log');
|
|
9
|
+
|
|
10
|
+
// https://pouchdb.com/api.html#list_indexes
|
|
11
|
+
// > Also tells you about the special _all_docs index, i.e. the default index on the _id field.
|
|
12
|
+
const BUILT_IN_INDEX_TYPE = 'special';
|
|
13
|
+
|
|
14
|
+
const standardizePartialFilterContent = (partialFilter) => {
|
|
15
|
+
if (!partialFilter) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isRootAndOnly = (
|
|
20
|
+
Object.keys(partialFilter).length === 1 &&
|
|
21
|
+
Object.hasOwn(partialFilter, '$and') &&
|
|
22
|
+
Array.isArray(partialFilter['$and'])
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return isRootAndOnly ? Object.assign({}, ...partialFilter['$and']) : partialFilter;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const hasDefinitionDiff = (dbIndex, configDefinition) => {
|
|
29
|
+
// The db item's fields have an order accompanying the field
|
|
30
|
+
const dbItemDefinition = { ...dbIndex.def, fields: (dbIndex.def.fields || []).map(e => Object.keys(e)[0]) };
|
|
31
|
+
|
|
32
|
+
// We observed that our unit tests returned no '$and' grouping for the partial filter.
|
|
33
|
+
// In contrast, testing against a real instance returned a grouping.
|
|
34
|
+
// To reliably determine definition diff, we ignore the db filter fields root '$and'.
|
|
35
|
+
// https://docs.couchdb.org/en/stable/api/database/find.html#explicit-operators
|
|
36
|
+
dbItemDefinition.partial_filter_selector = standardizePartialFilterContent(dbItemDefinition.partial_filter_selector);
|
|
37
|
+
|
|
38
|
+
return !!jsonDiff.diff(configDefinition, dbItemDefinition);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const cleanUpIndexes = async (db, storedIndexes, indexConfig) => {
|
|
42
|
+
for (const [key, value] of Object.entries(storedIndexes)) {
|
|
43
|
+
// Delete the index if it is no longer in the config
|
|
44
|
+
if (!(key in indexConfig)) {
|
|
45
|
+
await db.deleteIndex(value);
|
|
46
|
+
}
|
|
47
|
+
// We probably don't want to unnecessarily delete/recreate indexes
|
|
48
|
+
else if (key in indexConfig && hasDefinitionDiff(value, indexConfig[key])) {
|
|
49
|
+
await db.deleteIndex(value);
|
|
50
|
+
log.warn(`The "${value.name}" index config differs from what is saved and has been deleted for recreation.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const createIndexes = async (db, indexConfig) => {
|
|
56
|
+
for (const [key, value] of Object.entries(indexConfig)) {
|
|
57
|
+
const props = {
|
|
58
|
+
'index': value,
|
|
59
|
+
'ddoc': key,
|
|
60
|
+
'name': key,
|
|
61
|
+
'type': 'json'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
await db.createIndex(props);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const manageIndexes = async (db, indexMapping = {}) => {
|
|
69
|
+
const result = await db.getIndexes();
|
|
70
|
+
const indexes = Object.fromEntries((result.indexes || [])
|
|
71
|
+
.filter(obj => obj.type !== BUILT_IN_INDEX_TYPE)
|
|
72
|
+
// We're using the "name" here as the "ddoc" loaded from db will have a "_design/" prefix
|
|
73
|
+
.map(doc => [doc.name, doc])
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
await cleanUpIndexes(db, indexes, indexMapping);
|
|
77
|
+
await createIndexes(db, indexMapping);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const bold = text => `\x1b[1m${text}\x1b[0m`;
|
|
81
|
+
const usage = () => {
|
|
82
|
+
/* eslint-disable max-len */
|
|
83
|
+
info(`
|
|
84
|
+
${bold('cht-conf\'s upload-database-indexes action')}
|
|
85
|
+
Define indexes for the "medic" database to enhance query performance. To index only a subset of documents, use "partial_filter_selector".
|
|
86
|
+
|
|
87
|
+
${bold('Structure:')}
|
|
88
|
+
| ${bold('Prop')} | ${bold('Description')} |
|
|
89
|
+
| ${bold('[key]')} | The object key will be used for both the "name" and "ddoc" values of the index |
|
|
90
|
+
| ${bold('"fields"')} | A list of fields to index |
|
|
91
|
+
| ${bold('"partial_filter_selector"')} (optional) | A selector used to filter the set of documents included in the index |
|
|
92
|
+
|
|
93
|
+
${bold('Example:')}
|
|
94
|
+
{
|
|
95
|
+
"testing_by_id_and_type": {
|
|
96
|
+
"fields": ["_id", "type"],
|
|
97
|
+
"partial_filter_selector": {
|
|
98
|
+
"type": { "$nin": ["form", "translations", "meta"] },
|
|
99
|
+
"_id": { "$nin": ["branding", "extension-libs", "resources"] }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
${bold('Rational:')}
|
|
104
|
+
- The index is created on the "_id" and "type" fields.
|
|
105
|
+
According to the ESR (Equality, Sort, Range) rule, fields used in equality matches should come first, followed by sort fields, and then range fields.
|
|
106
|
+
This ordering optimizes query performance.
|
|
107
|
+
- The "partial_filter_selector" limits the index to documents where "type" and "_id" do not match the specified values, reducing index size and improving efficiency.
|
|
108
|
+
|
|
109
|
+
The filename 'database-indexes.json' should be used to contain the above configuration.
|
|
110
|
+
|
|
111
|
+
${bold('More info:')}
|
|
112
|
+
https://pouchdb.com/api.html#create_index
|
|
113
|
+
https://pouchdb.com/api.html#query_index
|
|
114
|
+
https://docs.couchdb.org/en/stable/api/database/find.html#db-find
|
|
115
|
+
https://www.mongodb.com/docs/manual/tutorial/equality-sort-range-guideline/
|
|
116
|
+
`);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const throwIfNotObject = (value, key = null) => {
|
|
120
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
121
|
+
const message = key
|
|
122
|
+
? `Entry "${key}" is not a valid JSON object (map).`
|
|
123
|
+
: 'The top-level structure must be a JSON object (map).';
|
|
124
|
+
usage();
|
|
125
|
+
throw new Error(message);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const parseAndCheckFile = (indexPath) => {
|
|
130
|
+
const map = fs.readJson(indexPath);
|
|
131
|
+
|
|
132
|
+
throwIfNotObject(map);
|
|
133
|
+
|
|
134
|
+
for (const [key, entry] of Object.entries(map)) {
|
|
135
|
+
throwIfNotObject(entry, key);
|
|
136
|
+
|
|
137
|
+
if (!Object.hasOwn(entry, 'fields')) {
|
|
138
|
+
throw new Error(`Missing "fields" property in entry "${key}".`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return map;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
requiresInstance: true,
|
|
147
|
+
execute: async () => {
|
|
148
|
+
const key = 'database-indexes';
|
|
149
|
+
const path = `${environment.pathToProject}/${key}.json`;
|
|
150
|
+
const indexPath = fs.path.resolve(path);
|
|
151
|
+
|
|
152
|
+
if (!fs.exists(indexPath)) {
|
|
153
|
+
log.warn(`No ${key} mapping file found at path: ${indexPath}`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const indexMapping = parseAndCheckFile(indexPath);
|
|
158
|
+
|
|
159
|
+
const doc = {
|
|
160
|
+
_id: key,
|
|
161
|
+
[key]: indexMapping
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const db = pouch();
|
|
165
|
+
|
|
166
|
+
const changes = await warnUploadOverwrite.preUploadDoc(db, doc);
|
|
167
|
+
if (changes) {
|
|
168
|
+
await manageIndexes(db, indexMapping);
|
|
169
|
+
await insertOrReplace(db, doc);
|
|
170
|
+
|
|
171
|
+
log.info(`${key} file uploaded`);
|
|
172
|
+
} else {
|
|
173
|
+
log.info(`${key} file not uploaded as no changes found`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await warnUploadOverwrite.postUploadDoc(db, doc);
|
|
177
|
+
}
|
|
178
|
+
};
|
package/src/lib/db.js
CHANGED
|
@@ -2,6 +2,7 @@ const PouchDB = require('pouchdb-core');
|
|
|
2
2
|
PouchDB.plugin(require('pouchdb-adapter-http'));
|
|
3
3
|
PouchDB.plugin(require('pouchdb-mapreduce'));
|
|
4
4
|
PouchDB.plugin(require('pouchdb-session-authentication'));
|
|
5
|
+
PouchDB.plugin(require('pouchdb-find'));
|
|
5
6
|
|
|
6
7
|
const ArchivingDB = require('./archiving-db');
|
|
7
8
|
const environment = require('./environment');
|
package/src/lib/main.js
CHANGED
|
@@ -38,6 +38,7 @@ const defaultActions = [
|
|
|
38
38
|
'upload-custom-translations',
|
|
39
39
|
'upload-privacy-policies',
|
|
40
40
|
'upload-extension-libs',
|
|
41
|
+
'upload-database-indexes',
|
|
41
42
|
];
|
|
42
43
|
const defaultArchiveActions = [
|
|
43
44
|
'compile-app-settings',
|
|
@@ -60,6 +61,7 @@ const defaultArchiveActions = [
|
|
|
60
61
|
'upload-custom-translations',
|
|
61
62
|
'upload-privacy-policies',
|
|
62
63
|
'upload-extension-libs',
|
|
64
|
+
'upload-database-indexes',
|
|
63
65
|
];
|
|
64
66
|
|
|
65
67
|
module.exports = async (argv, env) => {
|