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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepbase-sqlite",
3
- "version": "3.4.10",
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.10"
20
+ "deepbase": "^3.4.12"
21
21
  },
22
22
  "scripts": {
23
23
  "test": "mocha test/test.js"
@@ -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 childKey = key + '.';
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
- const childKey = key + '.';
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 prefix = parentKey ? parentKey + '.' : '';
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 fullKey = prefix ? `${prefix}.${key}` : key;
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);