spectre.db 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/engine.js ADDED
@@ -0,0 +1,605 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const fsp = fs.promises;
5
+ const path = require('path');
6
+ const zlib = require('zlib');
7
+ const crypto = require('crypto');
8
+ const { EventEmitter } = require('events');
9
+ const { promisify } = require('util');
10
+
11
+ const asyncGzip = promisify(zlib.gzip);
12
+ const asyncGunzip = promisify(zlib.gunzip);
13
+
14
+ const FORBIDDEN_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
15
+ const SENSITIVE_KEY_RE = /password|secret|token|apikey|api_key|private/i;
16
+
17
+ function validateKey(key) {
18
+ if (typeof key !== 'string' || key.length === 0) {
19
+ throw new TypeError('Key must be a non-empty string');
20
+ }
21
+ const parts = key.split('.');
22
+ for (const part of parts) {
23
+ if (part.length === 0) throw new Error(`Key contains an empty segment: "${key}"`);
24
+ if (FORBIDDEN_SEGMENTS.has(part)) throw new Error(`Forbidden key segment "${part}" in key: "${key}"`);
25
+ }
26
+ return parts;
27
+ }
28
+
29
+ function uniqueTmp(base) {
30
+ const rand = crypto.randomBytes(6).toString('hex');
31
+ return `${base}.${process.pid}.${Date.now()}.${rand}.tmp`;
32
+ }
33
+
34
+ function encryptValue(value, keyBuf) {
35
+ const iv = crypto.randomBytes(12);
36
+ const cipher = crypto.createCipheriv('aes-256-gcm', keyBuf, iv);
37
+ const plain = JSON.stringify(value);
38
+ const encrypted = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);
39
+ const tag = cipher.getAuthTag();
40
+ return { __enc: 1, iv: iv.toString('base64'), ct: encrypted.toString('base64'), tag: tag.toString('base64') };
41
+ }
42
+
43
+ function decryptValue(envelope, keyBuf) {
44
+ const iv = Buffer.from(envelope.iv, 'base64');
45
+ const ct = Buffer.from(envelope.ct, 'base64');
46
+ const tag = Buffer.from(envelope.tag, 'base64');
47
+ const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuf, iv);
48
+ decipher.setAuthTag(tag);
49
+ const plain = Buffer.concat([decipher.update(ct), decipher.final()]);
50
+ return JSON.parse(plain.toString('utf8'));
51
+ }
52
+
53
+ function toNullProto(value) {
54
+ if (value === null || value === undefined || typeof value !== 'object') return value;
55
+ if (Array.isArray(value)) return value.map(toNullProto);
56
+ const result = Object.create(null);
57
+ for (const k of Object.keys(value)) result[k] = toNullProto(value[k]);
58
+ return result;
59
+ }
60
+
61
+ function toPlainObject(value) {
62
+ if (value === null || value === undefined || typeof value !== 'object') return value;
63
+ if (Array.isArray(value)) return value.map(toPlainObject);
64
+ const result = {};
65
+ for (const k of Object.keys(value)) result[k] = toPlainObject(value[k]);
66
+ return result;
67
+ }
68
+
69
+ class WriteQueue {
70
+ constructor() {
71
+ this._chain = Promise.resolve();
72
+ }
73
+
74
+ push(fn) {
75
+ const result = this._chain.then(() => fn());
76
+ this._chain = result.catch(() => {});
77
+ return result;
78
+ }
79
+ }
80
+
81
+ class LRUNode {
82
+ constructor(key, value, expiry) {
83
+ this.key = key;
84
+ this.value = value;
85
+ this.expiry = expiry;
86
+ this.prev = null;
87
+ this.next = null;
88
+ }
89
+ }
90
+
91
+ class LRUCache {
92
+ constructor(maxSize) {
93
+ this._max = maxSize;
94
+ this._map = new Map();
95
+ this._index = new Map();
96
+ this._head = new LRUNode(null, null, -1);
97
+ this._tail = new LRUNode(null, null, -1);
98
+ this._head.next = this._tail;
99
+ this._tail.prev = this._head;
100
+ }
101
+
102
+ _attach(node) {
103
+ node.next = this._head.next;
104
+ node.prev = this._head;
105
+ this._head.next.prev = node;
106
+ this._head.next = node;
107
+ }
108
+
109
+ _detach(node) {
110
+ node.prev.next = node.next;
111
+ node.next.prev = node.prev;
112
+ node.prev = null;
113
+ node.next = null;
114
+ }
115
+
116
+ _evict(node) {
117
+ this._detach(node);
118
+ this._map.delete(node.key);
119
+ this._removeFromIndex(node.key);
120
+ }
121
+
122
+ _removeFromIndex(key) {
123
+ const parts = key.split('.');
124
+ for (let i = 1; i <= parts.length; i++) {
125
+ const prefix = parts.slice(0, i).join('.');
126
+ const set = this._index.get(prefix);
127
+ if (!set) continue;
128
+ set.delete(key);
129
+ if (set.size === 0) this._index.delete(prefix);
130
+ }
131
+ }
132
+
133
+ get(key) {
134
+ const node = this._map.get(key);
135
+ if (!node) return undefined;
136
+ if (node.expiry > 0 && Date.now() > node.expiry) { this._evict(node); return undefined; }
137
+ this._detach(node);
138
+ this._attach(node);
139
+ return node.value;
140
+ }
141
+
142
+ set(key, value, ttlMs = 0) {
143
+ const expiry = ttlMs > 0 ? Date.now() + ttlMs : -1;
144
+ if (this._map.has(key)) {
145
+ const node = this._map.get(key);
146
+ node.value = value;
147
+ node.expiry = expiry;
148
+ this._detach(node);
149
+ this._attach(node);
150
+ return;
151
+ }
152
+ const node = new LRUNode(key, value, expiry);
153
+ this._map.set(key, node);
154
+ this._attach(node);
155
+ const parts = key.split('.');
156
+ for (let i = 1; i <= parts.length; i++) {
157
+ const prefix = parts.slice(0, i).join('.');
158
+ if (!this._index.has(prefix)) this._index.set(prefix, new Set());
159
+ this._index.get(prefix).add(key);
160
+ }
161
+ if (this._map.size > this._max) this._evict(this._tail.prev);
162
+ }
163
+
164
+ invalidate(key) {
165
+ const affected = this._index.get(key);
166
+ if (affected) for (const k of [...affected]) { const node = this._map.get(k); if (node) this._evict(node); }
167
+ const parts = key.split('.');
168
+ for (let i = 1; i < parts.length; i++) {
169
+ const parent = parts.slice(0, i).join('.');
170
+ const node = this._map.get(parent);
171
+ if (node) this._evict(node);
172
+ }
173
+ }
174
+
175
+ clear() {
176
+ this._map.clear();
177
+ this._index.clear();
178
+ this._head.next = this._tail;
179
+ this._tail.prev = this._head;
180
+ }
181
+
182
+ get size() { return this._map.size; }
183
+ }
184
+
185
+ class WALWriter {
186
+ constructor(walPath) {
187
+ this._path = walPath;
188
+ this._fd = null;
189
+ }
190
+
191
+ async open() {
192
+ this._fd = await fsp.open(this._path, 'a');
193
+ }
194
+
195
+ async append(entry) {
196
+ await this._fd.write(JSON.stringify(entry) + '\n');
197
+ }
198
+
199
+ async truncateAndReopen() {
200
+ if (this._fd) await this._fd.close();
201
+ await fsp.writeFile(this._path, '');
202
+ this._fd = await fsp.open(this._path, 'a');
203
+ }
204
+
205
+ async close() {
206
+ if (this._fd) { await this._fd.close(); this._fd = null; }
207
+ }
208
+ }
209
+
210
+ class Engine extends EventEmitter {
211
+ constructor(dbPath, options = {}) {
212
+ super();
213
+
214
+ this._opts = {
215
+ maxCacheSize: 1000,
216
+ cacheTTL: 0,
217
+ compactThreshold: 500,
218
+ compactInterval: 5 * 60 * 1000,
219
+ compress: false,
220
+ encryptionKey: null,
221
+ backupCount: 3,
222
+ ...options,
223
+ };
224
+
225
+ this._encKey = null;
226
+ if (this._opts.encryptionKey) {
227
+ const raw = this._opts.encryptionKey;
228
+ this._encKey = Buffer.isBuffer(raw) && raw.length === 32
229
+ ? raw
230
+ : crypto.scryptSync(String(raw), 'spectre-db-kdf-v1', 32);
231
+ }
232
+
233
+ const resolved = path.resolve(dbPath);
234
+ this._dir = path.dirname(resolved);
235
+ this._base = path.basename(resolved).replace(/\.(json|db|snapshot)$/, '');
236
+ this._snapshotPath = path.join(this._dir, `${this._base}.snapshot`);
237
+ this._walPath = path.join(this._dir, `${this._base}.wal`);
238
+
239
+ this._store = Object.create(null);
240
+ this._cache = new LRUCache(this._opts.maxCacheSize);
241
+ this._queue = new WriteQueue();
242
+ this._wal = new WALWriter(this._walPath);
243
+ this._walOps = 0;
244
+ this._closed = false;
245
+ this._compactTimer = null;
246
+
247
+ this.ready = this._init();
248
+ }
249
+
250
+ async _init() {
251
+ await fsp.mkdir(this._dir, { recursive: true });
252
+ await this._loadSnapshot();
253
+ this._walOps = await this._replayWAL();
254
+ await this._wal.open();
255
+
256
+ if (this._opts.compactInterval > 0) {
257
+ this._compactTimer = setInterval(() => {
258
+ if (this._walOps >= this._opts.compactThreshold) {
259
+ this._queue.push(() => this._compact());
260
+ }
261
+ }, this._opts.compactInterval);
262
+ this._compactTimer.unref?.();
263
+ }
264
+ }
265
+
266
+ async _loadSnapshot() {
267
+ const exists = await fsp.access(this._snapshotPath).then(() => true, () => false);
268
+ if (!exists) return;
269
+ try {
270
+ let buf = await fsp.readFile(this._snapshotPath);
271
+ if (this._opts.compress) buf = await asyncGunzip(buf);
272
+ this._store = toNullProto(JSON.parse(buf.toString('utf8')));
273
+ } catch {
274
+ this.emit('warn', 'Snapshot corrupted, attempting backup restore');
275
+ await this._restoreFromBackup();
276
+ }
277
+ }
278
+
279
+ async _restoreFromBackup() {
280
+ for (let gen = 1; gen <= this._opts.backupCount; gen++) {
281
+ const p = `${this._snapshotPath}.${gen}.bak`;
282
+ const exists = await fsp.access(p).then(() => true, () => false);
283
+ if (!exists) continue;
284
+ try {
285
+ let buf = await fsp.readFile(p);
286
+ if (this._opts.compress) buf = await asyncGunzip(buf);
287
+ this._store = toNullProto(JSON.parse(buf.toString('utf8')));
288
+ this.emit('restore', { generation: gen, path: p });
289
+ return;
290
+ } catch {}
291
+ }
292
+ this._store = Object.create(null);
293
+ this.emit('reset');
294
+ }
295
+
296
+ async _replayWAL() {
297
+ const exists = await fsp.access(this._walPath).then(() => true, () => false);
298
+ if (!exists) return 0;
299
+ let text;
300
+ try { text = await fsp.readFile(this._walPath, 'utf8'); } catch { return 0; }
301
+ let count = 0;
302
+ for (const line of text.split('\n')) {
303
+ const trimmed = line.trim();
304
+ if (!trimmed) continue;
305
+ try { this._applyEntry(JSON.parse(trimmed)); count++; } catch {}
306
+ }
307
+ return count;
308
+ }
309
+
310
+ _applyEntry(entry) {
311
+ switch (entry.op) {
312
+ case 'set': this._applySet(entry.k.split('.'), entry.v); break;
313
+ case 'del': this._applyDelete(entry.k.split('.')); break;
314
+ case 'clear': this._store = Object.create(null); break;
315
+ case 'batch': for (const op of entry.ops) this._applyEntry(op); break;
316
+ }
317
+ }
318
+
319
+ _applySet(parts, value) {
320
+ let node = this._store;
321
+ for (let i = 0; i < parts.length - 1; i++) {
322
+ const part = parts[i];
323
+ if (node[part] === null || node[part] === undefined || typeof node[part] !== 'object' || Array.isArray(node[part])) {
324
+ node[part] = Object.create(null);
325
+ }
326
+ node = node[part];
327
+ }
328
+ node[parts[parts.length - 1]] = toNullProto(value);
329
+ }
330
+
331
+ _applyDelete(parts) {
332
+ let node = this._store;
333
+ for (let i = 0; i < parts.length - 1; i++) {
334
+ if (!node || typeof node[parts[i]] !== 'object' || Array.isArray(node[parts[i]])) return false;
335
+ node = node[parts[i]];
336
+ }
337
+ const last = parts[parts.length - 1];
338
+ if (!node || !(last in node)) return false;
339
+ delete node[last];
340
+ return true;
341
+ }
342
+
343
+ _readRaw(key) {
344
+ const parts = key.split('.');
345
+ let node = this._store;
346
+ for (const part of parts) {
347
+ if (node === null || node === undefined || typeof node !== 'object') return undefined;
348
+ node = node[part];
349
+ }
350
+ return node;
351
+ }
352
+
353
+ _unwrap(value) {
354
+ if (this._encKey && value !== null && typeof value === 'object' && value.__enc === 1) {
355
+ return decryptValue(value, this._encKey);
356
+ }
357
+ return value;
358
+ }
359
+
360
+ _wrap(key, value) {
361
+ if (this._encKey && SENSITIVE_KEY_RE.test(key) && value !== null && value !== undefined) {
362
+ return encryptValue(value, this._encKey);
363
+ }
364
+ return value;
365
+ }
366
+
367
+ get(key) {
368
+ validateKey(key);
369
+ const cached = this._cache.get(key);
370
+ if (cached !== undefined) return cached;
371
+ const raw = this._readRaw(key);
372
+ if (raw === undefined) return null;
373
+ const value = this._unwrap(raw);
374
+ if (value !== null && value !== undefined) this._cache.set(key, value, this._opts.cacheTTL);
375
+ return value ?? null;
376
+ }
377
+
378
+ set(key, value) {
379
+ validateKey(key);
380
+ const stored = this._wrap(key, value);
381
+ this._applySet(key.split('.'), stored);
382
+ this._cache.invalidate(key);
383
+ this._queue.push(() => this._wal.append({ op: 'set', k: key, v: stored }).then(() => { this._walOps++; }));
384
+ this.emit('change', { type: 'set', key, value });
385
+ return value;
386
+ }
387
+
388
+ delete(key) {
389
+ validateKey(key);
390
+ const deleted = this._applyDelete(key.split('.'));
391
+ if (!deleted) return false;
392
+ this._cache.invalidate(key);
393
+ this._queue.push(() => this._wal.append({ op: 'del', k: key }).then(() => { this._walOps++; }));
394
+ this.emit('change', { type: 'delete', key });
395
+ return true;
396
+ }
397
+
398
+ has(key) { return this.get(key) !== null; }
399
+ add(key, n) { if (typeof n !== 'number' || !isFinite(n)) throw new TypeError('add() requires a finite number'); const cur = this.get(key) ?? 0; if (typeof cur !== 'number') throw new TypeError(`Value at "${key}" is not a number`); return this.set(key, cur + n); }
400
+ sub(key, n) { return this.add(key, -n); }
401
+
402
+ push(key, value) {
403
+ const arr = this.get(key) ?? [];
404
+ if (!Array.isArray(arr)) throw new TypeError(`Value at "${key}" is not an array`);
405
+ arr.push(value);
406
+ this.set(key, arr);
407
+ return arr.length;
408
+ }
409
+
410
+ pull(key, predicate) {
411
+ const arr = this.get(key) ?? [];
412
+ if (!Array.isArray(arr)) throw new TypeError(`Value at "${key}" is not an array`);
413
+ const test = typeof predicate === 'function' ? predicate : v => JSON.stringify(v) === JSON.stringify(predicate);
414
+ const index = arr.findIndex(test);
415
+ if (index === -1) return false;
416
+ arr.splice(index, 1);
417
+ this.set(key, arr);
418
+ return true;
419
+ }
420
+
421
+ async transaction(fn) {
422
+ return this._queue.push(async () => {
423
+ const snapshot = toNullProto(toPlainObject(this._store));
424
+ const ops = [];
425
+ const tx = {
426
+ set: (key, value) => {
427
+ validateKey(key);
428
+ const stored = this._wrap(key, value);
429
+ this._applySet(key.split('.'), stored);
430
+ this._cache.invalidate(key);
431
+ ops.push({ op: 'set', k: key, v: stored });
432
+ return value;
433
+ },
434
+ delete: (key) => {
435
+ validateKey(key);
436
+ const ok = this._applyDelete(key.split('.'));
437
+ if (ok) { this._cache.invalidate(key); ops.push({ op: 'del', k: key }); }
438
+ return ok;
439
+ },
440
+ get: (key) => this.get(key),
441
+ add: (key, n) => { const cur = tx.get(key) ?? 0; if (typeof cur !== 'number') throw new TypeError(`Value at "${key}" is not a number`); return tx.set(key, cur + n); },
442
+ sub: (key, n) => tx.add(key, -n),
443
+ push: (key, value) => { const arr = tx.get(key) ?? []; if (!Array.isArray(arr)) throw new TypeError(`Value at "${key}" is not an array`); arr.push(value); return tx.set(key, arr); },
444
+ pull: (key, pred) => { const arr = tx.get(key) ?? []; if (!Array.isArray(arr)) throw new TypeError(`Value at "${key}" is not an array`); const test = typeof pred === 'function' ? pred : v => JSON.stringify(v) === JSON.stringify(pred); const i = arr.findIndex(test); if (i === -1) return false; arr.splice(i, 1); return tx.set(key, arr); },
445
+ };
446
+ try {
447
+ const result = await fn(tx);
448
+ if (ops.length > 0) { await this._wal.append({ op: 'batch', ops }); this._walOps++; }
449
+ this.emit('transaction', { ops });
450
+ return result;
451
+ } catch (err) {
452
+ this._store = toNullProto(snapshot);
453
+ this._cache.clear();
454
+ this.emit('rollback', { error: err });
455
+ throw err;
456
+ }
457
+ });
458
+ }
459
+
460
+ all() {
461
+ const result = [];
462
+ const traverse = (obj, prefix) => {
463
+ for (const key of Object.keys(obj)) {
464
+ const fullKey = prefix ? `${prefix}.${key}` : key;
465
+ const val = obj[key];
466
+ if (val !== null && typeof val === 'object' && !Array.isArray(val) && val.__enc !== 1) {
467
+ traverse(val, fullKey);
468
+ } else {
469
+ result.push({ ID: fullKey, data: this._unwrap(val) });
470
+ }
471
+ }
472
+ };
473
+ traverse(this._store, '');
474
+ return result;
475
+ }
476
+
477
+ filter(predicate) { return this.all().filter(({ ID, data }) => predicate(data, ID)); }
478
+ find(predicate) { return this.all().find(({ ID, data }) => predicate(data, ID)) ?? null; }
479
+ startsWith(prefix) { return this.all().filter(e => e.ID.startsWith(prefix)); }
480
+
481
+ paginate(prefix, page = 1, limit = 10, sortBy = 'data', sortDesc = true) {
482
+ const data = this.startsWith(prefix).sort((a, b) => {
483
+ const vA = a[sortBy], vB = b[sortBy];
484
+ if (vA === vB) return 0;
485
+ return sortDesc ? (vA < vB ? 1 : -1) : (vA > vB ? 1 : -1);
486
+ });
487
+ const total = data.length;
488
+ const pages = Math.ceil(total / limit) || 1;
489
+ const start = (page - 1) * limit;
490
+ return { data: data.slice(start, start + limit), pagination: { page, limit, total, pages, hasNext: page < pages, hasPrev: page > 1 } };
491
+ }
492
+
493
+ table(name) {
494
+ if (typeof name !== 'string' || name.length === 0) throw new TypeError('Table name must be a non-empty string');
495
+ validateKey(name);
496
+ return new Table(this, name);
497
+ }
498
+
499
+ async compact() { return this._queue.push(() => this._compact()); }
500
+
501
+ async _compact() {
502
+ if (this._closed) return;
503
+ await this._rotateBackups();
504
+ const plain = toPlainObject(this._store);
505
+ let content = Buffer.from(JSON.stringify(plain), 'utf8');
506
+ if (this._opts.compress) content = await asyncGzip(content, { level: 1 });
507
+ const tmp = uniqueTmp(this._snapshotPath);
508
+ try {
509
+ await fsp.writeFile(tmp, content);
510
+ await fsp.rename(tmp, this._snapshotPath);
511
+ } catch (err) {
512
+ await fsp.unlink(tmp).catch(() => {});
513
+ throw err;
514
+ }
515
+ await this._wal.truncateAndReopen();
516
+ this._walOps = 0;
517
+ this.emit('save', this.getStats());
518
+ }
519
+
520
+ async _rotateBackups() {
521
+ for (let gen = this._opts.backupCount; gen >= 1; gen--) {
522
+ const src = gen === 1 ? this._snapshotPath : `${this._snapshotPath}.${gen - 1}.bak`;
523
+ const dst = `${this._snapshotPath}.${gen}.bak`;
524
+ const exists = await fsp.access(src).then(() => true, () => false);
525
+ if (exists) await fsp.rename(src, dst).catch(() => {});
526
+ }
527
+ }
528
+
529
+ async save() { return this._queue.push(() => this._compact()); }
530
+
531
+ async clear() {
532
+ this._store = Object.create(null);
533
+ this._cache.clear();
534
+ this._queue.push(() => this._wal.append({ op: 'clear' }).then(() => { this._walOps++; }));
535
+ this.emit('clear');
536
+ return this;
537
+ }
538
+
539
+ async close() {
540
+ if (this._closed) return;
541
+ this._closed = true;
542
+ if (this._compactTimer) { clearInterval(this._compactTimer); this._compactTimer = null; }
543
+ await this._queue.push(async () => { if (this._walOps > 0) await this._compact(); });
544
+ await this._wal.close();
545
+ this.removeAllListeners();
546
+ }
547
+
548
+ getStats() {
549
+ const fileSize = fs.existsSync(this._snapshotPath) ? fs.statSync(this._snapshotPath).size : 0;
550
+ return {
551
+ driver: 'spectre.db',
552
+ compress: this._opts.compress,
553
+ encrypted: this._encKey !== null,
554
+ entries: this.all().length,
555
+ cacheSize: this._cache.size,
556
+ maxCacheSize: this._opts.maxCacheSize,
557
+ fileSize,
558
+ walOps: this._walOps,
559
+ compactThreshold: this._opts.compactThreshold,
560
+ shards: 0,
561
+ snapshotPath: this._snapshotPath,
562
+ walPath: this._walPath,
563
+ };
564
+ }
565
+ }
566
+
567
+ class Table {
568
+ constructor(db, name) {
569
+ this._db = db;
570
+ this._name = name;
571
+ }
572
+
573
+ _k(key) { return `${this._name}.${key}`; }
574
+ get(key) { return this._db.get(this._k(key)); }
575
+ set(key, value) { return this._db.set(this._k(key), value); }
576
+ has(key) { return this._db.has(this._k(key)); }
577
+ delete(key) { return this._db.delete(this._k(key)); }
578
+ add(key, n) { return this._db.add(this._k(key), n); }
579
+ sub(key, n) { return this._db.sub(this._k(key), n); }
580
+ push(key, value) { return this._db.push(this._k(key), value); }
581
+ pull(key, pred) { return this._db.pull(this._k(key), pred); }
582
+ all() { return this._db.startsWith(`${this._name}.`); }
583
+ count() { return this.all().length; }
584
+
585
+ async clear() {
586
+ return this._db.transaction(tx => { for (const { ID } of this.all()) tx.delete(ID); });
587
+ }
588
+
589
+ async transaction(fn) {
590
+ return this._db.transaction(tx => {
591
+ const scoped = {
592
+ set: (key, value) => tx.set(this._k(key), value),
593
+ delete: (key) => tx.delete(this._k(key)),
594
+ add: (key, n) => tx.add(this._k(key), n),
595
+ sub: (key, n) => tx.sub(this._k(key), n),
596
+ push: (key, value) => tx.push(this._k(key), value),
597
+ pull: (key, pred) => tx.pull(this._k(key), pred),
598
+ get: (key) => tx.get(this._k(key)),
599
+ };
600
+ return fn(scoped);
601
+ });
602
+ }
603
+ }
604
+
605
+ module.exports = { Engine, Table };