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