deepbase-sqlite 3.4.10 → 3.4.12
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/package.json +2 -2
- package/src/SqliteDriver.js +19 -8
- package/test/test.js +245 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepbase-sqlite",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.12",
|
|
4
4
|
"description": "⚡ DeepBase SQLite - SQLite database driver",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.cjs",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"better-sqlite3": "^11.8.1"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"deepbase": "^3.4.
|
|
20
|
+
"deepbase": "^3.4.12"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"test": "mocha test/test.js"
|
package/src/SqliteDriver.js
CHANGED
|
@@ -25,6 +25,8 @@ export class SqliteDriver extends DeepBaseDriver {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
async connect() {
|
|
28
|
+
if (this._connected) return;
|
|
29
|
+
|
|
28
30
|
if (!fs.existsSync(this.path)) {
|
|
29
31
|
fs.mkdirSync(this.path, { recursive: true });
|
|
30
32
|
}
|
|
@@ -44,7 +46,7 @@ export class SqliteDriver extends DeepBaseDriver {
|
|
|
44
46
|
this.setStmt = this.db.prepare('INSERT OR REPLACE INTO deepbase (key, value) VALUES (?, ?)');
|
|
45
47
|
this.delStmt = this.db.prepare('DELETE FROM deepbase WHERE key = ?');
|
|
46
48
|
this.getAllStmt = this.db.prepare('SELECT key, value FROM deepbase');
|
|
47
|
-
this.getKeysLikeStmt = this.db.prepare('SELECT key, value FROM deepbase WHERE key LIKE ?');
|
|
49
|
+
this.getKeysLikeStmt = this.db.prepare('SELECT key, value FROM deepbase WHERE key LIKE ? ESCAPE \'!\'');
|
|
48
50
|
|
|
49
51
|
this._connected = true;
|
|
50
52
|
}
|
|
@@ -54,6 +56,7 @@ export class SqliteDriver extends DeepBaseDriver {
|
|
|
54
56
|
this.db.close();
|
|
55
57
|
this.db = null;
|
|
56
58
|
}
|
|
59
|
+
this._connected = false;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
async get(...args) {
|
|
@@ -70,8 +73,7 @@ export class SqliteDriver extends DeepBaseDriver {
|
|
|
70
73
|
const row = this.getStmt.get(key);
|
|
71
74
|
|
|
72
75
|
// Check if there are child keys (nested properties)
|
|
73
|
-
const
|
|
74
|
-
const children = this.getKeysLikeStmt.all(childKey + '%');
|
|
76
|
+
const children = this.getKeysLikeStmt.all(this._likePrefix(key));
|
|
75
77
|
|
|
76
78
|
// If there are children, build object from them
|
|
77
79
|
if (children.length > 0) {
|
|
@@ -145,8 +147,7 @@ export class SqliteDriver extends DeepBaseDriver {
|
|
|
145
147
|
this.delStmt.run(key);
|
|
146
148
|
|
|
147
149
|
// Delete all children
|
|
148
|
-
|
|
149
|
-
this.db.prepare('DELETE FROM deepbase WHERE key LIKE ?').run(childKey + '%');
|
|
150
|
+
this.db.prepare('DELETE FROM deepbase WHERE key LIKE ? ESCAPE \'!\'').run(this._likePrefix(key));
|
|
150
151
|
});
|
|
151
152
|
|
|
152
153
|
transaction();
|
|
@@ -213,8 +214,7 @@ export class SqliteDriver extends DeepBaseDriver {
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
_buildObjectFromChildren(parentKey) {
|
|
216
|
-
const
|
|
217
|
-
const rows = this.getKeysLikeStmt.all(prefix + '%');
|
|
217
|
+
const rows = this.getKeysLikeStmt.all(parentKey ? this._likePrefix(parentKey) : '%');
|
|
218
218
|
const result = {};
|
|
219
219
|
|
|
220
220
|
for (const row of rows) {
|
|
@@ -230,7 +230,17 @@ export class SqliteDriver extends DeepBaseDriver {
|
|
|
230
230
|
return result;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
_escapeLikePattern(str) {
|
|
234
|
+
return str.replace(/[!%_]/g, '!$&');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_likePrefix(key) {
|
|
238
|
+
return this._escapeLikePattern(key) + '.%';
|
|
239
|
+
}
|
|
240
|
+
|
|
233
241
|
_setNestedValue(obj, path, value) {
|
|
242
|
+
if (path.length === 0) return;
|
|
243
|
+
|
|
234
244
|
if (path.length === 1) {
|
|
235
245
|
obj[path[0]] = value;
|
|
236
246
|
return;
|
|
@@ -248,7 +258,8 @@ export class SqliteDriver extends DeepBaseDriver {
|
|
|
248
258
|
const entries = [];
|
|
249
259
|
|
|
250
260
|
for (const [key, value] of Object.entries(obj)) {
|
|
251
|
-
const
|
|
261
|
+
const escapedKey = this._escapeDots(String(key));
|
|
262
|
+
const fullKey = prefix ? `${prefix}.${escapedKey}` : escapedKey;
|
|
252
263
|
|
|
253
264
|
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
254
265
|
entries.push(...this._flattenObject(value, fullKey));
|
package/test/test.js
CHANGED
|
@@ -388,6 +388,20 @@ describe('SqliteDriver', function() {
|
|
|
388
388
|
const result = await db.get('before');
|
|
389
389
|
assert.strictEqual(result, 'disconnect');
|
|
390
390
|
});
|
|
391
|
+
|
|
392
|
+
it('should be a no-op when already connected', async function() {
|
|
393
|
+
await db.set('key', 'value');
|
|
394
|
+
|
|
395
|
+
// connect again while already connected
|
|
396
|
+
await db.connect();
|
|
397
|
+
|
|
398
|
+
// data and operations should be unaffected
|
|
399
|
+
const result = await db.get('key');
|
|
400
|
+
assert.strictEqual(result, 'value');
|
|
401
|
+
|
|
402
|
+
await db.set('key2', 'value2');
|
|
403
|
+
assert.strictEqual(await db.get('key2'), 'value2');
|
|
404
|
+
});
|
|
391
405
|
});
|
|
392
406
|
|
|
393
407
|
describe('Singleton Pattern', function() {
|
|
@@ -501,6 +515,237 @@ describe('SqliteDriver', function() {
|
|
|
501
515
|
});
|
|
502
516
|
});
|
|
503
517
|
|
|
518
|
+
describe('Object Expansion with Special Keys', function() {
|
|
519
|
+
it('should handle object with dotted keys when expanding', async function() {
|
|
520
|
+
// Store an object with dots in its property names
|
|
521
|
+
await db.set('config', { 'server.port': 8080, 'server.host': 'localhost' });
|
|
522
|
+
|
|
523
|
+
// Trigger expansion by setting a nested property
|
|
524
|
+
await db.set('config', 'debug', true);
|
|
525
|
+
|
|
526
|
+
const config = await db.get('config');
|
|
527
|
+
assert.strictEqual(config['server.port'], 8080);
|
|
528
|
+
assert.strictEqual(config['server.host'], 'localhost');
|
|
529
|
+
assert.strictEqual(config.debug, true);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('should not crash on objects with empty string keys', async function() {
|
|
533
|
+
// This was the root cause of the production crash:
|
|
534
|
+
// empty string key produces trailing dot in DB, leading to empty path
|
|
535
|
+
await db.set('data', { '': 'empty_key_value', 'normal': 'ok' });
|
|
536
|
+
|
|
537
|
+
// Trigger expansion
|
|
538
|
+
await db.set('data', 'extra', 123);
|
|
539
|
+
|
|
540
|
+
const data = await db.get('data');
|
|
541
|
+
assert.strictEqual(data[''], 'empty_key_value');
|
|
542
|
+
assert.strictEqual(data.normal, 'ok');
|
|
543
|
+
assert.strictEqual(data.extra, 123);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('should handle nested objects with dotted keys during expansion', async function() {
|
|
547
|
+
await db.set('replace', {
|
|
548
|
+
'{lang}': 'es',
|
|
549
|
+
'{first_name}': 'Martin',
|
|
550
|
+
'@main': 'martin'
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Trigger expansion
|
|
554
|
+
await db.set('replace', '{lang}', 'en');
|
|
555
|
+
|
|
556
|
+
const result = await db.get('replace');
|
|
557
|
+
assert.strictEqual(result['{lang}'], 'en');
|
|
558
|
+
assert.strictEqual(result['{first_name}'], 'Martin');
|
|
559
|
+
assert.strictEqual(result['@main'], 'martin');
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should preserve dots in keys through set/get root object cycle', async function() {
|
|
563
|
+
const data = {
|
|
564
|
+
'api.v1.endpoint': 'https://api.example.com',
|
|
565
|
+
'api.v2.endpoint': 'https://v2.api.example.com'
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
await db.set(data);
|
|
569
|
+
const result = await db.get();
|
|
570
|
+
assert.deepStrictEqual(result, data);
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
describe('Keys with Underscores (SQL LIKE safety)', function() {
|
|
575
|
+
it('should not cross-match keys with underscores', async function() {
|
|
576
|
+
// _ is a SQL LIKE wildcard that matches any single character
|
|
577
|
+
await db.set('chat_1', 'msg', 'hello');
|
|
578
|
+
await db.set('chatX1', 'msg', 'world');
|
|
579
|
+
|
|
580
|
+
const chat1 = await db.get('chat_1');
|
|
581
|
+
assert.deepStrictEqual(chat1, { msg: 'hello' });
|
|
582
|
+
|
|
583
|
+
const chatX1 = await db.get('chatX1');
|
|
584
|
+
assert.deepStrictEqual(chatX1, { msg: 'world' });
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('should isolate data between similar keys with underscores', async function() {
|
|
588
|
+
await db.set('user_42', 'name', 'Alice');
|
|
589
|
+
await db.set('userX42', 'name', 'Bob');
|
|
590
|
+
await db.set('user.42', 'name', 'Charlie');
|
|
591
|
+
|
|
592
|
+
const u1 = await db.get('user_42');
|
|
593
|
+
const u2 = await db.get('userX42');
|
|
594
|
+
const u3 = await db.get('user.42');
|
|
595
|
+
|
|
596
|
+
assert.deepStrictEqual(u1, { name: 'Alice' });
|
|
597
|
+
assert.deepStrictEqual(u2, { name: 'Bob' });
|
|
598
|
+
assert.deepStrictEqual(u3, { name: 'Charlie' });
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('should delete only matching keys with underscores', async function() {
|
|
602
|
+
await db.set('item_a', 'x', 1);
|
|
603
|
+
await db.set('itemXa', 'x', 2);
|
|
604
|
+
|
|
605
|
+
await db.del('item_a');
|
|
606
|
+
|
|
607
|
+
assert.strictEqual(await db.get('item_a'), null);
|
|
608
|
+
assert.deepStrictEqual(await db.get('itemXa'), { x: 2 });
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('should handle keys with percent signs', async function() {
|
|
612
|
+
await db.set('progress', '100%', 'done', true);
|
|
613
|
+
await db.set('progress', 'abc', 'done', false);
|
|
614
|
+
|
|
615
|
+
const p100 = await db.get('progress', '100%');
|
|
616
|
+
assert.deepStrictEqual(p100, { done: true });
|
|
617
|
+
|
|
618
|
+
const pabc = await db.get('progress', 'abc');
|
|
619
|
+
assert.deepStrictEqual(pabc, { done: false });
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
describe('Storybot-like Structure', function() {
|
|
624
|
+
it('should handle dotted top-level keys with deep nesting', async function() {
|
|
625
|
+
const sessionKey = 'stm.1769201539421.dawduxooy';
|
|
626
|
+
|
|
627
|
+
await db.set(sessionKey, 'started', 'LuaGardenbot', true);
|
|
628
|
+
await db.set(sessionKey, 'active', 'LuaGardenbot', true);
|
|
629
|
+
await db.set(sessionKey, 'active', 'AmyWaybot', true);
|
|
630
|
+
|
|
631
|
+
const session = await db.get(sessionKey);
|
|
632
|
+
assert.deepStrictEqual(session, {
|
|
633
|
+
started: { LuaGardenbot: true },
|
|
634
|
+
active: { LuaGardenbot: true, AmyWaybot: true }
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('should handle replace map with special chars in keys', async function() {
|
|
639
|
+
const sessionKey = 'stm.12345.abc';
|
|
640
|
+
|
|
641
|
+
await db.set(sessionKey, 'replace', {
|
|
642
|
+
'{lang}': 'es',
|
|
643
|
+
'{vip_token}': '',
|
|
644
|
+
'{first_name}': 'STM User',
|
|
645
|
+
'{last_name}': '',
|
|
646
|
+
'{crypto_name}': 'kkerfhkkzgf',
|
|
647
|
+
'@main': 'martin'
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
const replace = await db.get(sessionKey, 'replace');
|
|
651
|
+
assert.strictEqual(replace['{lang}'], 'es');
|
|
652
|
+
assert.strictEqual(replace['@main'], 'martin');
|
|
653
|
+
assert.strictEqual(replace['{first_name}'], 'STM User');
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('should handle node history with numeric-like keys', async function() {
|
|
657
|
+
const sessionKey = 'stm.999.xyz';
|
|
658
|
+
|
|
659
|
+
await db.set(sessionKey, 'node', 'history', 'c0', {
|
|
660
|
+
from: 'LuaGardenbot',
|
|
661
|
+
message: 'new contact',
|
|
662
|
+
timestamp: 1771012955839
|
|
663
|
+
});
|
|
664
|
+
await db.set(sessionKey, 'node', 'history', '0-2760293686', {
|
|
665
|
+
from: 'LuaGardenbot',
|
|
666
|
+
message: 'Hey, I\'m Lua',
|
|
667
|
+
timestamp: 1771012956863
|
|
668
|
+
});
|
|
669
|
+
await db.set(sessionKey, 'node', 'history', '0-4151942372-o', {
|
|
670
|
+
from: 'main',
|
|
671
|
+
to: 'LuaGardenbot',
|
|
672
|
+
message: 'hola',
|
|
673
|
+
timestamp: 1771012960058
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
const history = await db.get(sessionKey, 'node', 'history');
|
|
677
|
+
assert.strictEqual(Object.keys(history).length, 3);
|
|
678
|
+
assert.strictEqual(history['c0'].from, 'LuaGardenbot');
|
|
679
|
+
assert.strictEqual(history['0-2760293686'].message, 'Hey, I\'m Lua');
|
|
680
|
+
assert.strictEqual(history['0-4151942372-o'].to, 'LuaGardenbot');
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('should handle object expansion with replace map then set nested', async function() {
|
|
684
|
+
const sessionKey = 'stm.12345.abc';
|
|
685
|
+
|
|
686
|
+
// Store replace map as object
|
|
687
|
+
await db.set(sessionKey, 'replace', {
|
|
688
|
+
'{lang}': 'es',
|
|
689
|
+
'{first_name}': 'User'
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// Now update a single key — triggers _expandParentObjects
|
|
693
|
+
await db.set(sessionKey, 'replace', '{lang}', 'en');
|
|
694
|
+
|
|
695
|
+
const replace = await db.get(sessionKey, 'replace');
|
|
696
|
+
assert.strictEqual(replace['{lang}'], 'en');
|
|
697
|
+
assert.strictEqual(replace['{first_name}'], 'User');
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('should handle full session lifecycle without crash', async function() {
|
|
701
|
+
const sessionKey = 'stm.1769201539421.dawduxooy';
|
|
702
|
+
|
|
703
|
+
// Initial object-style set
|
|
704
|
+
await db.set(sessionKey, 'replace', {
|
|
705
|
+
'{lang}': 'es',
|
|
706
|
+
'{vip_token}': '',
|
|
707
|
+
'{first_name}': 'STM User'
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
await db.set(sessionKey, 'config', { country: '', gender: 'M' });
|
|
711
|
+
await db.set(sessionKey, 'tokens', { input: 4554, output: 229, cost: 0.0079825 });
|
|
712
|
+
|
|
713
|
+
// Nested individual sets
|
|
714
|
+
await db.set(sessionKey, 'started', 'LuaGardenbot', true);
|
|
715
|
+
await db.set(sessionKey, 'active', 'LuaGardenbot', true);
|
|
716
|
+
await db.set(sessionKey, 'active', 'AmyWaybot', true);
|
|
717
|
+
await db.set(sessionKey, 'node', 'current', 'chapter', 2);
|
|
718
|
+
await db.set(sessionKey, 'node', 'current', 'id', '50-326833713');
|
|
719
|
+
|
|
720
|
+
// Update inside previously stored object — triggers expansion
|
|
721
|
+
await db.set(sessionKey, 'replace', '{lang}', 'en');
|
|
722
|
+
await db.set(sessionKey, 'config', 'country', 'AR');
|
|
723
|
+
|
|
724
|
+
// Verify everything
|
|
725
|
+
const session = await db.get(sessionKey);
|
|
726
|
+
|
|
727
|
+
assert.strictEqual(session.replace['{lang}'], 'en');
|
|
728
|
+
assert.strictEqual(session.replace['{first_name}'], 'STM User');
|
|
729
|
+
assert.strictEqual(session.config.country, 'AR');
|
|
730
|
+
assert.strictEqual(session.config.gender, 'M');
|
|
731
|
+
assert.strictEqual(session.tokens.cost, 0.0079825);
|
|
732
|
+
assert.strictEqual(session.started.LuaGardenbot, true);
|
|
733
|
+
assert.strictEqual(session.active.AmyWaybot, true);
|
|
734
|
+
assert.strictEqual(session.node.current.chapter, 2);
|
|
735
|
+
assert.strictEqual(session.node.current.id, '50-326833713');
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('should not mix sessions with similar dotted keys', async function() {
|
|
739
|
+
await db.set('stm.111.aaa', 'data', 'session1');
|
|
740
|
+
await db.set('stm.111.bbb', 'data', 'session2');
|
|
741
|
+
await db.set('stm.222.aaa', 'data', 'session3');
|
|
742
|
+
|
|
743
|
+
assert.deepStrictEqual(await db.get('stm.111.aaa'), { data: 'session1' });
|
|
744
|
+
assert.deepStrictEqual(await db.get('stm.111.bbb'), { data: 'session2' });
|
|
745
|
+
assert.deepStrictEqual(await db.get('stm.222.aaa'), { data: 'session3' });
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|
|
504
749
|
describe('Race Conditions', function() {
|
|
505
750
|
it('should handle 100 concurrent increments correctly', async function() {
|
|
506
751
|
this.timeout(5000);
|