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/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,5 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ Please email contact@etherpad.org to report security related issues.
@@ -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
+ };