ueberdb2 2.2.1 → 3.0.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/.eslintrc.cjs ADDED
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ // This is a workaround for https://github.com/eslint/eslint/issues/3458
4
+ require('eslint-config-etherpad/patch/modern-module-resolution');
5
+
6
+ module.exports = {
7
+ root: true,
8
+ extends: 'etherpad/node',
9
+ overrides: [
10
+ {
11
+ files: [
12
+ 'test/**/*',
13
+ ],
14
+ extends: 'etherpad/tests/backend',
15
+ overrides: [
16
+ {
17
+ files: [
18
+ 'test/lib/**/*',
19
+ ],
20
+ rules: {
21
+ 'mocha/no-exports': 'off',
22
+ },
23
+ },
24
+ ],
25
+ },
26
+ ],
27
+ };
@@ -19,6 +19,12 @@ jobs:
19
19
  env:
20
20
  COUCHDB_USER: ueberdb
21
21
  COUCHDB_PASSWORD: ueberdb
22
+ elasticsearch:
23
+ image: elasticsearch:7.17.3
24
+ ports:
25
+ - 9200:9200
26
+ env:
27
+ discovery.type: single-node
22
28
  mongo:
23
29
  image: mongo
24
30
  ports:
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Notable Changes
2
2
 
3
+ ## v3.0.0
4
+
5
+ Compatibility changes:
6
+
7
+ * Minimum supported Node.js version is now 14.15.0.
8
+ * `elasticsearch`: New index name and mapping (schema). To automatically copy
9
+ existing data to the new index when the ueberdb client is initialized, set
10
+ the `migrate_to_newer_schema` option to `true`.
11
+ * As mentioned in the v2.2.0 changes, passing callbacks to the database
12
+ methods is deprecated. Use the returned Promises instead.
13
+ * As mentioned in the v1.4.15 changes, `postgrespool` is deprecated. Use
14
+ `postgres` instead.
15
+
16
+ Bug fixes:
17
+
18
+ * `elasticsearch`: Rewrote driver to fix numerous bugs and modernize the code.
19
+
20
+ Updated database dependencies:
21
+
22
+ * `couch`: Updated `nano` to 10.0.0.
23
+ * `dirty_git`: Updated `simple-git` to 3.7.1.
24
+ * `elasticsearch`: Switched the client library from the deprecated
25
+ `elasticsearch` to `@elastic/elasticsearch` version 7.17.0.
26
+ * `postgres`: Updated `pg` to 8.7.3.
27
+ * `sqlite`: Updated `sqlite3` to 5.0.6.
28
+
3
29
  ## v2.2.0
4
30
 
5
31
  Compatibility changes:
@@ -16,144 +16,184 @@
16
16
  */
17
17
 
18
18
  const AbstractDatabase = require('../lib/AbstractDatabase');
19
- const es = require('elasticsearch');
19
+ const assert = require('assert').strict;
20
+ const {Buffer} = require('buffer');
21
+ const crypto = require('crypto');
22
+ const es = require('elasticsearch7');
20
23
 
21
- // initialize w/ default settings
22
- const elasticsearchSettings = {
23
- hostname: '127.0.0.1',
24
- port: '9200',
25
- base_index: 'ueberes',
24
+ const schema = '2';
26
25
 
27
- // for a list of valid API values see:
28
- // https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/configuration.html#config-options
29
- api: '7.6',
26
+ const keyToId = (key) => {
27
+ const keyBuf = Buffer.from(key);
28
+ return keyBuf.length > 512 ? crypto.createHash('sha512').update(keyBuf).digest('hex') : key;
30
29
  };
31
30
 
32
- let client;
31
+ const mappings = {
32
+ // _id is expected to equal key, unless the UTF-8 encoded key is > 512 bytes, in which case it is
33
+ // the hex-encoded sha512 hash of the UTF-8 encoded key.
34
+ properties: {
35
+ key: {type: 'wildcard'}, // For findKeys, and because _id is limited to 512 bytes.
36
+ value: {type: 'object', enabled: false}, // Values should be opaque to Elasticsearch.
37
+ },
38
+ };
39
+
40
+ const migrateToSchema2 = async (client, v1BaseIndex, v2Index, logger) => {
41
+ let recordsMigratedLastLogged = 0;
42
+ let recordsMigrated = 0;
43
+ const totals = new Map();
44
+ logger.info('Attempting elasticsearch record migration from schema v1 at base index ' +
45
+ `${v1BaseIndex} to schema v2 at index ${v2Index}...`);
46
+ const {body: indices} = await client.indices.get({index: [v1BaseIndex, `${v1BaseIndex}-*-*`]});
47
+ const scrollIds = new Map();
48
+ const q = [];
49
+ try {
50
+ for (const index of Object.keys(indices)) {
51
+ const {body: res} = await client.search({index, scroll: '10m'});
52
+ scrollIds.set(index, res._scroll_id);
53
+ q.push({index, res});
54
+ }
55
+ while (q.length) {
56
+ const {index, res: {hits: {hits, total: {value: total}}}} = q.shift();
57
+ if (hits.length === 0) continue;
58
+ totals.set(index, total);
59
+ const body = [];
60
+ for (const {_id, _type, _source: {val}} of hits) {
61
+ let key = `${_type}:${_id}`;
62
+ if (index !== v1BaseIndex) {
63
+ const parts = index.slice(v1BaseIndex.length + 1).split('-');
64
+ if (parts.length !== 2) {
65
+ throw new Error(`unable to migrate records from index ${index} due to data ambiguity`);
66
+ }
67
+ key = `${parts[0]}:${decodeURIComponent(_type)}:${parts[1]}:${_id}`;
68
+ }
69
+ body.push({index: {_id: keyToId(key)}}, {key, value: JSON.parse(val)});
70
+ }
71
+ await client.bulk({index: v2Index, body});
72
+ recordsMigrated += hits.length;
73
+ if (Math.floor(recordsMigrated / 100) > Math.floor(recordsMigratedLastLogged / 100)) {
74
+ const total = [...totals.values()].reduce((a, b) => a + b, 0);
75
+ logger.info(`Migrated ${recordsMigrated} records out of ${total}`);
76
+ recordsMigratedLastLogged = recordsMigrated;
77
+ }
78
+ q.push(
79
+ {index, res: (await client.scroll({scroll: '5m', scrollId: scrollIds.get(index)})).body});
80
+ }
81
+ logger.info(`Finished migrating ${recordsMigrated} records`);
82
+ } finally {
83
+ await Promise.all([...scrollIds.values()].map((scrollId) => client.clearScroll({scrollId})));
84
+ }
85
+ };
33
86
 
34
87
  exports.Database = class extends AbstractDatabase {
35
88
  constructor(settings) {
36
89
  super();
37
- this.db = null;
38
-
39
- this.settings = settings || {};
40
-
41
- // update settings if they were provided
42
- if (this.settings.host) {
43
- elasticsearchSettings.hostname = this.settings.host;
44
- }
45
-
46
- if (this.settings.port) {
47
- elasticsearchSettings.port = this.settings.port;
48
- }
90
+ this._client = null;
91
+ this.settings = {
92
+ host: '127.0.0.1',
93
+ port: '9200',
94
+ base_index: 'ueberes',
95
+ migrate_to_newer_schema: false,
96
+ // for a list of valid API values see:
97
+ // https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/configuration.html#config-options
98
+ api: '7.6',
99
+ ...settings || {},
100
+ json: false, // Elasticsearch will do the JSON conversion as necessary.
101
+ };
102
+ this._index = `${this.settings.base_index}_s${schema}`;
103
+ this._q = {index: this._index};
104
+ this._indexClean = true;
105
+ }
49
106
 
50
- if (this.settings.base_index) {
51
- elasticsearchSettings.base_index = this.settings.base_index;
52
- }
107
+ get isAsync() { return true; }
53
108
 
54
- if (this.settings.api) {
55
- elasticsearchSettings.api = this.settings.api;
56
- }
109
+ async _refreshIndex() {
110
+ if (this._indexClean) return;
111
+ this._indexClean = true;
112
+ await this._client.indices.refresh(this._q);
57
113
  }
58
114
 
59
115
  /**
60
116
  * Initialize the elasticsearch client, then ping the server to ensure that a
61
117
  * connection was made.
62
118
  */
63
- init(callback) {
119
+ async init() {
64
120
  // create elasticsearch client
65
- client = new es.Client({
66
- host: `${elasticsearchSettings.hostname}:${elasticsearchSettings.port}`,
67
- apiVersion: elasticsearchSettings.api,
68
- // log: "trace" // useful for debugging
121
+ const client = new es.Client({
122
+ node: `http://${this.settings.host}:${this.settings.port}`,
69
123
  });
70
-
71
- // test the connection
72
- client.ping({
73
- requestTimeout: 3000,
74
- }, (error) => {
75
- if (error) {
76
- console.error('unable to communicate with elasticsearch');
124
+ await client.ping();
125
+ if (!(await client.indices.exists({index: this._index})).body) {
126
+ let tmpIndex;
127
+ const {body: migrate} = await client.indices.exists({index: this.settings.base_index});
128
+ if (migrate && !this.settings.migrate_to_newer_schema) {
129
+ throw new Error(
130
+ `Data exists under the legacy index (schema) named ${this.settings.base_index}. ` +
131
+ 'Set migrate_to_newer_schema to true to copy the existing data to a new index ' +
132
+ `named ${this._index}.`);
77
133
  }
78
-
79
- callback(error);
80
- });
134
+ let attempt = 0;
135
+ while (true) {
136
+ tmpIndex = `${this._index}_${migrate ? 'migrate_attempt_' : 'i'}${attempt++}`;
137
+ if (!(await client.indices.exists({index: tmpIndex})).body) break;
138
+ }
139
+ await client.indices.create({index: tmpIndex, body: {mappings}});
140
+ if (migrate) await migrateToSchema2(client, this.settings.base_index, tmpIndex, this.logger);
141
+ await client.indices.putAlias({index: tmpIndex, name: this._index});
142
+ }
143
+ const indices = Object.values((await client.indices.get({index: this._index})).body);
144
+ assert.equal(indices.length, 1);
145
+ try {
146
+ assert.deepEqual(indices[0].mappings, mappings);
147
+ } catch (err) {
148
+ this.logger.warn(`Index ${this._index} mappings does not match expected; ` +
149
+ `attempting to use index anyway. Details: ${err}`);
150
+ }
151
+ this._client = client;
81
152
  }
82
153
 
83
154
  /**
84
155
  * This function provides read functionality to the database.
85
156
  *
86
- * @param {String} key Key, of the format "test:test1" or, optionally, of the
87
- * format "test:test1:check:check1"
88
- * @param {function} callback Function will be called in the event of an error or
89
- * upon completion of a successful database retrieval.
157
+ * @param {String} key Key
90
158
  */
91
- get(key, callback) {
92
- client.get(getIndexTypeId(key), (error, response) => {
93
- parseResponse(error, response, callback);
94
- });
159
+ async get(key) {
160
+ const {body} = await this._client.get({...this._q, id: keyToId(key)}, {ignore: [404]});
161
+ if (!body.found) return null;
162
+ return body._source.value;
95
163
  }
96
164
 
97
165
  /**
98
- * The three key scenarios for this are:
99
- * (test:test1, null) ; (test:*, *:*:*) ; (test:*, null)
100
- *
101
- * TODO This currently works only for the second implementation above.
102
- *
103
- * For more information:
104
- * - See the #Limitations section of the ueberDB README.
105
- * - See https://github.com/Pita/ueberDB/wiki/findKeys-functionality, as well
106
- * as the sqlite and mysql implementations.
107
- *
108
166
  * @param key Search key, which uses an asterisk (*) as the wild card.
109
167
  * @param notKey Used to filter the result set
110
- * @param callback First param is error, second is result
111
168
  */
112
- findKeys(key, notKey, callback) {
113
- const splitKey = key.split(':');
114
-
115
- client.search({
116
- index: elasticsearchSettings.base_index,
117
- type: splitKey[0],
118
- size: 100, // this is a pretty random threshold...
119
- }, (error, response) => {
120
- if (error) {
121
- console.error('findkeys', error);
122
- callback(error);
123
- return;
124
- }
125
-
126
- if (!error && response.hits) {
127
- const keys = [];
128
- for (let counter = 0; counter < response.hits.total; counter++) {
129
- keys.push(`${splitKey[0]}:${response.hits.hits[counter]._id}`);
130
- }
131
- callback(null, keys);
132
- }
133
- });
169
+ async findKeys(key, notKey) {
170
+ await this._refreshIndex();
171
+ const q = {
172
+ ...this._q,
173
+ body: {
174
+ query: {
175
+ bool: {
176
+ filter: {wildcard: {key: {value: key}}},
177
+ ...notKey == null ? {} : {
178
+ must_not: {wildcard: {key: {value: notKey}}},
179
+ },
180
+ },
181
+ },
182
+ },
183
+ };
184
+ const {body: {hits: {hits}}} = await this._client.search(q);
185
+ return hits.map((h) => h._source.key);
134
186
  }
135
187
 
136
188
  /**
137
189
  * This function provides write functionality to the database.
138
190
  *
139
- * @param {String} key Key, of the format "test:test1" or, optionally, of the
140
- * format "test:test1:check:check1"
141
- * @param {JSON|String} value The value to be stored to the database. The value is
142
- * always converted to {val:value} before being written to the database, to account
143
- * for situations where the value is just a string.
144
- * @param {function} callback Function will be called in the event of an error or on
145
- * completion of a successful database write.
191
+ * @param {String} key Record identifier.
192
+ * @param {JSON|String} value The value to store in the database.
146
193
  */
147
- set(key, value, callback) {
148
- const options = getIndexTypeId(key);
149
-
150
- options.body = {
151
- val: value,
152
- };
153
-
154
- client.index(options, (error, response) => {
155
- parseResponse(error, response, callback);
156
- });
194
+ async set(key, value) {
195
+ this._indexClean = false;
196
+ await this._client.index({...this._q, id: keyToId(key), body: {key, value}});
157
197
  }
158
198
 
159
199
  /**
@@ -162,15 +202,11 @@ exports.Database = class extends AbstractDatabase {
162
202
  * The index, type, and ID will be parsed from the key, and this document will
163
203
  * be deleted from the database.
164
204
  *
165
- * @param {String} key Key, of the format "test:test1" or, optionally, of the
166
- * format "test:test1:check:check1"
167
- * @param {function} callback Function will be called in the event of an error or on
168
- * completion of a successful database write.
205
+ * @param {String} key Record identifier.
169
206
  */
170
- remove(key, callback) {
171
- client.delete(key, (error, response) => {
172
- parseResponse(error, response, callback);
173
- });
207
+ async remove(key) {
208
+ this._indexClean = false;
209
+ await this._client.delete({...this._q, id: keyToId(key)}, {ignore: [404]});
174
210
  }
175
211
 
176
212
  /**
@@ -181,108 +217,32 @@ exports.Database = class extends AbstractDatabase {
181
217
  *
182
218
  * @param {Array} bulk An array of JSON data in the format:
183
219
  * {"type":type, "key":key, "value":value}
184
- * @param {function} callback This function will be called on an error or upon the
185
- * successful completion of the database write.
186
220
  */
187
- doBulk(bulk, callback) {
221
+ async doBulk(bulk) {
188
222
  // bulk is an array of JSON:
189
223
  // example: [{"type":"set", "key":"sessionstorage:{id}", "value":{"cookie":{...}}]
190
224
 
191
225
  const operations = [];
192
226
 
193
- for (let counter = 0; counter < bulk.length; counter++) {
194
- const indexTypeId = getIndexTypeId(bulk[counter].key);
195
- const operationPayload = {
196
- _index: indexTypeId.index,
197
- _type: indexTypeId.type,
198
- _id: indexTypeId.id,
199
- };
200
-
201
- switch (bulk[counter].type) {
227
+ for (const {type, key, value} of bulk) {
228
+ this._indexClean = false;
229
+ switch (type) {
202
230
  case 'set':
203
- operations.push({index: operationPayload});
204
- operations.push({val: JSON.parse(bulk[counter].value)});
231
+ operations.push({index: {_id: keyToId(key)}});
232
+ operations.push({key, value});
205
233
  break;
206
234
  case 'remove':
207
- operations.push({delete: operationPayload});
235
+ operations.push({delete: {_id: keyToId(key)}});
208
236
  break;
209
237
  default:
210
238
  continue;
211
239
  }
212
240
  }
213
-
214
- // send bulk request
215
- client.bulk({
216
- body: operations,
217
- }, (error, response) => {
218
- parseResponse(error, response, callback);
219
- });
220
- }
221
-
222
- close(callback) {
223
- callback(null);
224
- }
225
- };
226
-
227
- /** ************************
228
- **** Helper functions ****
229
- **************************/
230
-
231
- /**
232
- * This function parses a given key into an object with three
233
- * fields, .index, .type, and .id. This object can then be
234
- * used to build an elasticsearch path or to access an object
235
- * for bulk updates.
236
- *
237
- * @param {String} key Key, of the format "test:test1" or, optionally, of the
238
- * format "test:test1:check:check1"
239
- */
240
- const getIndexTypeId = (key) => {
241
- const returnObject = {};
242
-
243
- const splitKey = key.split(':');
244
-
245
- if (splitKey.length === 4) {
246
- /*
247
- * This is for keys like test:test1:check:check1.
248
- * These keys are stored at /base_index-test-check/test1/check1
249
- */
250
- returnObject.index = `${elasticsearchSettings.base_index}-${splitKey[0]}-${splitKey[2]}`;
251
- returnObject.type = encodeURIComponent(splitKey[1]);
252
- returnObject.id = splitKey[3];
253
- } else {
254
- // everything else ('test:test1') is stored /base_index/test/test1
255
- returnObject.index = elasticsearchSettings.base_index;
256
- returnObject.type = splitKey[0];
257
- returnObject.id = encodeURIComponent(splitKey[1]);
258
- }
259
-
260
- return returnObject;
261
- };
262
-
263
- /**
264
- * Extract data from elasticsearch responses, handle errors, handle callbacks.
265
- */
266
- const parseResponse = (error, response, callback) => {
267
- if (error) {
268
- // don't treat not found as an error (is this specific to etherpad?)
269
- if (error.message === 'Not Found' && !response.found) {
270
- callback(null, null);
271
- return;
272
- } else {
273
- console.error('elasticsearch_db: ', error);
274
- }
241
+ await this._client.bulk({...this._q, body: operations});
275
242
  }
276
243
 
277
- if (!error && response) {
278
- response = response._source;
279
-
280
- if (response) {
281
- response = response.val;
282
- }
283
-
284
- response = JSON.stringify(response);
244
+ async close() {
245
+ if (this._client != null) this._client.close();
246
+ this._client = null;
285
247
  }
286
-
287
- callback(error, response);
288
248
  };
@@ -208,7 +208,7 @@ exports.Database = class extends AbstractDatabase {
208
208
  callback(err);
209
209
  }
210
210
  callback(err, results);
211
- }
211
+ },
212
212
  );
213
213
  }
214
214
 
@@ -622,10 +622,12 @@ exports.Database = class {
622
622
  }
623
623
  if (success) entries.forEach((entry) => markDone(entry, null));
624
624
  }
625
- // At this point we could call db.buffer.evictOld() to ensure that the number of entries in
626
- // this.buffer is at or below capacity, but if we haven't run out of memory by this point then
627
- // it should be safe to continue using the memory until the next call to this.buffer.set()
628
- // evicts the old entries. This saves some CPU cycles at the expense of memory.
625
+ // This call to this.buffer.evictOld() can be safely removed (if we haven't run out of memory by
626
+ // this point then it is probably safe to continue using the memory until the next call to
627
+ // this.buffer.set() evicts the old entries), except removing it would cause some reads to be
628
+ // satisfied from the cache even when this.settings.cache = 0. That would contradict the
629
+ // documented behavior for cache = 0.
630
+ this.buffer.evictOld();
629
631
  }
630
632
  };
631
633
 
package/package.json CHANGED
@@ -21,35 +21,29 @@
21
21
  }
22
22
  ],
23
23
  "dependencies": {
24
- "async": "^3.2.2",
24
+ "async": "^3.2.3",
25
25
  "cassandra-driver": "^4.6.3",
26
26
  "dirty": "^1.1.3",
27
- "elasticsearch": "^16.7.2",
27
+ "elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
28
28
  "mongodb": "^3.7.3",
29
29
  "mssql": "^8.1.0",
30
30
  "mysql": "2.18.1",
31
- "nano": "^9.0.5",
32
- "pg": "^8.7.1",
31
+ "nano": "^10.0.0",
32
+ "pg": "^8.7.3",
33
33
  "redis": "^3.1.2",
34
34
  "rethinkdb": "^2.4.2",
35
- "simple-git": "^3.6.0"
35
+ "simple-git": "^3.7.1"
36
36
  },
37
37
  "optionalDependencies": {
38
- "sqlite3": "github:mapbox/node-sqlite3#593c9d498be2510d286349134537e3bf89401c4a"
38
+ "sqlite3": "^5.0.6"
39
39
  },
40
40
  "devDependencies": {
41
- "cli-table": "^0.3.8",
42
- "eslint": "^7.32.0",
43
- "eslint-config-etherpad": "^2.0.3",
44
- "eslint-plugin-cypress": "^2.12.1",
45
- "eslint-plugin-eslint-comments": "^3.2.0",
46
- "eslint-plugin-mocha": "^9.0.0",
47
- "eslint-plugin-node": "^11.1.0",
48
- "eslint-plugin-prefer-arrow": "^1.2.3",
49
- "eslint-plugin-promise": "^5.1.1",
50
- "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0",
51
- "mocha": "^9.2.2",
41
+ "cli-table": "^0.3.11",
42
+ "eslint": "^8.14.0",
43
+ "eslint-config-etherpad": "^3.0.13",
44
+ "mocha": "^10.0.0",
52
45
  "randexp": "^0.5.3",
46
+ "typescript": "^4.6.4",
53
47
  "wtfnode": "^0.9.1"
54
48
  },
55
49
  "repository": {
@@ -57,7 +51,7 @@
57
51
  "url": "https://github.com/ether/ueberDB.git"
58
52
  },
59
53
  "main": "./index",
60
- "version": "2.2.1",
54
+ "version": "3.0.0",
61
55
  "bugs": {
62
56
  "url": "https://github.com/ether/ueberDB/issues"
63
57
  },
@@ -77,46 +71,7 @@
77
71
  "email": "john@mclear.co.uk"
78
72
  }
79
73
  ],
80
- "eslintConfig": {
81
- "overrides": [
82
- {
83
- "files": [
84
- "**/.eslintrc.js"
85
- ],
86
- "extends": "etherpad/node"
87
- },
88
- {
89
- "files": [
90
- "**/*"
91
- ],
92
- "excludedFiles": [
93
- "**/.eslintrc.js"
94
- ],
95
- "extends": "etherpad/node"
96
- },
97
- {
98
- "files": [
99
- "test/**/*"
100
- ],
101
- "excludedFiles": [
102
- "**/.eslintrc.js"
103
- ],
104
- "extends": "etherpad/tests/backend",
105
- "overrides": [
106
- {
107
- "files": [
108
- "test/lib/**/*"
109
- ],
110
- "rules": {
111
- "mocha/no-exports": "off"
112
- }
113
- }
114
- ]
115
- }
116
- ],
117
- "root": true
118
- },
119
74
  "engines": {
120
- "node": "^10.17.0 || >=11.14.0"
75
+ "node": ">=14.15.0"
121
76
  }
122
77
  }
@@ -61,4 +61,10 @@ exports.databases = {
61
61
  findKeysMax: 30,
62
62
  },
63
63
  },
64
+ elasticsearch: {
65
+ base_index: 'ueberdb_test',
66
+ speeds: {
67
+ findKeysMax: 30,
68
+ },
69
+ },
64
70
  };
package/test/test.js CHANGED
@@ -21,7 +21,7 @@ after(async function () {
21
21
  console.error('node should have exited by now but something is keeping it open ' +
22
22
  'such as an open connection or active timer');
23
23
  wtfnode.dump();
24
- process.exit(1); // eslint-disable-line no-process-exit
24
+ process.exit(1); // eslint-disable-line n/no-process-exit
25
25
  }, 5000).unref();
26
26
  });
27
27
 
@@ -237,7 +237,7 @@ describe(__filename, function () {
237
237
  });
238
238
 
239
239
  it('speed is acceptable', async function () {
240
- this.timeout(60000);
240
+ this.timeout(180000);
241
241
 
242
242
  const {speeds: {
243
243
  count = 1000,
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ const assert = require('assert').strict;
4
+ const es = require('elasticsearch7');
5
+ const {databases: {elasticsearch: cfg}} = require('./lib/databases');
6
+ const logging = require('../lib/logging');
7
+ const ueberdb = require('../index');
8
+
9
+ const logger = new class extends logging.ConsoleLogger {
10
+ info() {}
11
+ isInfoEnabled() { return false; }
12
+ }();
13
+
14
+ describe(__filename, function () {
15
+ this.timeout(60000);
16
+
17
+ const {base_index = 'ueberdb_test'} = cfg;
18
+ let client;
19
+ let db;
20
+
21
+ beforeEach(async function () {
22
+ client = new es.Client({
23
+ node: `http://${cfg.host || '127.0.0.1'}:${cfg.port || '9200'}`,
24
+ });
25
+ await client.indices.delete({index: `${base_index}*`}, {ignore: [404]});
26
+ });
27
+
28
+ afterEach(async function () {
29
+ if (db != null) await db.close();
30
+ db = null;
31
+ await client.indices.delete({index: `${base_index}*`}, {ignore: [404]});
32
+ client.close();
33
+ client = null;
34
+ });
35
+
36
+ describe('migration to schema v2', function () {
37
+ describe('no old data', function () {
38
+ for (const migrate of [false, true]) {
39
+ it(`migration ${migrate ? 'en' : 'dis'}abled`, async function () {
40
+ const settings = {base_index, ...cfg};
41
+ delete settings.migrate_to_newer_schema;
42
+ db = new ueberdb.Database('elasticsearch', settings, {}, logger);
43
+ await db.init();
44
+ const indices = [];
45
+ const {body: res} = await client.indices.get({index: `${base_index}*`});
46
+ for (const [k, v] of Object.entries(res)) {
47
+ indices.push(k);
48
+ indices.push(...Object.keys(v.aliases));
49
+ }
50
+ assert.deepEqual(indices.sort(), [`${base_index}_s2`, `${base_index}_s2_i0`].sort());
51
+ });
52
+ }
53
+ });
54
+
55
+ describe('existing data', function () {
56
+ const data = new Map([
57
+ ['foo:number', 42],
58
+ ['foo:string', 'value'],
59
+ ['foo:object', {k: 'v'}],
60
+ ['foo:p:s:number', 42],
61
+ ['foo:p:s:string', 'value'],
62
+ ['foo:p:s:object', {k: 'v'}],
63
+ ]);
64
+
65
+ const setOld = async (k, v) => {
66
+ const kp = k.split(':');
67
+ const index = kp.length === 4 ? `${base_index}-${kp[0]}-${kp[2]}` : base_index;
68
+ await client.index({
69
+ index,
70
+ type: kp.length === 4 ? encodeURIComponent(kp[1]) : kp[0],
71
+ id: kp.length === 4 ? kp[3] : encodeURIComponent(kp[1]),
72
+ body: {
73
+ // The old elasticsearch driver was inconsistent: doBulk() called JSON.parse() on the
74
+ // value from ueberdb before writing, but set() did not. We'll assume that any existing
75
+ // data came from set() writes, not doBulk() writes.
76
+ val: JSON.stringify(v),
77
+ },
78
+ });
79
+ await client.indices.refresh({index});
80
+ };
81
+
82
+ beforeEach(async function () {
83
+ await Promise.all([...data].map(async ([k, v]) => await setOld(k, v)));
84
+ });
85
+
86
+ it('migration disabled => init error', async function () {
87
+ const settings = {base_index, ...cfg};
88
+ delete settings.migrate_to_newer_schema;
89
+ db = new ueberdb.Database('elasticsearch', settings, {}, logger);
90
+ await assert.rejects(db.init(), /migrate_to_newer_schema/);
91
+ });
92
+
93
+ it('migration enabled', async function () {
94
+ const settings = {base_index, ...cfg, migrate_to_newer_schema: true};
95
+ db = new ueberdb.Database('elasticsearch', settings, {}, logger);
96
+ await db.init();
97
+ await Promise.all([...data].map(async ([k, v]) => {
98
+ assert.deepEqual(await db.get(k), v);
99
+ }));
100
+ });
101
+
102
+ it('each attempt uses a new index', async function () {
103
+ await setOld('a-x:b:c-x:d', 'v'); // Force a conversion failure.
104
+ const settings = {base_index, ...cfg, migrate_to_newer_schema: true};
105
+ db = new ueberdb.Database('elasticsearch', settings, {}, logger);
106
+ const getIndices =
107
+ async () => Object.keys((await client.indices.get({index: `${base_index}_s2*`})).body);
108
+ assert.deepEqual(await getIndices(), []);
109
+ await assert.rejects(db.init(), /ambig/);
110
+ assert.deepEqual(await getIndices(), [`${base_index}_s2_migrate_attempt_0`]);
111
+ await assert.rejects(db.init(), /ambig/);
112
+ assert.deepEqual((await getIndices()).sort(), [
113
+ `${base_index}_s2_migrate_attempt_0`,
114
+ `${base_index}_s2_migrate_attempt_1`,
115
+ ]);
116
+ });
117
+
118
+ it('final name not created until success', async function () {
119
+ });
120
+
121
+ describe('ambiguous key', function () {
122
+ for (const k of ['a:b:c-x:d', 'a-x:b:c:d', 'a-x:b:c-x:d']) {
123
+ it(k, async function () {
124
+ await setOld(k, 'v');
125
+ const settings = {base_index, ...cfg, migrate_to_newer_schema: true};
126
+ db = new ueberdb.Database('elasticsearch', settings, {}, logger);
127
+ await assert.rejects(db.init(), /ambig/);
128
+ });
129
+ }
130
+ });
131
+ });
132
+ });
133
+ });