ueberdb2 1.4.16
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/.github/workflows/npmpublish.yml +103 -0
- package/.travis.yml +46 -0
- package/CHANGELOG.md +167 -0
- package/CONTRIBUTING.md +103 -0
- package/LICENSE +202 -0
- package/README.md +356 -0
- package/SECURITY.md +5 -0
- package/databases/cassandra_db.js +250 -0
- package/databases/couch_db.js +201 -0
- package/databases/dirty_db.js +80 -0
- package/databases/dirty_git_db.js +78 -0
- package/databases/elasticsearch_db.js +288 -0
- package/databases/mock_db.js +42 -0
- package/databases/mongodb_db.js +136 -0
- package/databases/mssql_db.js +218 -0
- package/databases/mysql_db.js +178 -0
- package/databases/postgres_db.js +198 -0
- package/databases/postgrespool_db.js +11 -0
- package/databases/redis_db.js +128 -0
- package/databases/rethink_db.js +98 -0
- package/databases/sqlite_db.js +158 -0
- package/index.js +191 -0
- package/lib/AbstractDatabase.js +32 -0
- package/lib/CacheAndBufferLayer.js +610 -0
- package/package.json +122 -0
- package/test/lib/databases.js +62 -0
- package/test/lib/mysql.sql +84 -0
- package/test/test.js +312 -0
- package/test/test_bulk.js +71 -0
- package/test/test_lru.js +145 -0
- package/test/test_metrics.js +733 -0
- package/test/test_mysql.js +68 -0
- package/test/test_postgres.js +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# UeberDB2: Abstract your databases
|
|
2
|
+
|
|
3
|
+
## About
|
|
4
|
+
|
|
5
|
+
✓ UeberDB turns every database into a simple key value store by providing a
|
|
6
|
+
layer of abstraction between your software and your database.
|
|
7
|
+
|
|
8
|
+
✓ UeberDB uses a cache and buffer to make databases faster. Reads are cached and
|
|
9
|
+
writes are done in a bulk. This can be turned off.
|
|
10
|
+
|
|
11
|
+
✓ UeberDB does bulk writing ergo reduces the overhead of database transactions.
|
|
12
|
+
|
|
13
|
+
✓ UeberDB uses a simple and clean syntax ergo getting started is easy.
|
|
14
|
+
|
|
15
|
+
## Database Support
|
|
16
|
+
|
|
17
|
+
* Couch
|
|
18
|
+
* Dirty
|
|
19
|
+
* Elasticsearch
|
|
20
|
+
* Maria
|
|
21
|
+
* Mongo
|
|
22
|
+
* MsSQL
|
|
23
|
+
* MySQL
|
|
24
|
+
* Postgres (single connection and with connection pool)
|
|
25
|
+
* Redis
|
|
26
|
+
* Rethink
|
|
27
|
+
* SQLite
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
npm install ueberdb2
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
### Basic
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
const ueberdb = require('ueberdb2');
|
|
41
|
+
|
|
42
|
+
// mysql
|
|
43
|
+
const db = new ueberdb.Database('mysql', {
|
|
44
|
+
user: 'root',
|
|
45
|
+
host: 'localhost',
|
|
46
|
+
password: '',
|
|
47
|
+
database: 'store',
|
|
48
|
+
engine: 'InnoDB',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// dirty to file system
|
|
52
|
+
//const db = new ueberdb.Database('dirty', {filename: 'var/dirty.db'});
|
|
53
|
+
|
|
54
|
+
async function example(db) {
|
|
55
|
+
await db.init();
|
|
56
|
+
|
|
57
|
+
// no need for await because it's already in cache.
|
|
58
|
+
db.set('valueA', {a: 1, b: 2});
|
|
59
|
+
|
|
60
|
+
db.get('valueA', function (err, value) {
|
|
61
|
+
// close the database connection.
|
|
62
|
+
db.close(function () {
|
|
63
|
+
process.exit(0);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
example(db);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### findKeys
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
const ueberdb = require('ueberdb2');
|
|
75
|
+
const db = new ueberdb.Database('dirty', {filename: 'var/dirty.db'});
|
|
76
|
+
|
|
77
|
+
async function example(db){
|
|
78
|
+
await db.init();
|
|
79
|
+
|
|
80
|
+
// no need for await because it's already in cache.
|
|
81
|
+
db.set('valueA', {a: 1, b: 2});
|
|
82
|
+
db.set('valueA:h1', {a: 1, b: 2});
|
|
83
|
+
db.set('valueA:h2', {a: 3, b: 4});
|
|
84
|
+
|
|
85
|
+
db.findKeys('valueA:*', null, function (err, value) { // TODO: Check this
|
|
86
|
+
// value will be ['valueA:h1', 'valueA:h2']
|
|
87
|
+
db.close(function () {
|
|
88
|
+
process.exit(0);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
example(db);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Getting and setting subkeys
|
|
97
|
+
|
|
98
|
+
ueberDB can store complex JSON objects. Sometimes you only want to get or set a
|
|
99
|
+
specific (sub-)property of the stored object. The `.getSub()` and `.setSub()`
|
|
100
|
+
methods make this easier.
|
|
101
|
+
|
|
102
|
+
#### `getSub`
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
db.getSub(key, propertyPath, callback);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Fetches the object stored at `key`, walks the property path given in
|
|
109
|
+
`propertyPath`, and returns the value at that location. `propertyPath` must be
|
|
110
|
+
an array. If `propertyPath` is an empty array then `getSub()` is equivalent to
|
|
111
|
+
`get()`. Returns a nullish value (`null` or `undefined`) if the record does not
|
|
112
|
+
exist or if the given property path does not exist.
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
db.set(key, {prop1: {prop2: ['value']}}, (err) => {
|
|
118
|
+
if (err != null) throw err;
|
|
119
|
+
|
|
120
|
+
db.getSub(key, ['prop1', 'prop2', '0'], (err, val) => {
|
|
121
|
+
if (err != null) throw err;
|
|
122
|
+
console.log('1.', val); // prints "1. value"
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
db.getSub(key, ['prop1', 'prop2'], (err, val) => {
|
|
126
|
+
if (err != null) throw err;
|
|
127
|
+
console.log('2.', val); // prints "2. [ 'value' ]"
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
db.getSub(key, ['prop1'], (err, val) => {
|
|
131
|
+
if (err != null) throw err;
|
|
132
|
+
console.log('3.', val); // prints "3. { prop2: [ 'value' ] }"
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
db.getSub(key, [], (err, val) => {
|
|
136
|
+
if (err != null) throw err;
|
|
137
|
+
console.log('4.', val); // prints "4. { prop1: { prop2: [ 'value' ] } }"
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
db.getSub(key, ['does', 'not', 'exist'], (err, val) => {
|
|
141
|
+
if (err != null) throw err;
|
|
142
|
+
console.log('5.', val); // prints "5. null" or "5. undefined"
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### `setSub`
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
db.setSub(key, propertyPath, value, cb);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Fetches the object stored at `key`, walks the property path given in
|
|
154
|
+
`propertyPath`, and sets the value at that location to `value`. `propertyPath`
|
|
155
|
+
must be an array. If `propertyPath` is an empty array then `setSub()` is
|
|
156
|
+
equivalent to `set()`. Empty objects are created as needed if the property path
|
|
157
|
+
does not exist (including if `key` does not exist in the database). It is an
|
|
158
|
+
error to attempt to set a property on a non-object. `cb` is optional and is
|
|
159
|
+
called when the database driver has reported that the change has been written.
|
|
160
|
+
|
|
161
|
+
Examples:
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
// Assumption: The database does not yet have any records.
|
|
165
|
+
|
|
166
|
+
// Equivalent to db.set('key1', 'value', cb):
|
|
167
|
+
db.setSub('key1', [], 'value', cb);
|
|
168
|
+
|
|
169
|
+
// Equivalent to db.set('key2', {prop1: {prop2: {0: 'value'}}}, cb):
|
|
170
|
+
db.setSub('key2', ['prop1', 'prop2', '0'], 'value', cb):
|
|
171
|
+
|
|
172
|
+
db.set('key3', {prop1: 'value'}, (err) => {
|
|
173
|
+
if (err != null) return cb(err);
|
|
174
|
+
// Equivalent to db.set('key3', {prop1: 'value', prop2: 'other value'}, cb):
|
|
175
|
+
db.setSub('key3', ['prop2'], 'other value', cb);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
db.set('key3', {prop1: 'value'}, (err) => {
|
|
179
|
+
if (err != null) return cb(err);
|
|
180
|
+
// TypeError: Cannot set property "badProp" on non-object "value":
|
|
181
|
+
db.setSub('key3', ['prop1', 'badProp'], 'foo', cb);
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Disable the read cache
|
|
186
|
+
|
|
187
|
+
Set the `cache` wrapper option to 0 to force every read operation to go directly
|
|
188
|
+
to the database driver (except for reads of written values that have not yet
|
|
189
|
+
been committed to the database):
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
const ueberdb = require('ueberdb2');
|
|
193
|
+
|
|
194
|
+
(async () => {
|
|
195
|
+
const db = new ueberdb.Database(
|
|
196
|
+
'dirty', {filename: 'var/dirty.db'}, {cache: 0});
|
|
197
|
+
await db.init();
|
|
198
|
+
db.set('valueA', {a: 1, b: 2});
|
|
199
|
+
db.get('valueA', (err, value) => {
|
|
200
|
+
console.log(JSON.stringify(value));
|
|
201
|
+
db.close(() => {
|
|
202
|
+
process.exit(0);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
})();
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Disable write buffering
|
|
209
|
+
|
|
210
|
+
Set the `writeInterval` wrapper option to 0 to force writes to go directly to
|
|
211
|
+
the database driver:
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
const ueberdb = require('ueberdb2');
|
|
215
|
+
|
|
216
|
+
(async () => {
|
|
217
|
+
const db = new ueberdb.Database(
|
|
218
|
+
'dirty', {filename: 'var/dirty.db'}, {writeInterval: 0});
|
|
219
|
+
await db.init();
|
|
220
|
+
db.set('valueA', {a: 1, b: 2});
|
|
221
|
+
db.get('valueA', (err, value) => {
|
|
222
|
+
console.log(JSON.stringify(value));
|
|
223
|
+
db.close(() => {
|
|
224
|
+
process.exit(0);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
})();
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Feature support
|
|
231
|
+
|
|
232
|
+
| | Get | Set | findKeys | Remove | getSub | setSub | doBulk |CI Coverage|
|
|
233
|
+
|--------|-----|-----|----------|--------|--------|--------|--------|--------|
|
|
234
|
+
| cassandra | ✓ | ✓ | * | ✓ | ✓ | ✓ | ✓ |
|
|
235
|
+
| couchdb | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
236
|
+
| dirty | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | ✓ |
|
|
237
|
+
| dirty_git | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
|
|
238
|
+
| elasticsearch | ✓ | ✓ | * | ✓ | ✓ | ✓ | ✓ |
|
|
239
|
+
| maria | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
240
|
+
| mysql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
241
|
+
| postgres | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
242
|
+
| redis | ✓ | ✓ | * | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
243
|
+
| rethinkdb | ✓ | ✓ | * | ✓ | ✓ | ✓ | ✓ |
|
|
244
|
+
| sqlite | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
245
|
+
|
|
246
|
+
## Limitations
|
|
247
|
+
|
|
248
|
+
### findKeys query support
|
|
249
|
+
|
|
250
|
+
The following characters should be avoided in keys `\^$.|?*+()[{` as they will
|
|
251
|
+
cause findKeys to fail.
|
|
252
|
+
|
|
253
|
+
### findKeys database support*
|
|
254
|
+
|
|
255
|
+
The following have limitations on findKeys
|
|
256
|
+
|
|
257
|
+
* redis (Only keys of the format \*:\*:\*)
|
|
258
|
+
* cassandra (Only keys of the format \*:\*:\*)
|
|
259
|
+
* elasticsearch (Only keys of the format \*:\*:\*)
|
|
260
|
+
* rethink (Currently doesn't work)
|
|
261
|
+
|
|
262
|
+
For details on how it works please refer to the wiki:
|
|
263
|
+
https://github.com/ether/UeberDB/wiki/findKeys-functionality
|
|
264
|
+
|
|
265
|
+
### Scaling, High availability and disaster recovery.
|
|
266
|
+
|
|
267
|
+
To scale UeberDB you should use sharding especially for real time applications.
|
|
268
|
+
An example of this is sharding given Pads within Etherpad based on their initial
|
|
269
|
+
pad authors geographical location. High availability and disaster recovery can
|
|
270
|
+
be provided through replication of your database however YMMV on passing
|
|
271
|
+
Settings to your database library. Do not be under the illusion that UeberDB
|
|
272
|
+
provides any Stateless capabilities, it does not. An option is to use something
|
|
273
|
+
like rethinkdb and set cache to 0 but YMMV.
|
|
274
|
+
|
|
275
|
+
### Key Length Restrictions
|
|
276
|
+
|
|
277
|
+
Your Key Length will be limited by the database you chose to use but keep into
|
|
278
|
+
account portability within your application.
|
|
279
|
+
|
|
280
|
+
### doBulk operations on .set out of memory
|
|
281
|
+
|
|
282
|
+
doBulk operations that chain IE a large number of .set without a pause to handle
|
|
283
|
+
the channel clearance can cause a `Javascript out of heap memory`. It's very
|
|
284
|
+
rare this happens and is usually due to a bug in software causing a constant
|
|
285
|
+
write to the database.
|
|
286
|
+
|
|
287
|
+
## MySQL /MariaDB Advice
|
|
288
|
+
|
|
289
|
+
You should create your database as utf8mb4_bin.
|
|
290
|
+
|
|
291
|
+
## Redis TLS communication
|
|
292
|
+
|
|
293
|
+
If you enabled TLS on your Redis database (available since Redis 6.0) you will
|
|
294
|
+
need to change your connections parameters, here is an example:
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
settings:
|
|
298
|
+
{
|
|
299
|
+
host:
|
|
300
|
+
port: rediss://<redis_database_address>:<redis_database_port>
|
|
301
|
+
socket:
|
|
302
|
+
database:
|
|
303
|
+
password:
|
|
304
|
+
client_options
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Do not provide a `host` value.
|
|
309
|
+
|
|
310
|
+
If you don't provide a certificate on the client side, you need to add the
|
|
311
|
+
environment variable `NODE_TLS_REJECT_UNAUTHORIZED = 0` and add the flag
|
|
312
|
+
`--tls-auth-clients no` when launching the redis-server to accept connections.
|
|
313
|
+
|
|
314
|
+
## How to add support for another database
|
|
315
|
+
|
|
316
|
+
1. Add the database driver to `packages.json`, this will happen automatically if
|
|
317
|
+
you run `npm install %yourdatabase%`
|
|
318
|
+
1. Create `databases/DATABASENAME_db.js` and have it export a `Database` class
|
|
319
|
+
that derives from `lib/AbstractDatabase.js`. Implement the required
|
|
320
|
+
functions.
|
|
321
|
+
1. Add a service for the database to the test job in
|
|
322
|
+
`.github/workflows/npmpublish.yml`.
|
|
323
|
+
1. Add an entry to `test/lib/databases.js` for your database and configure it to
|
|
324
|
+
work with the service added to the GitHub workflow.
|
|
325
|
+
1. Install and start the database server and configure it to work with the
|
|
326
|
+
settings in your `test/lib/databases.js` entry.
|
|
327
|
+
1. Run `npm test` to ensure that it works.
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
[Apache License v2](http://www.apache.org/licenses/LICENSE-2.0.html)
|
|
332
|
+
|
|
333
|
+
## What's changed from UeberDB?
|
|
334
|
+
|
|
335
|
+
* Dropped broken databases: CrateDB, LevelDB, LMDB (probably a
|
|
336
|
+
breaking change for some people)
|
|
337
|
+
* Introduced CI.
|
|
338
|
+
* Introduced better testing.
|
|
339
|
+
* Fixed broken database clients IE Redis.
|
|
340
|
+
* Updated Depdendencies where possible.
|
|
341
|
+
* Tidied file structure.
|
|
342
|
+
* Improved documentation.
|
|
343
|
+
* Sensible name for software makes it clear that it's maintained by The Etherpad
|
|
344
|
+
Foundation.
|
|
345
|
+
* Make db.init await / async
|
|
346
|
+
|
|
347
|
+
### Dirty_Git Easter Egg.
|
|
348
|
+
|
|
349
|
+
* I suck at hiding Easter eggs..
|
|
350
|
+
|
|
351
|
+
Dirty_git will `commit` and `push` to Git on every `set`. To use `git init` or
|
|
352
|
+
`git clone` within your dirty database location and then set your upstream IE
|
|
353
|
+
`git remote add origin git://whztevz`.
|
|
354
|
+
|
|
355
|
+
The logic behind dirty git is that you can still use dirty but you can also have
|
|
356
|
+
offsite backups. It's noisy and spammy but it can be useful.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS-IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const AbstractDatabase = require('../lib/AbstractDatabase');
|
|
17
|
+
const cassandra = require('cassandra-driver');
|
|
18
|
+
|
|
19
|
+
exports.Database = class extends AbstractDatabase {
|
|
20
|
+
/**
|
|
21
|
+
* @param {Object} settings The required settings object to initiate the Cassandra database
|
|
22
|
+
* @param {String[]} settings.clientOptions See
|
|
23
|
+
* http://www.datastax.com/drivers/nodejs/2.0/global.html#ClientOptions for a full set of
|
|
24
|
+
* options that can be used
|
|
25
|
+
* @param {String} settings.columnFamily The column family that should be used to store data. The
|
|
26
|
+
* column family will be created if it doesn't exist
|
|
27
|
+
* @param {Function} [settings.logger] Function that will be used to pass on log events emitted by
|
|
28
|
+
* the Cassandra driver. See https://github.com/datastax/nodejs-driver#logging for more
|
|
29
|
+
* information
|
|
30
|
+
*/
|
|
31
|
+
constructor(settings) {
|
|
32
|
+
super();
|
|
33
|
+
if (!settings.clientOptions) {
|
|
34
|
+
throw new Error('The Cassandra client options should be defined');
|
|
35
|
+
}
|
|
36
|
+
if (!settings.columnFamily) {
|
|
37
|
+
throw new Error('The Cassandra column family should be defined');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.settings = {};
|
|
41
|
+
this.settings.clientOptions = settings.clientOptions;
|
|
42
|
+
this.settings.columnFamily = settings.columnFamily;
|
|
43
|
+
this.settings.logger = settings.logger;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initializes the Cassandra client, connects to Cassandra and creates the CF if it didn't exist
|
|
48
|
+
* already
|
|
49
|
+
*
|
|
50
|
+
* @param {Function} callback Standard callback method.
|
|
51
|
+
* @param {Error} callback.err An error object (if any.)
|
|
52
|
+
*/
|
|
53
|
+
init(callback) {
|
|
54
|
+
// Create a client
|
|
55
|
+
this.client = new cassandra.Client(this.settings.clientOptions);
|
|
56
|
+
|
|
57
|
+
// Pass on log messages if a logger has been configured
|
|
58
|
+
if (this.settings.logger) {
|
|
59
|
+
this.client.on('log', this.settings.logger);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check whether our column family already exists and create it if necessary
|
|
63
|
+
this.client.execute(
|
|
64
|
+
'SELECT columnfamily_name FROM system.schema_columnfamilies WHERE keyspace_name = ?',
|
|
65
|
+
[this.settings.clientOptions.keyspace],
|
|
66
|
+
(err, result) => {
|
|
67
|
+
if (err) {
|
|
68
|
+
return callback(err);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let isDefined = false;
|
|
72
|
+
const length = result.rows.length;
|
|
73
|
+
for (let i = 0; i < length; i++) {
|
|
74
|
+
if (result.rows[i].columnfamily_name === this.settings.columnFamily) {
|
|
75
|
+
isDefined = true;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isDefined) {
|
|
81
|
+
return callback(null);
|
|
82
|
+
} else {
|
|
83
|
+
const cql =
|
|
84
|
+
`CREATE COLUMNFAMILY "${this.settings.columnFamily}" ` +
|
|
85
|
+
'(key text PRIMARY KEY, data text)';
|
|
86
|
+
this.client.execute(cql, callback);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Gets a value from Cassandra
|
|
93
|
+
*
|
|
94
|
+
* @param {String} key The key for which the value should be retrieved
|
|
95
|
+
* @param {Function} callback Standard callback method
|
|
96
|
+
* @param {Error} callback.err An error object, if any
|
|
97
|
+
* @param {String} callback.value The value for the given key (if any)
|
|
98
|
+
*/
|
|
99
|
+
get(key, callback) {
|
|
100
|
+
const cql = `SELECT data FROM "${this.settings.columnFamily}" WHERE key = ?`;
|
|
101
|
+
this.client.execute(cql, [key], (err, result) => {
|
|
102
|
+
if (err) {
|
|
103
|
+
return callback(err);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!result.rows || result.rows.length === 0) {
|
|
107
|
+
return callback(null, null);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return callback(null, result.rows[0].data);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Cassandra has no native `findKeys` method. This function implements a naive filter by
|
|
116
|
+
* retrieving *all* the keys and filtering those. This should obviously be used with the utmost
|
|
117
|
+
* care and is probably not something you want to run in production.
|
|
118
|
+
*
|
|
119
|
+
* @param {String} key The filter for keys that should match
|
|
120
|
+
* @param {String} [notKey] The filter for keys that shouldn't match
|
|
121
|
+
* @param {Function} callback Standard callback method
|
|
122
|
+
* @param {Error} callback.err An error object, if any
|
|
123
|
+
* @param {String[]} callback.keys An array of keys that match the specified filters
|
|
124
|
+
*/
|
|
125
|
+
findKeys(key, notKey, callback) {
|
|
126
|
+
let cql = null;
|
|
127
|
+
if (!notKey) {
|
|
128
|
+
// Get all the keys
|
|
129
|
+
cql = `SELECT key FROM "${this.settings.columnFamily}"`;
|
|
130
|
+
this.client.execute(cql, (err, result) => {
|
|
131
|
+
if (err) {
|
|
132
|
+
return callback(err);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Construct a regular expression based on the given key
|
|
136
|
+
const regex = new RegExp(`^${key.replace(/\*/g, '.*')}$`);
|
|
137
|
+
|
|
138
|
+
const keys = [];
|
|
139
|
+
result.rows.forEach((row) => {
|
|
140
|
+
if (regex.test(row.key)) {
|
|
141
|
+
keys.push(row.key);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return callback(null, keys);
|
|
146
|
+
});
|
|
147
|
+
} else if (notKey === '*:*:*') {
|
|
148
|
+
// restrict key to format 'text:*'
|
|
149
|
+
const matches = /^([^:]+):\*$/.exec(key);
|
|
150
|
+
if (matches) {
|
|
151
|
+
// Get the 'text' bit out of the key and get all those keys from a special column.
|
|
152
|
+
// We can retrieve them from this column as we're duplicating them on .set/.remove
|
|
153
|
+
cql = `SELECT * from "${this.settings.columnFamily}" WHERE key = ?`;
|
|
154
|
+
this.client.execute(cql, [`ueberdb:keys:${matches[1]}`], (err, result) => {
|
|
155
|
+
if (err) {
|
|
156
|
+
return callback(err);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!result.rows || result.rows.length === 0) {
|
|
160
|
+
return callback(null, []);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const keys = result.rows.map((row) => row.data);
|
|
164
|
+
return callback(null, keys);
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
const msg =
|
|
168
|
+
'Cassandra db only supports key patterns like pad:* when notKey is set to *:*:*';
|
|
169
|
+
return callback(new Error(msg), null);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
return callback(new Error('Cassandra db currently only supports *:*:* as notKey'), null);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Sets a value for a key
|
|
178
|
+
*
|
|
179
|
+
* @param {String} key The key to set
|
|
180
|
+
* @param {String} value The value associated to this key
|
|
181
|
+
* @param {Function} callback Standard callback method
|
|
182
|
+
* @param {Error} callback.err An error object, if any
|
|
183
|
+
*/
|
|
184
|
+
set(key, value, callback) {
|
|
185
|
+
this.doBulk([{type: 'set', key, value}], callback);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Removes a key and it's value from the column family
|
|
190
|
+
*
|
|
191
|
+
* @param {String} key The key to remove
|
|
192
|
+
* @param {Function} callback Standard callback method
|
|
193
|
+
* @param {Error} callback.err An error object, if any
|
|
194
|
+
*/
|
|
195
|
+
remove(key, callback) {
|
|
196
|
+
this.doBulk([{type: 'remove', key}], callback);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Performs multiple operations in one action
|
|
201
|
+
*
|
|
202
|
+
* @param {Object[]} bulk The set of operations that should be performed
|
|
203
|
+
* @param {Function} callback Standard callback method
|
|
204
|
+
* @param {Error} callback.err An error object, if any
|
|
205
|
+
*/
|
|
206
|
+
doBulk(bulk, callback) {
|
|
207
|
+
const queries = [];
|
|
208
|
+
bulk.forEach((operation) => {
|
|
209
|
+
// We support finding keys of the form `test:*`. If anything matches, we will try and save
|
|
210
|
+
// this
|
|
211
|
+
const matches = /^([^:]+):([^:]+)$/.exec(operation.key);
|
|
212
|
+
if (operation.type === 'set') {
|
|
213
|
+
queries.push({
|
|
214
|
+
query: `UPDATE "${this.settings.columnFamily}" SET data = ? WHERE key = ?`,
|
|
215
|
+
params: [operation.value, operation.key],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (matches) {
|
|
219
|
+
queries.push({
|
|
220
|
+
query: `UPDATE "${this.settings.columnFamily}" SET data = ? WHERE key = ?`,
|
|
221
|
+
params: ['1', `ueberdb:keys:${matches[1]}`],
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
} else if (operation.type === 'remove') {
|
|
225
|
+
queries.push({
|
|
226
|
+
query: `DELETE FROM "${this.settings.columnFamily}" WHERE key=?`,
|
|
227
|
+
params: [operation.key],
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (matches) {
|
|
231
|
+
queries.push({
|
|
232
|
+
query: `DELETE FROM "${this.settings.columnFamily}" WHERE key = ?`,
|
|
233
|
+
params: [`ueberdb:keys:${matches[1]}`],
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
this.client.batch(queries, {prepare: true}, callback);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Closes the Cassandra connection
|
|
243
|
+
*
|
|
244
|
+
* @param {Function} callback Standard callback method
|
|
245
|
+
* @param {Error} callback.err Error object in case something goes wrong
|
|
246
|
+
*/
|
|
247
|
+
close(callback) {
|
|
248
|
+
this.pool.shutdown(callback);
|
|
249
|
+
}
|
|
250
|
+
};
|