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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* 2012 Max 'Azul' Wiehle
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS-IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const AbstractDatabase = require('../lib/AbstractDatabase');
|
|
19
|
+
const nano = require('nano');
|
|
20
|
+
const async = require('async');
|
|
21
|
+
|
|
22
|
+
const DESIGN_NAME = 'ueberDb';
|
|
23
|
+
const DESIGN_PATH = `_design/${DESIGN_NAME}`;
|
|
24
|
+
|
|
25
|
+
const handleError = (er) => {
|
|
26
|
+
if (er) throw new Error(er);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
exports.Database = class extends AbstractDatabase {
|
|
30
|
+
constructor(settings) {
|
|
31
|
+
super();
|
|
32
|
+
this.db = null;
|
|
33
|
+
this.client = null;
|
|
34
|
+
this.settings = settings;
|
|
35
|
+
|
|
36
|
+
// force some settings
|
|
37
|
+
// used by CacheAndBufferLayer.js
|
|
38
|
+
this.settings.cache = 1000;
|
|
39
|
+
this.settings.writeInterval = 100;
|
|
40
|
+
this.settings.json = false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
init(callback) {
|
|
44
|
+
const settings = this.settings;
|
|
45
|
+
let client = null;
|
|
46
|
+
let db = null;
|
|
47
|
+
|
|
48
|
+
const config = {
|
|
49
|
+
url: `http://${settings.host}:${settings.port}`,
|
|
50
|
+
requestDefaults: {
|
|
51
|
+
pool: {
|
|
52
|
+
maxSockets: settings.maxListeners || 1,
|
|
53
|
+
},
|
|
54
|
+
auth: {
|
|
55
|
+
user: settings.user,
|
|
56
|
+
pass: settings.password,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const createDb = () => {
|
|
62
|
+
client.db.create(settings.database, (er, body) => {
|
|
63
|
+
if (er) return callback(er);
|
|
64
|
+
return setDb();
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const setDb = () => {
|
|
69
|
+
db = client.use(settings.database);
|
|
70
|
+
checkUeberDbDesignDocument(db);
|
|
71
|
+
this.client = client;
|
|
72
|
+
this.db = db;
|
|
73
|
+
callback();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Always ensure that couchDb has at least an empty design doc for UeberDb use
|
|
77
|
+
// this will be necessary for the `findKeys` method
|
|
78
|
+
const checkUeberDbDesignDocument = () => {
|
|
79
|
+
db.head(DESIGN_PATH, (er, _, header) => {
|
|
80
|
+
if (er && er.statusCode === 404) return db.insert({views: {}}, DESIGN_PATH, handleError);
|
|
81
|
+
if (er) throw new Error(er);
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
client = nano(config);
|
|
86
|
+
client.db.get(settings.database, (er, body) => {
|
|
87
|
+
if (er && er.statusCode === 404) return createDb();
|
|
88
|
+
if (er) return callback(er);
|
|
89
|
+
return setDb();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get(key, callback) {
|
|
94
|
+
const db = this.db;
|
|
95
|
+
db.get(key, (er, doc) => {
|
|
96
|
+
if (er && er.statusCode !== 404) {
|
|
97
|
+
console.log('GET');
|
|
98
|
+
console.log(er);
|
|
99
|
+
}
|
|
100
|
+
if (doc == null) return callback(null, null);
|
|
101
|
+
callback(null, doc.value);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
findKeys(key, notKey, callback) {
|
|
106
|
+
const regex = this.createFindRegex(key, notKey);
|
|
107
|
+
const queryKey = `${key}__${notKey}`;
|
|
108
|
+
const db = this.db;
|
|
109
|
+
|
|
110
|
+
// always look up if the query haven't be done before
|
|
111
|
+
const checkQuery = () => {
|
|
112
|
+
db.get(DESIGN_PATH, (er, doc) => {
|
|
113
|
+
handleError(er);
|
|
114
|
+
const queryExists = queryKey in doc.views;
|
|
115
|
+
if (!queryExists) return createQuery(doc);
|
|
116
|
+
makeQuery();
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Cache the query for faster reuse in the future
|
|
121
|
+
const createQuery = (doc) => {
|
|
122
|
+
const mapFunction = {
|
|
123
|
+
map: `function (doc) { if (${regex}.test(doc._id)) { emit(doc._id, null); } }`,
|
|
124
|
+
};
|
|
125
|
+
doc.views[queryKey] = mapFunction;
|
|
126
|
+
db.insert(doc, DESIGN_PATH, (er) => {
|
|
127
|
+
handleError(er);
|
|
128
|
+
makeQuery();
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// If this is the first time the request is used, this can take a while…
|
|
133
|
+
const makeQuery = (er) => {
|
|
134
|
+
db.view(DESIGN_NAME, queryKey, (er, docs) => {
|
|
135
|
+
handleError(er);
|
|
136
|
+
docs = docs.rows.map((doc) => doc.key);
|
|
137
|
+
callback(null, docs);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
checkQuery();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
set(key, value, callback) {
|
|
145
|
+
const db = this.db;
|
|
146
|
+
db.get(key, (er, doc) => {
|
|
147
|
+
if (doc == null) return db.insert({_id: key, value}, callback);
|
|
148
|
+
db.insert({_id: key, _rev: doc._rev, value}, callback);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
remove(key, callback) {
|
|
153
|
+
const db = this.db;
|
|
154
|
+
db.head(key, (er, _, header) => {
|
|
155
|
+
if (er && er.statusCode === 404) return callback(null);
|
|
156
|
+
if (er) return callback(er);
|
|
157
|
+
// etag has additional quotation marks, remove them
|
|
158
|
+
const etag = JSON.parse(header).etag;
|
|
159
|
+
db.destroy(key, etag, (er, body) => {
|
|
160
|
+
if (er) return callback(er);
|
|
161
|
+
callback(null);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
doBulk(bulk, callback) {
|
|
167
|
+
const db = this.db;
|
|
168
|
+
const keys = bulk.map((op) => op.key);
|
|
169
|
+
const revs = {};
|
|
170
|
+
const setters = [];
|
|
171
|
+
async.series([
|
|
172
|
+
(callback) => {
|
|
173
|
+
db.fetchRevs({keys}, (er, r) => {
|
|
174
|
+
if (er) throw new Error(JSON.stringify(er));
|
|
175
|
+
const rows = r.rows;
|
|
176
|
+
for (const j in r.rows) {
|
|
177
|
+
// couchDB will return error instead of value if key does not exist
|
|
178
|
+
if (rows[j].value != null) revs[rows[j].key] = rows[j].value.rev;
|
|
179
|
+
}
|
|
180
|
+
callback();
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
(callback) => {
|
|
184
|
+
for (const item of bulk) {
|
|
185
|
+
const set = {_id: item.key};
|
|
186
|
+
if (revs[item.key] != null) set._rev = revs[item.key];
|
|
187
|
+
if (item.type === 'set') set.value = item.value;
|
|
188
|
+
if (item.type === 'remove') set._deleted = true;
|
|
189
|
+
setters.push(set);
|
|
190
|
+
}
|
|
191
|
+
callback();
|
|
192
|
+
},
|
|
193
|
+
], (err) => {
|
|
194
|
+
db.bulk({docs: setters}, callback);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
close(callback) {
|
|
199
|
+
if (callback) callback();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* 2011 Peter 'Pita' Martischka
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS-IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
*
|
|
20
|
+
* Fair warning that length may not provide the correct value upon load.
|
|
21
|
+
* See https://github.com/ether/etherpad-lite/pull/3984
|
|
22
|
+
*
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const AbstractDatabase = require('../lib/AbstractDatabase');
|
|
26
|
+
const Dirty = require('dirty');
|
|
27
|
+
|
|
28
|
+
exports.Database = class extends AbstractDatabase {
|
|
29
|
+
constructor(settings) {
|
|
30
|
+
super();
|
|
31
|
+
this.db = null;
|
|
32
|
+
|
|
33
|
+
if (!settings || !settings.filename) {
|
|
34
|
+
settings = {filename: null};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.settings = settings;
|
|
38
|
+
|
|
39
|
+
// set default settings
|
|
40
|
+
this.settings.cache = 0;
|
|
41
|
+
this.settings.writeInterval = 0;
|
|
42
|
+
this.settings.json = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
init(callback) {
|
|
46
|
+
this.db = new Dirty(this.settings.filename);
|
|
47
|
+
this.db.on('load', (err) => {
|
|
48
|
+
callback();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get(key, callback) {
|
|
53
|
+
callback(null, this.db.get(key));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
findKeys(key, notKey, callback) {
|
|
57
|
+
const keys = [];
|
|
58
|
+
const regex = this.createFindRegex(key, notKey);
|
|
59
|
+
this.db.forEach((key, val) => {
|
|
60
|
+
if (key.search(regex) !== -1) {
|
|
61
|
+
keys.push(key);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
callback(null, keys);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
set(key, value, callback) {
|
|
68
|
+
this.db.set(key, value, callback);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
remove(key, callback) {
|
|
72
|
+
this.db.rm(key, callback);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
close(callback) {
|
|
76
|
+
this.db.close();
|
|
77
|
+
this.db = null;
|
|
78
|
+
if (callback) callback();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* 2011 Peter 'Pita' Martischka
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS-IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const AbstractDatabase = require('../lib/AbstractDatabase');
|
|
19
|
+
const Dirty = require('dirty');
|
|
20
|
+
|
|
21
|
+
exports.Database = class extends AbstractDatabase {
|
|
22
|
+
constructor(settings) {
|
|
23
|
+
super();
|
|
24
|
+
this.db = null;
|
|
25
|
+
|
|
26
|
+
if (!settings || !settings.filename) {
|
|
27
|
+
settings = {filename: null};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.settings = settings;
|
|
31
|
+
|
|
32
|
+
// set default settings
|
|
33
|
+
this.settings.cache = 0;
|
|
34
|
+
this.settings.writeInterval = 0;
|
|
35
|
+
this.settings.json = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
init(callback) {
|
|
39
|
+
this.db = new Dirty(this.settings.filename);
|
|
40
|
+
this.db.on('load', (err) => {
|
|
41
|
+
callback();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get(key, callback) {
|
|
46
|
+
callback(null, this.db.get(key));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
findKeys(key, notKey, callback) {
|
|
50
|
+
const keys = [];
|
|
51
|
+
const regex = this.createFindRegex(key, notKey);
|
|
52
|
+
this.db.forEach((key, val) => {
|
|
53
|
+
if (key.search(regex) !== -1) {
|
|
54
|
+
keys.push(key);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
callback(null, keys);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
set(key, value, callback) {
|
|
61
|
+
this.db.set(key, value, callback);
|
|
62
|
+
const databasePath = require('path').dirname(this.settings.filename);
|
|
63
|
+
require('simple-git')(databasePath)
|
|
64
|
+
.silent(true)
|
|
65
|
+
.add('./*.db')
|
|
66
|
+
.commit('Automated commit...')
|
|
67
|
+
.push(['-u', 'origin', 'master'], () => console.debug('Stored git commit'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
remove(key, callback) {
|
|
71
|
+
this.db.rm(key, callback);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
close(callback) {
|
|
75
|
+
this.db.close();
|
|
76
|
+
if (callback) callback();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* 2015 Visionist, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS-IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const AbstractDatabase = require('../lib/AbstractDatabase');
|
|
19
|
+
const es = require('elasticsearch');
|
|
20
|
+
|
|
21
|
+
// initialize w/ default settings
|
|
22
|
+
const elasticsearchSettings = {
|
|
23
|
+
hostname: '127.0.0.1',
|
|
24
|
+
port: '9200',
|
|
25
|
+
base_index: 'ueberes',
|
|
26
|
+
|
|
27
|
+
// for a list of valid API values see:
|
|
28
|
+
// https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/configuration.html#config-options
|
|
29
|
+
api: '7.6',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let client;
|
|
33
|
+
|
|
34
|
+
exports.Database = class extends AbstractDatabase {
|
|
35
|
+
constructor(settings) {
|
|
36
|
+
super();
|
|
37
|
+
this.db = null;
|
|
38
|
+
|
|
39
|
+
this.settings = settings || {};
|
|
40
|
+
|
|
41
|
+
// update settings if they were provided
|
|
42
|
+
if (this.settings.host) {
|
|
43
|
+
elasticsearchSettings.hostname = this.settings.host;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this.settings.port) {
|
|
47
|
+
elasticsearchSettings.port = this.settings.port;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this.settings.base_index) {
|
|
51
|
+
elasticsearchSettings.base_index = this.settings.base_index;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (this.settings.api) {
|
|
55
|
+
elasticsearchSettings.api = this.settings.api;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initialize the elasticsearch client, then ping the server to ensure that a
|
|
61
|
+
* connection was made.
|
|
62
|
+
*/
|
|
63
|
+
init(callback) {
|
|
64
|
+
// create elasticsearch client
|
|
65
|
+
client = new es.Client({
|
|
66
|
+
host: `${elasticsearchSettings.hostname}:${elasticsearchSettings.port}`,
|
|
67
|
+
apiVersion: elasticsearchSettings.api,
|
|
68
|
+
// log: "trace" // useful for debugging
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// test the connection
|
|
72
|
+
client.ping({
|
|
73
|
+
requestTimeout: 3000,
|
|
74
|
+
}, (error) => {
|
|
75
|
+
if (error) {
|
|
76
|
+
console.error('unable to communicate with elasticsearch');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
callback(error);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* This function provides read functionality to the database.
|
|
85
|
+
*
|
|
86
|
+
* @param {String} key Key, of the format "test:test1" or, optionally, of the
|
|
87
|
+
* format "test:test1:check:check1"
|
|
88
|
+
* @param {function} callback Function will be called in the event of an error or
|
|
89
|
+
* upon completion of a successful database retrieval.
|
|
90
|
+
*/
|
|
91
|
+
get(key, callback) {
|
|
92
|
+
client.get(getIndexTypeId(key), (error, response) => {
|
|
93
|
+
parseResponse(error, response, callback);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
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
|
+
* @param key Search key, which uses an asterisk (*) as the wild card.
|
|
109
|
+
* @param notKey Used to filter the result set
|
|
110
|
+
* @param callback First param is error, second is result
|
|
111
|
+
*/
|
|
112
|
+
findKeys(key, notKey, callback) {
|
|
113
|
+
const splitKey = key.split(':');
|
|
114
|
+
|
|
115
|
+
client.search({
|
|
116
|
+
index: elasticsearchSettings.base_index,
|
|
117
|
+
type: splitKey[0],
|
|
118
|
+
size: 100, // this is a pretty random threshold...
|
|
119
|
+
}, (error, response) => {
|
|
120
|
+
if (error) {
|
|
121
|
+
console.error('findkeys', error);
|
|
122
|
+
callback(error);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!error && response.hits) {
|
|
127
|
+
const keys = [];
|
|
128
|
+
for (let counter = 0; counter < response.hits.total; counter++) {
|
|
129
|
+
keys.push(`${splitKey[0]}:${response.hits.hits[counter]._id}`);
|
|
130
|
+
}
|
|
131
|
+
callback(null, keys);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* This function provides write functionality to the database.
|
|
138
|
+
*
|
|
139
|
+
* @param {String} key Key, of the format "test:test1" or, optionally, of the
|
|
140
|
+
* format "test:test1:check:check1"
|
|
141
|
+
* @param {JSON|String} value The value to be stored to the database. The value is
|
|
142
|
+
* always converted to {val:value} before being written to the database, to account
|
|
143
|
+
* for situations where the value is just a string.
|
|
144
|
+
* @param {function} callback Function will be called in the event of an error or on
|
|
145
|
+
* completion of a successful database write.
|
|
146
|
+
*/
|
|
147
|
+
set(key, value, callback) {
|
|
148
|
+
const options = getIndexTypeId(key);
|
|
149
|
+
|
|
150
|
+
options.body = {
|
|
151
|
+
val: value,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
client.index(options, (error, response) => {
|
|
155
|
+
parseResponse(error, response, callback);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* This function provides delete functionality to the database.
|
|
161
|
+
*
|
|
162
|
+
* The index, type, and ID will be parsed from the key, and this document will
|
|
163
|
+
* be deleted from the database.
|
|
164
|
+
*
|
|
165
|
+
* @param {String} key Key, of the format "test:test1" or, optionally, of the
|
|
166
|
+
* format "test:test1:check:check1"
|
|
167
|
+
* @param {function} callback Function will be called in the event of an error or on
|
|
168
|
+
* completion of a successful database write.
|
|
169
|
+
*/
|
|
170
|
+
remove(key, callback) {
|
|
171
|
+
client.delete(key, (error, response) => {
|
|
172
|
+
parseResponse(error, response, callback);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* This uses the bulk upload functionality of elasticsearch (url:port/_bulk).
|
|
178
|
+
*
|
|
179
|
+
* The CacheAndBufferLayer will periodically (every this.settings.writeInterval)
|
|
180
|
+
* flush writes that have already been done in the local cache out to the database.
|
|
181
|
+
*
|
|
182
|
+
* @param {Array} bulk An array of JSON data in the format:
|
|
183
|
+
* {"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
|
+
*/
|
|
187
|
+
doBulk(bulk, callback) {
|
|
188
|
+
// bulk is an array of JSON:
|
|
189
|
+
// example: [{"type":"set", "key":"sessionstorage:{id}", "value":{"cookie":{...}}]
|
|
190
|
+
|
|
191
|
+
const operations = [];
|
|
192
|
+
|
|
193
|
+
for (let counter = 0; counter < bulk.length; counter++) {
|
|
194
|
+
const indexTypeId = getIndexTypeId(bulk[counter].key);
|
|
195
|
+
const operationPayload = {
|
|
196
|
+
_index: indexTypeId.index,
|
|
197
|
+
_type: indexTypeId.type,
|
|
198
|
+
_id: indexTypeId.id,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
switch (bulk[counter].type) {
|
|
202
|
+
case 'set':
|
|
203
|
+
operations.push({index: operationPayload});
|
|
204
|
+
operations.push({val: JSON.parse(bulk[counter].value)});
|
|
205
|
+
break;
|
|
206
|
+
case 'remove':
|
|
207
|
+
operations.push({delete: operationPayload});
|
|
208
|
+
break;
|
|
209
|
+
default:
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
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
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!error && response) {
|
|
278
|
+
response = response._source;
|
|
279
|
+
|
|
280
|
+
if (response) {
|
|
281
|
+
response = response.val;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
response = JSON.stringify(response);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
callback(error, response);
|
|
288
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const events = require('events');
|
|
4
|
+
|
|
5
|
+
exports.Database = class extends events.EventEmitter {
|
|
6
|
+
constructor(settings) {
|
|
7
|
+
super();
|
|
8
|
+
this.settings = {
|
|
9
|
+
writeInterval: 1,
|
|
10
|
+
...settings,
|
|
11
|
+
};
|
|
12
|
+
settings.mock = this;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
close(cb) {
|
|
16
|
+
this.emit('close', cb);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
doBulk(ops, cb) {
|
|
20
|
+
this.emit('doBulk', ops, cb);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
findKeys(key, notKey, cb) {
|
|
24
|
+
this.emit('findKeys', key, notKey, cb);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get(key, cb) {
|
|
28
|
+
this.emit('get', key, cb);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
init(cb) {
|
|
32
|
+
this.emit('init', cb);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
remove(key, cb) {
|
|
36
|
+
this.emit('remove', key, cb);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
set(key, value, cb) {
|
|
40
|
+
this.emit('set', key, value, cb);
|
|
41
|
+
}
|
|
42
|
+
};
|