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