ueberdb2 2.2.3 → 4.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 +8 -2
- package/CHANGELOG.md +57 -0
- package/README.md +2 -10
- package/databases/elasticsearch_db.js +156 -196
- package/databases/mssql_db.js +1 -1
- package/databases/redis_db.js +37 -69
- package/lib/AbstractDatabase.js +7 -7
- package/lib/CacheAndBufferLayer.js +6 -4
- package/package.json +14 -59
- package/test/lib/databases.js +6 -0
- package/test/test.js +2 -3
- 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
|
+
};
|
|
@@ -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:
|
|
@@ -56,7 +62,7 @@ jobs:
|
|
|
56
62
|
- uses: actions/checkout@v3
|
|
57
63
|
- uses: actions/setup-node@v3
|
|
58
64
|
with:
|
|
59
|
-
node-version:
|
|
65
|
+
node-version: 14
|
|
60
66
|
- run: npm ci
|
|
61
67
|
# Optional dependencies must be installed manually.
|
|
62
68
|
- run: npm i sqlite3
|
|
@@ -86,7 +92,7 @@ jobs:
|
|
|
86
92
|
-
|
|
87
93
|
uses: actions/setup-node@v3
|
|
88
94
|
with:
|
|
89
|
-
node-version:
|
|
95
|
+
node-version: 14
|
|
90
96
|
registry-url: https://registry.npmjs.org/
|
|
91
97
|
cache: 'npm'
|
|
92
98
|
# This is required if the package has a prepare script that uses something
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# Notable Changes
|
|
2
2
|
|
|
3
|
+
## v4.0.0
|
|
4
|
+
|
|
5
|
+
Compatibility changes:
|
|
6
|
+
|
|
7
|
+
* `redis`: The `socket` and `client_options` settings, deprecated since
|
|
8
|
+
v1.3.1, have been removed.
|
|
9
|
+
* `redis`: The client configuration object has changed with the new version of
|
|
10
|
+
the `redis` client library. See the [`redis` client library
|
|
11
|
+
documentation](https://github.com/redis/node-redis/blob/redis%404.1.0/docs/client-configuration.md)
|
|
12
|
+
for details.
|
|
13
|
+
|
|
14
|
+
Bug fixes:
|
|
15
|
+
|
|
16
|
+
* `redis`: Several `findKeys()` fixes.
|
|
17
|
+
|
|
18
|
+
Updated database dependencies:
|
|
19
|
+
|
|
20
|
+
* `redis`: Updated `redis` from 3.1.2 to 4.1.0.
|
|
21
|
+
|
|
22
|
+
## v3.0.1
|
|
23
|
+
|
|
24
|
+
Bug fixes:
|
|
25
|
+
|
|
26
|
+
* Fixed `findKeys()` calls containing special regular expression characters
|
|
27
|
+
(applicable to the database drivers that use the glob-to-regex helper
|
|
28
|
+
function).
|
|
29
|
+
|
|
30
|
+
## v3.0.0
|
|
31
|
+
|
|
32
|
+
Compatibility changes:
|
|
33
|
+
|
|
34
|
+
* Minimum supported Node.js version is now 14.15.0.
|
|
35
|
+
* `elasticsearch`: New index name and mapping (schema). To automatically copy
|
|
36
|
+
existing data to the new index when the ueberdb client is initialized, set
|
|
37
|
+
the `migrate_to_newer_schema` option to `true`.
|
|
38
|
+
* As mentioned in the v2.2.0 changes, passing callbacks to the database
|
|
39
|
+
methods is deprecated. Use the returned Promises instead.
|
|
40
|
+
* `postgrespool`: As mentioned in the v1.4.15 changes, `postgrespool` is
|
|
41
|
+
deprecated. Use `postgres` instead.
|
|
42
|
+
* `redis`: As mentioned in the v1.3.1 changes, the `socket` and
|
|
43
|
+
`client_options` settings are deprecated. Pass the [client options
|
|
44
|
+
object](https://www.npmjs.com/package/redis/v/3.1.2#options-object-properties)
|
|
45
|
+
directly.
|
|
46
|
+
|
|
47
|
+
Bug fixes:
|
|
48
|
+
|
|
49
|
+
* `elasticsearch`: Rewrote driver to fix numerous bugs and modernize the code.
|
|
50
|
+
|
|
51
|
+
Updated database dependencies:
|
|
52
|
+
|
|
53
|
+
* `couch`: Updated `nano` to 10.0.0.
|
|
54
|
+
* `dirty_git`: Updated `simple-git` to 3.7.1.
|
|
55
|
+
* `elasticsearch`: Switched the client library from the deprecated
|
|
56
|
+
`elasticsearch` to `@elastic/elasticsearch` version 7.17.0.
|
|
57
|
+
* `postgres`: Updated `pg` to 8.7.3.
|
|
58
|
+
* `sqlite`: Updated `sqlite3` to 5.0.6.
|
|
59
|
+
|
|
3
60
|
## v2.2.0
|
|
4
61
|
|
|
5
62
|
Compatibility changes:
|
package/README.md
CHANGED
|
@@ -272,16 +272,8 @@ You should create your database as utf8mb4_bin.
|
|
|
272
272
|
If you enabled TLS on your Redis database (available since Redis 6.0) you will
|
|
273
273
|
need to change your connections parameters, here is an example:
|
|
274
274
|
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
{
|
|
278
|
-
host:
|
|
279
|
-
port: rediss://<redis_database_address>:<redis_database_port>
|
|
280
|
-
socket:
|
|
281
|
-
database:
|
|
282
|
-
password:
|
|
283
|
-
client_options
|
|
284
|
-
}
|
|
275
|
+
```javascript
|
|
276
|
+
const db = new ueberdb.Database('redis', {url: 'rediss://localhost'});
|
|
285
277
|
```
|
|
286
278
|
|
|
287
279
|
Do not provide a `host` value.
|
|
@@ -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
package/databases/redis_db.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
/* eslint new-cap: ["error", {"capIsNewExceptions": ["KEYS", "SMEMBERS"]}] */
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* 2011 Peter 'Pita' Martischka
|
|
@@ -18,111 +17,80 @@
|
|
|
18
17
|
*/
|
|
19
18
|
|
|
20
19
|
const AbstractDatabase = require('../lib/AbstractDatabase');
|
|
21
|
-
const async = require('async');
|
|
22
20
|
const redis = require('redis');
|
|
23
21
|
|
|
24
22
|
exports.Database = class extends AbstractDatabase {
|
|
25
23
|
constructor(settings) {
|
|
26
24
|
super();
|
|
27
|
-
this.
|
|
25
|
+
this._client = null;
|
|
28
26
|
this.settings = settings || {};
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
if (!this.settings.password) return callback();
|
|
33
|
-
this.client.auth(this.settings.password, callback);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
select(callback) {
|
|
37
|
-
if (!this.settings.database) return callback();
|
|
38
|
-
this.client.select(this.settings.database, callback);
|
|
39
|
-
}
|
|
29
|
+
get isAsync() { return true; }
|
|
40
30
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this.settings.client_options);
|
|
46
|
-
} else {
|
|
47
|
-
// Deprecated, but kept for backwards compatibility.
|
|
48
|
-
this.client = redis.createClient(this.settings.port,
|
|
49
|
-
this.settings.host, this.settings.client_options);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
this.client.database = this.settings.database;
|
|
53
|
-
async.waterfall([this.auth.bind(this), this.select.bind(this)], callback);
|
|
31
|
+
async init() {
|
|
32
|
+
this._client = redis.createClient(this.settings);
|
|
33
|
+
await this._client.connect();
|
|
34
|
+
await this._client.ping();
|
|
54
35
|
}
|
|
55
36
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.client = redis.createClient(this.settings);
|
|
59
|
-
callback();
|
|
37
|
+
async get(key) {
|
|
38
|
+
return await this._client.get(key);
|
|
60
39
|
}
|
|
61
40
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
} else if (notKey === '*:*:*') {
|
|
73
|
-
// restrict key to format "text:*"
|
|
74
|
-
const matches = /^([^:]+):\*$/.exec(key);
|
|
75
|
-
if (matches) {
|
|
76
|
-
this.client.SMEMBERS(`ueberDB:keys:${matches[1]}`, callback);
|
|
77
|
-
} else {
|
|
78
|
-
const msg = 'redis db only supports key patterns like pad:* when notKey is set to *:*:*';
|
|
79
|
-
callback(new Error(msg), null);
|
|
80
|
-
}
|
|
81
|
-
} else {
|
|
82
|
-
callback(new Error('redis db currently only supports *:*:* as notKey'), null);
|
|
41
|
+
async findKeys(key, notKey) {
|
|
42
|
+
const [type] = /^([^:*]+):\*$/.exec(key) || [];
|
|
43
|
+
if (type != null && ['*:*:*', `${key}:*`].includes(notKey)) {
|
|
44
|
+
// Performance optimization for a common Etherpad case.
|
|
45
|
+
return await this._client.sMembers(`ueberDB:keys:${type}`);
|
|
46
|
+
}
|
|
47
|
+
let keys = await this._client.keys(key.replace(/[?[\]\\]/g, '\\$&'));
|
|
48
|
+
if (notKey != null) {
|
|
49
|
+
const regex = this.createFindRegex(key, notKey);
|
|
50
|
+
keys = keys.filter((k) => regex.test(k));
|
|
83
51
|
}
|
|
52
|
+
return keys;
|
|
84
53
|
}
|
|
85
54
|
|
|
86
|
-
set(key, value
|
|
55
|
+
async set(key, value) {
|
|
87
56
|
const matches = /^([^:]+):([^:]+)$/.exec(key);
|
|
88
|
-
|
|
89
|
-
this.
|
|
90
|
-
|
|
91
|
-
|
|
57
|
+
await Promise.all([
|
|
58
|
+
matches && this._client.sAdd(`ueberDB:keys:${matches[1]}`, matches[0]),
|
|
59
|
+
this._client.set(key, value),
|
|
60
|
+
]);
|
|
92
61
|
}
|
|
93
62
|
|
|
94
|
-
remove(key
|
|
63
|
+
async remove(key) {
|
|
95
64
|
const matches = /^([^:]+):([^:]+)$/.exec(key);
|
|
96
|
-
|
|
97
|
-
this.
|
|
98
|
-
|
|
99
|
-
|
|
65
|
+
await Promise.all([
|
|
66
|
+
matches && this._client.sRem(`ueberDB:keys:${matches[1]}`, matches[0]),
|
|
67
|
+
this._client.del(key),
|
|
68
|
+
]);
|
|
100
69
|
}
|
|
101
70
|
|
|
102
|
-
doBulk(bulk
|
|
103
|
-
const multi = this.
|
|
71
|
+
async doBulk(bulk) {
|
|
72
|
+
const multi = this._client.multi();
|
|
104
73
|
|
|
105
74
|
for (const {key, type, value} of bulk) {
|
|
106
75
|
const matches = /^([^:]+):([^:]+)$/.exec(key);
|
|
107
76
|
if (type === 'set') {
|
|
108
77
|
if (matches) {
|
|
109
|
-
multi.
|
|
78
|
+
multi.sAdd(`ueberDB:keys:${matches[1]}`, matches[0]);
|
|
110
79
|
}
|
|
111
80
|
multi.set(key, value);
|
|
112
81
|
} else if (type === 'remove') {
|
|
113
82
|
if (matches) {
|
|
114
|
-
multi.
|
|
83
|
+
multi.sRem(`ueberDB:keys:${matches[1]}`, matches[0]);
|
|
115
84
|
}
|
|
116
85
|
multi.del(key);
|
|
117
86
|
}
|
|
118
87
|
}
|
|
119
88
|
|
|
120
|
-
multi.exec(
|
|
89
|
+
await multi.exec();
|
|
121
90
|
}
|
|
122
91
|
|
|
123
|
-
close(
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
});
|
|
92
|
+
async close() {
|
|
93
|
+
await this._client.quit();
|
|
94
|
+
this._client = null;
|
|
127
95
|
}
|
|
128
96
|
};
|
package/lib/AbstractDatabase.js
CHANGED
|
@@ -4,6 +4,11 @@ const logging = require('./logging');
|
|
|
4
4
|
|
|
5
5
|
const nullLogger = logging.normalizeLogger(null);
|
|
6
6
|
|
|
7
|
+
// Format: All characters match themselves except * matches any zero or more characters. No
|
|
8
|
+
// backslash escaping is supported, so it is impossible to create a pattern that matches only the
|
|
9
|
+
// '*' character.
|
|
10
|
+
const simpleGlobToRegExp = (s) => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
11
|
+
|
|
7
12
|
module.exports = class AbstractDatabase {
|
|
8
13
|
constructor() {
|
|
9
14
|
if (new.target === module.exports) {
|
|
@@ -19,13 +24,8 @@ module.exports = class AbstractDatabase {
|
|
|
19
24
|
* For findKey regex. Used by document dbs like mongodb or dirty.
|
|
20
25
|
*/
|
|
21
26
|
createFindRegex(key, notKey) {
|
|
22
|
-
let regex =
|
|
23
|
-
|
|
24
|
-
regex = `(?=^${key}$)`;
|
|
25
|
-
if (notKey != null) {
|
|
26
|
-
notKey = notKey.replace(/\*/g, '.*');
|
|
27
|
-
regex += `(?!${notKey}$)`;
|
|
28
|
-
}
|
|
27
|
+
let regex = `^(?=${simpleGlobToRegExp(key)}$)`;
|
|
28
|
+
if (notKey != null) regex += `(?!${simpleGlobToRegExp(notKey)}$)`;
|
|
29
29
|
return new RegExp(regex);
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -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.
|
|
33
|
-
"redis": "^
|
|
31
|
+
"nano": "^10.0.0",
|
|
32
|
+
"pg": "^8.7.3",
|
|
33
|
+
"redis": "^4.1.0",
|
|
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": "4.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
|
|
|
@@ -142,7 +142,6 @@ describe(__filename, function () {
|
|
|
142
142
|
|
|
143
143
|
it('findKeys with exclusion works', async function () {
|
|
144
144
|
if (database === 'mongodb') this.skip(); // TODO: Fix mongodb.
|
|
145
|
-
if (database === 'redis') this.skip(); // TODO: Fix redis.
|
|
146
145
|
const key = new Randexp(/([a-z]\w{0,20})foo\1/).gen();
|
|
147
146
|
await Promise.all([
|
|
148
147
|
db.set(key, true),
|
|
@@ -237,7 +236,7 @@ describe(__filename, function () {
|
|
|
237
236
|
});
|
|
238
237
|
|
|
239
238
|
it('speed is acceptable', async function () {
|
|
240
|
-
this.timeout(
|
|
239
|
+
this.timeout(180000);
|
|
241
240
|
|
|
242
241
|
const {speeds: {
|
|
243
242
|
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
|
+
});
|