ueberdb2 4.0.11 → 4.0.17
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/.eslintignore +2 -0
- package/.eslintrc.cjs +44 -5
- package/.github/workflows/npmpublish.yml +3 -3
- package/databases/{cassandra_db.js → cassandra_db.ts} +45 -30
- package/databases/{couch_db.js → couch_db.ts} +78 -31
- package/databases/{dirty_db.js → dirty_db.ts} +19 -14
- package/databases/{dirty_git_db.js → dirty_git_db.ts} +19 -15
- package/databases/{elasticsearch_db.js → elasticsearch_db.ts} +30 -21
- package/databases/{memory_db.js → memory_db.ts} +8 -8
- package/databases/mock_db.ts +43 -0
- package/databases/{mongodb_db.js → mongodb_db.ts} +22 -16
- package/databases/{mssql_db.js → mssql_db.ts} +29 -21
- package/databases/{mysql_db.js → mysql_db.ts} +20 -15
- package/databases/{postgres_db.js → postgres_db.ts} +37 -22
- package/databases/{postgrespool_db.js → postgrespool_db.ts} +3 -3
- package/databases/redis_db.ts +129 -0
- package/databases/{rethink_db.js → rethink_db.ts} +35 -19
- package/databases/{sqlite_db.js → sqlite_db.ts} +37 -36
- package/dist/databases/cassandra_db.js +237 -0
- package/dist/databases/couch_db.js +181 -0
- package/dist/databases/dirty_db.js +78 -0
- package/dist/databases/dirty_git_db.js +77 -0
- package/dist/databases/elasticsearch_db.js +251 -0
- package/dist/databases/memory_db.js +39 -0
- package/dist/databases/mock_db.js +40 -0
- package/dist/databases/mongodb_db.js +127 -0
- package/dist/databases/mssql_db.js +187 -0
- package/dist/databases/mysql_db.js +170 -0
- package/dist/databases/postgres_db.js +192 -0
- package/dist/databases/postgrespool_db.js +12 -0
- package/dist/databases/redis_db.js +105 -0
- package/dist/databases/rethink_db.js +123 -0
- package/dist/databases/sqlite_db.js +140 -0
- package/dist/index.js +215 -0
- package/dist/lib/AbstractDatabase.js +38 -0
- package/dist/lib/CacheAndBufferLayer.js +657 -0
- package/dist/lib/logging.js +34 -0
- package/dist/test/lib/databases.js +72 -0
- package/dist/test/test.js +373 -0
- package/dist/test/test_bulk.js +74 -0
- package/dist/test/test_elasticsearch.js +157 -0
- package/dist/test/test_findKeys.js +69 -0
- package/dist/test/test_flush.js +83 -0
- package/dist/test/test_getSub.js +57 -0
- package/dist/test/test_lru.js +155 -0
- package/dist/test/test_memory.js +59 -0
- package/dist/test/test_metrics.js +772 -0
- package/dist/test/test_mysql.js +91 -0
- package/dist/test/test_postgres.js +40 -0
- package/dist/test/test_setSub.js +48 -0
- package/dist/test/test_tojson.js +62 -0
- package/docker-compose.yml +44 -0
- package/{index.js → index.ts} +76 -25
- package/lib/AbstractDatabase.ts +79 -0
- package/lib/{CacheAndBufferLayer.js → CacheAndBufferLayer.ts} +17 -16
- package/lib/{logging.js → logging.ts} +10 -6
- package/package.json +18 -3
- package/test/lib/{databases.js → databases.ts} +8 -5
- package/test/test.ts +328 -0
- package/test/test_bulk.ts +69 -0
- package/test/{test_elasticsearch.js → test_elasticsearch.ts} +48 -53
- package/test/{test_findKeys.js → test_findKeys.ts} +15 -17
- package/test/{test_flush.js → test_flush.ts} +16 -22
- package/test/test_getSub.ts +28 -0
- package/test/test_lru.ts +151 -0
- package/test/test_memory.ts +32 -0
- package/test/{test_metrics.js → test_metrics.ts} +73 -68
- package/test/{test_mysql.js → test_mysql.ts} +16 -22
- package/test/test_postgres.ts +16 -0
- package/test/{test_setSub.js → test_setSub.ts} +8 -12
- package/test/test_tojson.ts +34 -0
- package/databases/mock_db.js +0 -42
- package/databases/redis_db.js +0 -96
- package/lib/AbstractDatabase.js +0 -37
- package/test/test.js +0 -328
- package/test/test_bulk.js +0 -69
- package/test/test_getSub.js +0 -31
- package/test/test_lru.js +0 -145
- package/test/test_memory.js +0 -31
- package/test/test_postgres.js +0 -16
- package/test/test_tojson.js +0 -37
|
@@ -0,0 +1,251 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.Database = void 0;
|
|
22
|
+
const AbstractDatabase_1 = __importDefault(require("../lib/AbstractDatabase"));
|
|
23
|
+
const assert_1 = require("assert");
|
|
24
|
+
const buffer_1 = require("buffer");
|
|
25
|
+
const crypto_1 = require("crypto");
|
|
26
|
+
const elasticsearch7_1 = require("elasticsearch7");
|
|
27
|
+
const schema = '2';
|
|
28
|
+
const keyToId = (key) => {
|
|
29
|
+
const keyBuf = buffer_1.Buffer.from(key);
|
|
30
|
+
return keyBuf.length > 512 ? (0, crypto_1.createHash)('sha512').update(keyBuf).digest('hex') : key;
|
|
31
|
+
};
|
|
32
|
+
const mappings = {
|
|
33
|
+
// _id is expected to equal key, unless the UTF-8 encoded key is > 512 bytes, in which case it is
|
|
34
|
+
// the hex-encoded sha512 hash of the UTF-8 encoded key.
|
|
35
|
+
properties: {
|
|
36
|
+
key: { type: 'wildcard' },
|
|
37
|
+
value: { type: 'object', enabled: false }, // Values should be opaque to Elasticsearch.
|
|
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)
|
|
58
|
+
continue;
|
|
59
|
+
totals.set(index, total);
|
|
60
|
+
const body = [];
|
|
61
|
+
for (const { _id, _type, _source: { val } } of hits) {
|
|
62
|
+
let key = `${_type}:${_id}`;
|
|
63
|
+
if (v1BaseIndex && index !== v1BaseIndex) {
|
|
64
|
+
const parts = index.slice(v1BaseIndex.length + 1).split('-');
|
|
65
|
+
if (parts.length !== 2) {
|
|
66
|
+
throw new Error(`unable to migrate records from index ${index} due to data ambiguity`);
|
|
67
|
+
}
|
|
68
|
+
key = `${parts[0]}:${decodeURIComponent(_type)}:${parts[1]}:${_id}`;
|
|
69
|
+
}
|
|
70
|
+
body.push({ index: { _id: keyToId(key) } }, { key, value: JSON.parse(val) });
|
|
71
|
+
}
|
|
72
|
+
await client.bulk({ index: v2Index, body });
|
|
73
|
+
recordsMigrated += hits.length;
|
|
74
|
+
if (Math.floor(recordsMigrated / 100) > Math.floor(recordsMigratedLastLogged / 100)) {
|
|
75
|
+
// @ts-ignore
|
|
76
|
+
const total = [...totals.values()].reduce((a, b) => a + b, 0);
|
|
77
|
+
logger.info(`Migrated ${recordsMigrated} records out of ${total}`);
|
|
78
|
+
recordsMigratedLastLogged = recordsMigrated;
|
|
79
|
+
}
|
|
80
|
+
q.push({ index, res: (await client.scroll({ scroll: '5m', scrollId: scrollIds.get(index) })).body });
|
|
81
|
+
}
|
|
82
|
+
logger.info(`Finished migrating ${recordsMigrated} records`);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
// @ts-ignore
|
|
86
|
+
await Promise.all([...scrollIds.values()].map((scrollId) => client.clearScroll({ scrollId })));
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const Database = class extends AbstractDatabase_1.default {
|
|
90
|
+
_client;
|
|
91
|
+
_index;
|
|
92
|
+
_indexClean;
|
|
93
|
+
_q;
|
|
94
|
+
constructor(settings) {
|
|
95
|
+
super();
|
|
96
|
+
this._client = null;
|
|
97
|
+
this.settings = {
|
|
98
|
+
host: '127.0.0.1',
|
|
99
|
+
port: '9200',
|
|
100
|
+
base_index: 'ueberes',
|
|
101
|
+
migrate_to_newer_schema: false,
|
|
102
|
+
// for a list of valid API values see:
|
|
103
|
+
// https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/configuration.html#config-options
|
|
104
|
+
api: '7.6',
|
|
105
|
+
...settings || {},
|
|
106
|
+
json: false, // Elasticsearch will do the JSON conversion as necessary.
|
|
107
|
+
};
|
|
108
|
+
this._index = `${this.settings.base_index}_s${schema}`;
|
|
109
|
+
this._q = { index: this._index };
|
|
110
|
+
this._indexClean = true;
|
|
111
|
+
}
|
|
112
|
+
get isAsync() { return true; }
|
|
113
|
+
async _refreshIndex() {
|
|
114
|
+
if (this._indexClean)
|
|
115
|
+
return;
|
|
116
|
+
this._indexClean = true;
|
|
117
|
+
await this._client.indices.refresh(this._q);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Initialize the elasticsearch client, then ping the server to ensure that a
|
|
121
|
+
* connection was made.
|
|
122
|
+
*/
|
|
123
|
+
async init() {
|
|
124
|
+
// create elasticsearch client
|
|
125
|
+
const client = new elasticsearch7_1.Client({
|
|
126
|
+
node: `http://${this.settings.host}:${this.settings.port}`,
|
|
127
|
+
});
|
|
128
|
+
await client.ping();
|
|
129
|
+
if (!(await client.indices.exists({ index: this._index })).body) {
|
|
130
|
+
let tmpIndex;
|
|
131
|
+
// @ts-ignore
|
|
132
|
+
const { body: migrate } = await client.indices.exists({ index: this.settings.base_index });
|
|
133
|
+
if (migrate && !this.settings.migrate_to_newer_schema) {
|
|
134
|
+
throw new Error(`Data exists under the legacy index (schema) named ${this.settings.base_index}. ` +
|
|
135
|
+
'Set migrate_to_newer_schema to true to copy the existing data to a new index ' +
|
|
136
|
+
`named ${this._index}.`);
|
|
137
|
+
}
|
|
138
|
+
let attempt = 0;
|
|
139
|
+
while (true) {
|
|
140
|
+
tmpIndex = `${this._index}_${migrate ? 'migrate_attempt_' : 'i'}${attempt++}`;
|
|
141
|
+
if (!(await client.indices.exists({ index: tmpIndex })).body)
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
await client.indices.create({ index: tmpIndex, body: { mappings } });
|
|
145
|
+
if (migrate)
|
|
146
|
+
await migrateToSchema2(client, this.settings.base_index, tmpIndex, this.logger);
|
|
147
|
+
await client.indices.putAlias({ index: tmpIndex, name: this._index });
|
|
148
|
+
}
|
|
149
|
+
const indices = Object.values((await client.indices.get({ index: this._index })).body);
|
|
150
|
+
(0, assert_1.equal)(indices.length, 1);
|
|
151
|
+
try {
|
|
152
|
+
// @ts-ignore
|
|
153
|
+
assert.deepEqual(indices[0].mappings, mappings);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
this.logger.warn(`Index ${this._index} mappings does not match expected; ` +
|
|
157
|
+
`attempting to use index anyway. Details: ${err}`);
|
|
158
|
+
}
|
|
159
|
+
this._client = client;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* This function provides read functionality to the database.
|
|
163
|
+
*
|
|
164
|
+
* @param {String} key Key
|
|
165
|
+
*/
|
|
166
|
+
async get(key) {
|
|
167
|
+
const { body } = await this._client.get({ ...this._q, id: keyToId(key) }, { ignore: [404] });
|
|
168
|
+
if (!body.found)
|
|
169
|
+
return null;
|
|
170
|
+
return body._source.value;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* @param key Search key, which uses an asterisk (*) as the wild card.
|
|
174
|
+
* @param notKey Used to filter the result set
|
|
175
|
+
*/
|
|
176
|
+
async findKeys(key, notKey) {
|
|
177
|
+
await this._refreshIndex();
|
|
178
|
+
const q = {
|
|
179
|
+
...this._q,
|
|
180
|
+
body: {
|
|
181
|
+
query: {
|
|
182
|
+
bool: {
|
|
183
|
+
filter: { wildcard: { key: { value: key } } },
|
|
184
|
+
...notKey == null ? {} : {
|
|
185
|
+
must_not: { wildcard: { key: { value: notKey } } },
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
const { body: { hits: { hits } } } = await this._client.search(q);
|
|
192
|
+
return hits.map((h) => h._source.key);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* This function provides write functionality to the database.
|
|
196
|
+
*
|
|
197
|
+
* @param {String} key Record identifier.
|
|
198
|
+
* @param {JSON|String} value The value to store in the database.
|
|
199
|
+
*/
|
|
200
|
+
async set(key, value) {
|
|
201
|
+
this._indexClean = false;
|
|
202
|
+
await this._client.index({ ...this._q, id: keyToId(key), body: { key, value } });
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* This function provides delete functionality to the database.
|
|
206
|
+
*
|
|
207
|
+
* The index, type, and ID will be parsed from the key, and this document will
|
|
208
|
+
* be deleted from the database.
|
|
209
|
+
*
|
|
210
|
+
* @param {String} key Record identifier.
|
|
211
|
+
*/
|
|
212
|
+
async remove(key) {
|
|
213
|
+
this._indexClean = false;
|
|
214
|
+
await this._client.delete({ ...this._q, id: keyToId(key) }, { ignore: [404] });
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* This uses the bulk upload functionality of elasticsearch (url:port/_bulk).
|
|
218
|
+
*
|
|
219
|
+
* The CacheAndBufferLayer will periodically (every this.settings.writeInterval)
|
|
220
|
+
* flush writes that have already been done in the local cache out to the database.
|
|
221
|
+
*
|
|
222
|
+
* @param {Array} bulk An array of JSON data in the format:
|
|
223
|
+
* {"type":type, "key":key, "value":value}
|
|
224
|
+
*/
|
|
225
|
+
async doBulk(bulk) {
|
|
226
|
+
// bulk is an array of JSON:
|
|
227
|
+
// example: [{"type":"set", "key":"sessionstorage:{id}", "value":{"cookie":{...}}]
|
|
228
|
+
const operations = [];
|
|
229
|
+
for (const { type, key, value } of bulk) {
|
|
230
|
+
this._indexClean = false;
|
|
231
|
+
switch (type) {
|
|
232
|
+
case 'set':
|
|
233
|
+
operations.push({ index: { _id: keyToId(key) } });
|
|
234
|
+
operations.push({ key, value });
|
|
235
|
+
break;
|
|
236
|
+
case 'remove':
|
|
237
|
+
operations.push({ delete: { _id: keyToId(key) } });
|
|
238
|
+
break;
|
|
239
|
+
default:
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
await this._client.bulk({ ...this._q, body: operations });
|
|
244
|
+
}
|
|
245
|
+
async close() {
|
|
246
|
+
if (this._client != null)
|
|
247
|
+
this._client.close();
|
|
248
|
+
this._client = null;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
exports.Database = Database;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Database = void 0;
|
|
7
|
+
const AbstractDatabase_1 = __importDefault(require("../lib/AbstractDatabase"));
|
|
8
|
+
const Database = class MemoryDB extends AbstractDatabase_1.default {
|
|
9
|
+
_data;
|
|
10
|
+
constructor(settings) {
|
|
11
|
+
super();
|
|
12
|
+
this.settings = settings;
|
|
13
|
+
settings.json = false;
|
|
14
|
+
settings.cache = 0;
|
|
15
|
+
settings.writeInterval = 0;
|
|
16
|
+
this._data = null;
|
|
17
|
+
}
|
|
18
|
+
get isAsync() { return true; }
|
|
19
|
+
close() {
|
|
20
|
+
this._data = null;
|
|
21
|
+
}
|
|
22
|
+
findKeys(key, notKey) {
|
|
23
|
+
const regex = this.createFindRegex(key, notKey);
|
|
24
|
+
return [...this._data.keys()].filter((k) => regex.test(k));
|
|
25
|
+
}
|
|
26
|
+
get(key) {
|
|
27
|
+
return this._data.get(key);
|
|
28
|
+
}
|
|
29
|
+
init() {
|
|
30
|
+
this._data = this.settings.data || new Map();
|
|
31
|
+
}
|
|
32
|
+
remove(key) {
|
|
33
|
+
this._data.delete(key);
|
|
34
|
+
}
|
|
35
|
+
set(key, value) {
|
|
36
|
+
this._data.set(key, value);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
exports.Database = Database;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Database = void 0;
|
|
7
|
+
const events_1 = __importDefault(require("events"));
|
|
8
|
+
const Database = class extends events_1.default.EventEmitter {
|
|
9
|
+
settings;
|
|
10
|
+
constructor(settings) {
|
|
11
|
+
super();
|
|
12
|
+
this.settings = {
|
|
13
|
+
writeInterval: 1,
|
|
14
|
+
...settings,
|
|
15
|
+
};
|
|
16
|
+
settings.mock = this;
|
|
17
|
+
}
|
|
18
|
+
close(cb) {
|
|
19
|
+
this.emit('close', cb);
|
|
20
|
+
}
|
|
21
|
+
doBulk(ops, cb) {
|
|
22
|
+
this.emit('doBulk', ops, cb);
|
|
23
|
+
}
|
|
24
|
+
findKeys(key, notKey, cb) {
|
|
25
|
+
this.emit('findKeys', key, notKey, cb);
|
|
26
|
+
}
|
|
27
|
+
get(key, cb) {
|
|
28
|
+
this.emit('get', key, cb);
|
|
29
|
+
}
|
|
30
|
+
init(cb) {
|
|
31
|
+
this.emit('init', cb);
|
|
32
|
+
}
|
|
33
|
+
remove(key, cb) {
|
|
34
|
+
this.emit('remove', key, cb);
|
|
35
|
+
}
|
|
36
|
+
set(key, value, cb) {
|
|
37
|
+
this.emit('set', key, value, cb);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
exports.Database = Database;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 2020 Sylchauf
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
const AbstractDatabase_1 = __importDefault(require("../lib/AbstractDatabase"));
|
|
22
|
+
exports.Database = class extends AbstractDatabase_1.default {
|
|
23
|
+
interval;
|
|
24
|
+
database;
|
|
25
|
+
client;
|
|
26
|
+
collection;
|
|
27
|
+
constructor(settings) {
|
|
28
|
+
super();
|
|
29
|
+
this.settings = settings;
|
|
30
|
+
if (!this.settings.url)
|
|
31
|
+
throw new Error('You must specify a mongodb url');
|
|
32
|
+
// For backwards compatibility:
|
|
33
|
+
if (this.settings.database == null)
|
|
34
|
+
this.settings.database = this.settings.dbName;
|
|
35
|
+
if (!this.settings.collection)
|
|
36
|
+
this.settings.collection = 'ueberdb';
|
|
37
|
+
}
|
|
38
|
+
clearPing() {
|
|
39
|
+
if (this.interval) {
|
|
40
|
+
clearInterval(this.interval);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
schedulePing() {
|
|
44
|
+
this.clearPing();
|
|
45
|
+
this.interval = setInterval(() => {
|
|
46
|
+
this.database.command({
|
|
47
|
+
ping: 1,
|
|
48
|
+
});
|
|
49
|
+
}, 10000);
|
|
50
|
+
}
|
|
51
|
+
init(callback) {
|
|
52
|
+
const MongoClient = require('mongodb').MongoClient;
|
|
53
|
+
MongoClient.connect(this.settings.url, (err, client) => {
|
|
54
|
+
if (!err) {
|
|
55
|
+
this.client = client;
|
|
56
|
+
this.database = client.db(this.settings.database);
|
|
57
|
+
this.collection = this.database.collection(this.settings.collection);
|
|
58
|
+
}
|
|
59
|
+
callback(err);
|
|
60
|
+
});
|
|
61
|
+
this.schedulePing();
|
|
62
|
+
}
|
|
63
|
+
get(key, callback) {
|
|
64
|
+
this.collection.findOne({ _id: key }, (err, document) => {
|
|
65
|
+
if (err)
|
|
66
|
+
callback(err);
|
|
67
|
+
else
|
|
68
|
+
callback(null, document ? document.value : null);
|
|
69
|
+
});
|
|
70
|
+
this.schedulePing();
|
|
71
|
+
}
|
|
72
|
+
findKeys(key, notKey, callback) {
|
|
73
|
+
const selector = {
|
|
74
|
+
$and: [
|
|
75
|
+
{ _id: { $regex: `${key.replace(/\*/g, '')}` } },
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
if (notKey) {
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
selector.$and.push({ _id: { $not: { $regex: `${notKey.replace(/\*/g, '')}` } } });
|
|
81
|
+
}
|
|
82
|
+
this.collection.find(selector, async (err, res) => {
|
|
83
|
+
if (err) {
|
|
84
|
+
callback(err);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const data = await res.toArray();
|
|
88
|
+
callback(null, data.map((i) => i._id));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
this.schedulePing();
|
|
92
|
+
}
|
|
93
|
+
set(key, value, callback) {
|
|
94
|
+
if (key.length > 100) {
|
|
95
|
+
callback('Your Key can only be 100 chars');
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
this.collection.update({ _id: key }, { $set: { value } }, { upsert: true }, callback);
|
|
99
|
+
}
|
|
100
|
+
this.schedulePing();
|
|
101
|
+
}
|
|
102
|
+
remove(key, callback) {
|
|
103
|
+
this.collection.remove({ _id: key }, callback);
|
|
104
|
+
this.schedulePing();
|
|
105
|
+
}
|
|
106
|
+
doBulk(bulk, callback) {
|
|
107
|
+
const bulkMongo = this.collection.initializeOrderedBulkOp();
|
|
108
|
+
for (const i in bulk) {
|
|
109
|
+
if (bulk[i].type === 'set') {
|
|
110
|
+
bulkMongo.find({ _id: bulk[i].key }).upsert().updateOne({ $set: { value: bulk[i].value } });
|
|
111
|
+
}
|
|
112
|
+
else if (bulk[i].type === 'remove') {
|
|
113
|
+
bulkMongo.find({ _id: bulk[i].key }).deleteOne();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
bulkMongo.execute().then((res) => {
|
|
117
|
+
callback(null, res);
|
|
118
|
+
}).catch((error) => {
|
|
119
|
+
callback(error);
|
|
120
|
+
});
|
|
121
|
+
this.schedulePing();
|
|
122
|
+
}
|
|
123
|
+
close(callback) {
|
|
124
|
+
this.clearPing();
|
|
125
|
+
this.client.close(callback);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint new-cap: ["error", {"capIsNewExceptions": ["mssql.NVarChar"]}] */
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.Database = void 0;
|
|
8
|
+
/**
|
|
9
|
+
* 2019 - exspecto@gmail.com
|
|
10
|
+
*
|
|
11
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
12
|
+
* you may not use this file except in compliance with the License.
|
|
13
|
+
* You may obtain a copy of the License at
|
|
14
|
+
*
|
|
15
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
16
|
+
*
|
|
17
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
18
|
+
* distributed under the License is distributed on an "AS-IS" BASIS,
|
|
19
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
20
|
+
* See the License for the specific language governing permissions and
|
|
21
|
+
* limitations under the License.
|
|
22
|
+
*
|
|
23
|
+
*
|
|
24
|
+
* Note: This requires MS SQL Server >= 2008 due to the usage of the MERGE statement
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
const AbstractDatabase_1 = __importDefault(require("../lib/AbstractDatabase"));
|
|
28
|
+
const async_1 = __importDefault(require("async"));
|
|
29
|
+
const mssql_1 = __importDefault(require("mssql"));
|
|
30
|
+
const Database = class MSSQL extends AbstractDatabase_1.default {
|
|
31
|
+
db;
|
|
32
|
+
constructor(settings) {
|
|
33
|
+
super();
|
|
34
|
+
settings = settings || {};
|
|
35
|
+
if (settings.json != null) {
|
|
36
|
+
settings.parseJSON = settings.json;
|
|
37
|
+
}
|
|
38
|
+
// set the request timeout to 5 minutes
|
|
39
|
+
settings.requestTimeout = 300000;
|
|
40
|
+
settings.server = settings.host;
|
|
41
|
+
this.settings = settings;
|
|
42
|
+
/*
|
|
43
|
+
Turning off the cache and write buffer here. You
|
|
44
|
+
can reenable it, but also take a look at maxInserts in
|
|
45
|
+
the doBulk function to decide how you want to split it up.
|
|
46
|
+
*/
|
|
47
|
+
this.settings.cache = 0;
|
|
48
|
+
this.settings.writeInterval = 0;
|
|
49
|
+
}
|
|
50
|
+
init(callback) {
|
|
51
|
+
const sqlCreate = "IF OBJECT_ID(N'dbo.store', N'U') IS NULL" +
|
|
52
|
+
' BEGIN' +
|
|
53
|
+
' CREATE TABLE [store] (' +
|
|
54
|
+
' [key] NVARCHAR(100) PRIMARY KEY,' +
|
|
55
|
+
' [value] NTEXT NOT NULL' +
|
|
56
|
+
' );' +
|
|
57
|
+
' END';
|
|
58
|
+
// @ts-ignore
|
|
59
|
+
new mssql_1.default.ConnectionPool(this.settings).connect().then((pool) => {
|
|
60
|
+
this.db = pool;
|
|
61
|
+
const request = new mssql_1.default.Request(this.db);
|
|
62
|
+
request.query(sqlCreate, (err) => {
|
|
63
|
+
callback(err);
|
|
64
|
+
});
|
|
65
|
+
this.db.on('error', (err) => {
|
|
66
|
+
console.log(err);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
get(key, callback) {
|
|
71
|
+
const request = new mssql_1.default.Request(this.db);
|
|
72
|
+
request.input('key', mssql_1.default.NVarChar(100), key);
|
|
73
|
+
request.query('SELECT [value] FROM [store] WHERE [key] = @key', (err, results) => {
|
|
74
|
+
let value = null;
|
|
75
|
+
if (!err && results && results.rowsAffected[0] === 1) {
|
|
76
|
+
// @ts-ignore
|
|
77
|
+
value = results.recordset[0].value;
|
|
78
|
+
}
|
|
79
|
+
callback(err, value);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
findKeys(key, notKey, callback) {
|
|
83
|
+
const request = new mssql_1.default.Request(this.db);
|
|
84
|
+
let query = 'SELECT [key] FROM [store] WHERE [key] LIKE @key';
|
|
85
|
+
// desired keys are key, e.g. pad:%
|
|
86
|
+
key = key.replace(/\*/g, '%');
|
|
87
|
+
request.input('key', mssql_1.default.NVarChar(100), key);
|
|
88
|
+
if (notKey != null) {
|
|
89
|
+
// not desired keys are notKey, e.g. %:%:%
|
|
90
|
+
notKey = notKey.replace(/\*/g, '%');
|
|
91
|
+
request.input('notkey', mssql_1.default.NVarChar(100), notKey);
|
|
92
|
+
query += ' AND [key] NOT LIKE @notkey';
|
|
93
|
+
}
|
|
94
|
+
request.query(query, (err, results) => {
|
|
95
|
+
const value = [];
|
|
96
|
+
if (!err && results && results.rowsAffected[0] > 0) {
|
|
97
|
+
for (let i = 0; i < results.recordset.length; i++) {
|
|
98
|
+
value.push(results.recordset[i].key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
callback(err, value);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
set(key, value, callback) {
|
|
105
|
+
const request = new mssql_1.default.Request(this.db);
|
|
106
|
+
if (key.length > 100) {
|
|
107
|
+
callback('Your Key can only be 100 chars');
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const query = 'MERGE [store] t USING (SELECT @key [key], @value [value]) s' +
|
|
111
|
+
' ON t.[key] = s.[key]' +
|
|
112
|
+
' WHEN MATCHED AND s.[value] IS NOT NULL THEN UPDATE SET t.[value] = s.[value]' +
|
|
113
|
+
' WHEN NOT MATCHED THEN INSERT ([key], [value]) VALUES (s.[key], s.[value]);';
|
|
114
|
+
request.input('key', mssql_1.default.NVarChar(100), key);
|
|
115
|
+
request.input('value', mssql_1.default.NText, value);
|
|
116
|
+
request.query(query, (err, info) => {
|
|
117
|
+
callback(err ? err.toString() : '');
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
remove(key, callback) {
|
|
122
|
+
const request = new mssql_1.default.Request(this.db);
|
|
123
|
+
request.input('key', mssql_1.default.NVarChar(100), key);
|
|
124
|
+
request.query('DELETE FROM [store] WHERE [key] = @key', callback);
|
|
125
|
+
}
|
|
126
|
+
doBulk(bulk, callback) {
|
|
127
|
+
const maxInserts = 100;
|
|
128
|
+
const request = new mssql_1.default.Request(this.db);
|
|
129
|
+
let firstReplace = true;
|
|
130
|
+
let firstRemove = true;
|
|
131
|
+
const replacements = [];
|
|
132
|
+
let removeSQL = 'DELETE FROM [store] WHERE [key] IN (';
|
|
133
|
+
for (const i in bulk) {
|
|
134
|
+
if (bulk[i].type === 'set') {
|
|
135
|
+
if (firstReplace) {
|
|
136
|
+
replacements.push('BEGIN TRANSACTION;');
|
|
137
|
+
firstReplace = false;
|
|
138
|
+
}
|
|
139
|
+
else if (Number(i) % maxInserts === 0) {
|
|
140
|
+
replacements.push('\nCOMMIT TRANSACTION;\nBEGIN TRANSACTION;\n');
|
|
141
|
+
}
|
|
142
|
+
replacements.push(`MERGE [store] t USING (SELECT '${bulk[i].key}' [key], '${bulk[i].value}' [value]) s`, 'ON t.[key] = s.[key]', 'WHEN MATCHED AND s.[value] IS NOT NULL THEN UPDATE SET t.[value] = s.[value]', 'WHEN NOT MATCHED THEN INSERT ([key], [value]) VALUES (s.[key], s.[value]);');
|
|
143
|
+
}
|
|
144
|
+
else if (bulk[i].type === 'remove') {
|
|
145
|
+
if (!firstRemove) {
|
|
146
|
+
removeSQL += ',';
|
|
147
|
+
}
|
|
148
|
+
firstRemove = false;
|
|
149
|
+
removeSQL += `'${bulk[i].key}'`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
removeSQL += ');';
|
|
153
|
+
replacements.push('COMMIT TRANSACTION;');
|
|
154
|
+
async_1.default.parallel([
|
|
155
|
+
(callback) => {
|
|
156
|
+
if (!firstReplace) {
|
|
157
|
+
request.batch(replacements.join('\n'), (err, results) => {
|
|
158
|
+
if (err) {
|
|
159
|
+
callback(err);
|
|
160
|
+
}
|
|
161
|
+
callback(err, results);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
callback();
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
(callback) => {
|
|
169
|
+
if (!firstRemove) {
|
|
170
|
+
request.query(removeSQL, callback);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
callback();
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
], (err, results) => {
|
|
177
|
+
if (err) {
|
|
178
|
+
callback(err);
|
|
179
|
+
}
|
|
180
|
+
callback(err, results);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
close(callback) {
|
|
184
|
+
this.db && this.db.close(callback);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
exports.Database = Database;
|