cht-conf 4.8.0 → 4.9.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,17 @@
1
+ # [4.9.0](https://github.com/medic/cht-conf/compare/v4.8.1...v4.9.0) (2025-06-02)
2
+
3
+
4
+ ### Features
5
+
6
+ * **#692:** update the priority function to accept additional parameters ([#699](https://github.com/medic/cht-conf/issues/699)) ([ad28242](https://github.com/medic/cht-conf/commit/ad28242603d5d2d89bb48324e93e60b55052ce3d)), closes [#692](https://github.com/medic/cht-conf/issues/692) [#692](https://github.com/medic/cht-conf/issues/692)
7
+
8
+ ## [4.8.1](https://github.com/medic/cht-conf/compare/v4.8.0...v4.8.1) (2025-05-07)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **#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)
14
+
1
15
  # [4.8.0](https://github.com/medic/cht-conf/compare/v4.7.0...v4.8.0) (2025-04-25)
2
16
 
3
17
 
package/README.md CHANGED
@@ -168,6 +168,7 @@ In order to avoid overwriting someone else's configuration cht-conf records the
168
168
  * upload privacy policies to server
169
169
  * upload branding to server
170
170
  * upload partners to server
171
+ * upload database indexes to server
171
172
 
172
173
  ## Forms
173
174
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cht-conf",
3
- "version": "4.8.0",
3
+ "version": "4.9.0",
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",
@@ -92,7 +93,7 @@
92
93
  "nyc": "^17.1.0",
93
94
  "pouchdb-adapter-memory": "^7.2.2",
94
95
  "rewire": "^7.0.0",
95
- "semantic-release": "^22.0.0",
96
+ "semantic-release": "^24.2.4",
96
97
  "sinon": "^19.0.2"
97
98
  }
98
99
  }
@@ -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) => {
@@ -136,7 +136,7 @@ function emitTasks(taskDefinition, Utils, Task, emit, c, r) {
136
136
 
137
137
  priority = taskDefinition.priority;
138
138
  if (typeof priority === 'function') {
139
- priority = priority(c, r);
139
+ priority = priority(c, r, event, dueDate, scheduledTaskIdx);
140
140
  }
141
141
 
142
142
  if (priority) {