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 +27 -0
- package/.github/workflows/npmpublish.yml +6 -0
- package/CHANGELOG.md +26 -0
- package/databases/elasticsearch_db.js +156 -196
- package/databases/mssql_db.js +1 -1
- package/lib/CacheAndBufferLayer.js +6 -4
- package/package.json +13 -58
- package/test/lib/databases.js +6 -0
- package/test/test.js +2 -2
- package/test/test_elasticsearch.js +133 -0
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
|
+
};
|
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
|
|
19
|
+
const assert = require('assert').strict;
|
|
20
|
+
const {Buffer} = require('buffer');
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
const es = require('elasticsearch7');
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
const elasticsearchSettings = {
|
|
23
|
-
hostname: '127.0.0.1',
|
|
24
|
-
port: '9200',
|
|
25
|
-
base_index: 'ueberes',
|
|
24
|
+
const schema = '2';
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
elasticsearchSettings.base_index = this.settings.base_index;
|
|
52
|
-
}
|
|
107
|
+
get isAsync() { return true; }
|
|
53
108
|
|
|
54
|
-
|
|
55
|
-
|
|
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(
|
|
119
|
+
async init() {
|
|
64
120
|
// create elasticsearch client
|
|
65
|
-
client = new es.Client({
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
140
|
-
*
|
|
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
|
|
148
|
-
|
|
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
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
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 (
|
|
194
|
-
|
|
195
|
-
|
|
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:
|
|
204
|
-
operations.push({
|
|
231
|
+
operations.push({index: {_id: keyToId(key)}});
|
|
232
|
+
operations.push({key, value});
|
|
205
233
|
break;
|
|
206
234
|
case 'remove':
|
|
207
|
-
operations.push({delete:
|
|
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
|
-
|
|
278
|
-
|
|
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
|
};
|
package/databases/mssql_db.js
CHANGED
|
@@ -622,10 +622,12 @@ exports.Database = class {
|
|
|
622
622
|
}
|
|
623
623
|
if (success) entries.forEach((entry) => markDone(entry, null));
|
|
624
624
|
}
|
|
625
|
-
//
|
|
626
|
-
// this
|
|
627
|
-
//
|
|
628
|
-
//
|
|
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.
|
|
24
|
+
"async": "^3.2.3",
|
|
25
25
|
"cassandra-driver": "^4.6.3",
|
|
26
26
|
"dirty": "^1.1.3",
|
|
27
|
-
"
|
|
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": "^
|
|
32
|
-
"pg": "^8.7.
|
|
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.
|
|
35
|
+
"simple-git": "^3.7.1"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"sqlite3": "
|
|
38
|
+
"sqlite3": "^5.0.6"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"cli-table": "^0.3.
|
|
42
|
-
"eslint": "^
|
|
43
|
-
"eslint-config-etherpad": "^
|
|
44
|
-
"
|
|
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": "
|
|
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": "
|
|
75
|
+
"node": ">=14.15.0"
|
|
121
76
|
}
|
|
122
77
|
}
|
package/test/lib/databases.js
CHANGED
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(
|
|
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
|
+
});
|