deepbase-sqlite 3.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/LICENSE +23 -0
- package/README.md +205 -0
- package/package.json +47 -0
- package/src/SqliteDriver.js +303 -0
- package/src/index.js +4 -0
- package/test/test.js +446 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 mclasen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# deepbase-sqlite
|
|
2
|
+
|
|
3
|
+
SQLite driver for DeepBase.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install deepbase deepbase-sqlite
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Description
|
|
12
|
+
|
|
13
|
+
Stores data in SQLite database files. Perfect for:
|
|
14
|
+
|
|
15
|
+
- ✅ Production applications
|
|
16
|
+
- ✅ Medium to large datasets
|
|
17
|
+
- ✅ Fast queries and transactions
|
|
18
|
+
- ✅ ACID compliance
|
|
19
|
+
- ✅ Embedded database solution
|
|
20
|
+
- ✅ Zero configuration needed
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
import DeepBase from 'deepbase';
|
|
26
|
+
import SqliteDriver from 'deepbase-sqlite';
|
|
27
|
+
|
|
28
|
+
const db = new DeepBase(new SqliteDriver({
|
|
29
|
+
path: './data',
|
|
30
|
+
name: 'mydb'
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
await db.connect();
|
|
34
|
+
|
|
35
|
+
await db.set('users', 'alice', { name: 'Alice', age: 30 });
|
|
36
|
+
const alice = await db.get('users', 'alice');
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Options
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
new SqliteDriver({
|
|
43
|
+
path: './data', // Directory to store database files
|
|
44
|
+
name: 'default', // Database filename (without .db)
|
|
45
|
+
nidAlphabet: 'ABC...', // Alphabet for ID generation
|
|
46
|
+
nidLength: 10 // Length of generated IDs
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
### High Performance
|
|
53
|
+
|
|
54
|
+
Uses `better-sqlite3` for synchronous operations wrapped in async API:
|
|
55
|
+
|
|
56
|
+
- Prepared statements for optimal performance
|
|
57
|
+
- Transaction support for batch operations
|
|
58
|
+
- Fast lookups with indexed keys
|
|
59
|
+
|
|
60
|
+
### Singleton Pattern
|
|
61
|
+
|
|
62
|
+
Multiple instances pointing to the same database file will share the same connection:
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
const db1 = new DeepBase(new SqliteDriver({ name: 'mydb' }));
|
|
66
|
+
const db2 = new DeepBase(new SqliteDriver({ name: 'mydb' }));
|
|
67
|
+
// Both use the same underlying database connection
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Nested Data Structure
|
|
71
|
+
|
|
72
|
+
Efficiently stores nested objects using a key-value schema:
|
|
73
|
+
|
|
74
|
+
- Keys are stored as dot-notation paths (e.g., `user.profile.name`)
|
|
75
|
+
- Values are stored as JSON
|
|
76
|
+
- Fast lookups for both exact keys and partial paths
|
|
77
|
+
|
|
78
|
+
### ACID Compliance
|
|
79
|
+
|
|
80
|
+
SQLite provides:
|
|
81
|
+
|
|
82
|
+
- **Atomicity**: All operations complete or none do
|
|
83
|
+
- **Consistency**: Data remains valid across transactions
|
|
84
|
+
- **Isolation**: Concurrent operations don't interfere
|
|
85
|
+
- **Durability**: Committed data persists even after crashes
|
|
86
|
+
|
|
87
|
+
## Database Structure
|
|
88
|
+
|
|
89
|
+
Data is stored in a simple key-value table:
|
|
90
|
+
|
|
91
|
+
```sql
|
|
92
|
+
CREATE TABLE deepbase (
|
|
93
|
+
key TEXT PRIMARY KEY,
|
|
94
|
+
value TEXT NOT NULL
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Example data:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
key | value
|
|
102
|
+
-----------------------|------------------
|
|
103
|
+
users.alice.name | "Alice"
|
|
104
|
+
users.alice.age | 30
|
|
105
|
+
users.bob.name | "Bob"
|
|
106
|
+
users.bob.age | 25
|
|
107
|
+
config.theme | "dark"
|
|
108
|
+
config.lang | "en"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Use Cases
|
|
112
|
+
|
|
113
|
+
- **Production Apps**: Reliable embedded database for web/desktop apps
|
|
114
|
+
- **Medium Datasets**: Handles millions of records efficiently
|
|
115
|
+
- **Offline First**: Works without network or external database server
|
|
116
|
+
- **Desktop Apps**: Perfect for Electron or Tauri applications
|
|
117
|
+
- **Mobile Apps**: Lightweight database for React Native/Capacitor
|
|
118
|
+
- **IoT Devices**: Embedded storage for edge computing
|
|
119
|
+
- **Serverless**: Deploy with your functions, no external DB needed
|
|
120
|
+
|
|
121
|
+
## Performance
|
|
122
|
+
|
|
123
|
+
SQLite offers excellent performance:
|
|
124
|
+
|
|
125
|
+
- Fast reads and writes with prepared statements
|
|
126
|
+
- Efficient indexing for quick lookups
|
|
127
|
+
- Transaction batching for bulk operations
|
|
128
|
+
- Low memory footprint
|
|
129
|
+
|
|
130
|
+
## Migration
|
|
131
|
+
|
|
132
|
+
Easy to migrate between SQLite and other drivers:
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
import DeepBase from 'deepbase';
|
|
136
|
+
import SqliteDriver from 'deepbase-sqlite';
|
|
137
|
+
import MongoDriver from 'deepbase-mongodb';
|
|
138
|
+
|
|
139
|
+
const db = new DeepBase([
|
|
140
|
+
new SqliteDriver({ path: './data' }),
|
|
141
|
+
new MongoDriver({ url: 'mongodb://localhost:27017' })
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
await db.connect();
|
|
145
|
+
await db.migrate(0, 1); // Migrate SQLite to MongoDB
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## File Structure
|
|
149
|
+
|
|
150
|
+
Data is stored as SQLite database files:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
data/
|
|
154
|
+
mydb.db
|
|
155
|
+
users.db
|
|
156
|
+
config.db
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Comparison with JSON Driver
|
|
160
|
+
|
|
161
|
+
| Feature | SQLite | JSON |
|
|
162
|
+
|---------|--------|------|
|
|
163
|
+
| Performance | ⚡ Very Fast | 🐌 Slower for large data |
|
|
164
|
+
| File Size | 📦 Compact | 📄 Human readable |
|
|
165
|
+
| Transactions | ✅ ACID | ❌ No transactions |
|
|
166
|
+
| Query Speed | 🚀 Indexed | 🔍 Full scan |
|
|
167
|
+
| Reliability | 💪 Very High | ⚠️ File corruption risk |
|
|
168
|
+
| Debugging | 🔧 SQL tools | 👁️ Easy to inspect |
|
|
169
|
+
|
|
170
|
+
## Best Practices
|
|
171
|
+
|
|
172
|
+
### Use Transactions for Bulk Operations
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
// Better: Use root object set for bulk inserts
|
|
176
|
+
const data = {
|
|
177
|
+
user1: { name: 'Alice' },
|
|
178
|
+
user2: { name: 'Bob' },
|
|
179
|
+
user3: { name: 'Charlie' }
|
|
180
|
+
};
|
|
181
|
+
await db.set('users', data);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Disconnect Properly
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
// Always disconnect to close database connection
|
|
188
|
+
await db.disconnect();
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Use Appropriate Paths
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
// Good: Organize data hierarchically
|
|
195
|
+
await db.set('users', userId, 'profile', data);
|
|
196
|
+
|
|
197
|
+
// Avoid: Flat structure loses benefits of nesting
|
|
198
|
+
await db.set(`user_${userId}_profile`, data);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT - Copyright (c) Martin Clasen
|
|
204
|
+
|
|
205
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "deepbase-sqlite",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "⚡ DeepBase SQLite - SQLite database driver",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"module": "src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"better-sqlite3": "^11.8.1"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"deepbase": "^3.0.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "mocha test/test.js"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"mocha": "^10.8.2"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/clasen/DeepBase.git"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"deepbase",
|
|
31
|
+
"sqlite",
|
|
32
|
+
"sqlite3",
|
|
33
|
+
"driver",
|
|
34
|
+
"database",
|
|
35
|
+
"persist",
|
|
36
|
+
"nested",
|
|
37
|
+
"objects"
|
|
38
|
+
],
|
|
39
|
+
"author": "Martin Clasen",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/clasen/DeepBase/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/clasen/DeepBase#readme"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { DeepBaseDriver } from 'deepbase';
|
|
2
|
+
import Database from 'better-sqlite3';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import * as pathModule from 'path';
|
|
5
|
+
import { customAlphabet } from 'nanoid';
|
|
6
|
+
|
|
7
|
+
export class SqliteDriver extends DeepBaseDriver {
|
|
8
|
+
static _instances = {};
|
|
9
|
+
|
|
10
|
+
constructor({name, path, ...opts} = {}) {
|
|
11
|
+
super(opts);
|
|
12
|
+
|
|
13
|
+
this.name = name || "default";
|
|
14
|
+
this.path = path || './db';
|
|
15
|
+
this.nanoid = customAlphabet(this.nidAlphabet, this.nidLength);
|
|
16
|
+
|
|
17
|
+
this.path = pathModule.resolve(this.path);
|
|
18
|
+
this.fileName = pathModule.join(this.path, `${this.name}.db`);
|
|
19
|
+
|
|
20
|
+
// Singleton pattern per file
|
|
21
|
+
if (SqliteDriver._instances[this.fileName]) {
|
|
22
|
+
return SqliteDriver._instances[this.fileName];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.db = null;
|
|
26
|
+
SqliteDriver._instances[this.fileName] = this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async connect() {
|
|
30
|
+
if (!fs.existsSync(this.path)) {
|
|
31
|
+
fs.mkdirSync(this.path, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.db = new Database(this.fileName);
|
|
35
|
+
|
|
36
|
+
// Create table if it doesn't exist
|
|
37
|
+
this.db.exec(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS deepbase (
|
|
39
|
+
key TEXT PRIMARY KEY,
|
|
40
|
+
value TEXT NOT NULL
|
|
41
|
+
)
|
|
42
|
+
`);
|
|
43
|
+
|
|
44
|
+
// Prepare statements for better performance
|
|
45
|
+
this.getStmt = this.db.prepare('SELECT value FROM deepbase WHERE key = ?');
|
|
46
|
+
this.setStmt = this.db.prepare('INSERT OR REPLACE INTO deepbase (key, value) VALUES (?, ?)');
|
|
47
|
+
this.delStmt = this.db.prepare('DELETE FROM deepbase WHERE key = ?');
|
|
48
|
+
this.getAllStmt = this.db.prepare('SELECT key, value FROM deepbase');
|
|
49
|
+
this.getKeysLikeStmt = this.db.prepare('SELECT key, value FROM deepbase WHERE key LIKE ?');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async disconnect() {
|
|
53
|
+
if (this.db) {
|
|
54
|
+
this.db.close();
|
|
55
|
+
this.db = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async get(...args) {
|
|
60
|
+
if (args.length === 0) {
|
|
61
|
+
// Get root object
|
|
62
|
+
return this._getRootObject();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const key = this._pathToKey(args);
|
|
66
|
+
const row = this.getStmt.get(key);
|
|
67
|
+
|
|
68
|
+
// Check if there are child keys (nested properties)
|
|
69
|
+
const childKey = key + '.';
|
|
70
|
+
const children = this.getKeysLikeStmt.all(childKey + '%');
|
|
71
|
+
|
|
72
|
+
// If there are children, build object from them
|
|
73
|
+
if (children.length > 0) {
|
|
74
|
+
return this._buildObjectFromChildren(key);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// If direct key exists and no children, return it
|
|
78
|
+
if (row) {
|
|
79
|
+
return JSON.parse(row.value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if we need to look into a parent object
|
|
83
|
+
// For example: if we're looking for 'users.abc.name' but only 'users.abc' exists as a JSON object
|
|
84
|
+
const parentPath = this._findParentWithValue(args);
|
|
85
|
+
if (parentPath) {
|
|
86
|
+
const parentKey = this._pathToKey(parentPath);
|
|
87
|
+
const parentRow = this.getStmt.get(parentKey);
|
|
88
|
+
if (parentRow) {
|
|
89
|
+
const parentValue = JSON.parse(parentRow.value);
|
|
90
|
+
const relativePath = args.slice(parentPath.length);
|
|
91
|
+
return this._getFromObject(parentValue, relativePath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async set(...args) {
|
|
99
|
+
if (args.length === 0) {
|
|
100
|
+
throw new Error('set() requires at least one argument');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (args.length === 1) {
|
|
104
|
+
// Setting root object
|
|
105
|
+
await this._setRootObject(args[0]);
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const keys = args.slice(0, -1);
|
|
110
|
+
const value = args[args.length - 1];
|
|
111
|
+
const key = this._pathToKey(keys);
|
|
112
|
+
|
|
113
|
+
// Check if any parent path exists as an object that needs to be expanded
|
|
114
|
+
this._expandParentObjects(keys);
|
|
115
|
+
|
|
116
|
+
this.setStmt.run(key, JSON.stringify(value));
|
|
117
|
+
return keys;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async del(...keys) {
|
|
121
|
+
if (keys.length === 0) {
|
|
122
|
+
// Delete everything
|
|
123
|
+
this.db.exec('DELETE FROM deepbase');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const key = this._pathToKey(keys);
|
|
128
|
+
|
|
129
|
+
// Delete the key itself
|
|
130
|
+
this.delStmt.run(key);
|
|
131
|
+
|
|
132
|
+
// Delete all children
|
|
133
|
+
const childKey = key + '.';
|
|
134
|
+
this.db.prepare('DELETE FROM deepbase WHERE key LIKE ?').run(childKey + '%');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async inc(...args) {
|
|
138
|
+
const i = args.pop();
|
|
139
|
+
return this.upd(...args, n => n + i);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async dec(...args) {
|
|
143
|
+
const i = args.pop();
|
|
144
|
+
return this.upd(...args, n => n - i);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async add(...keys) {
|
|
148
|
+
const value = keys.pop();
|
|
149
|
+
const id = this.nanoid();
|
|
150
|
+
await this.set(...[...keys, id], value);
|
|
151
|
+
return [...keys, id];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async upd(...args) {
|
|
155
|
+
const func = args.pop();
|
|
156
|
+
return this.set(...args, func(await this.get(...args)));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_pathToKey(path) {
|
|
160
|
+
return path.join('.');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_keyToPath(key) {
|
|
164
|
+
return key.split('.');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_getRootObject() {
|
|
168
|
+
const rows = this.getAllStmt.all();
|
|
169
|
+
const result = {};
|
|
170
|
+
|
|
171
|
+
for (const row of rows) {
|
|
172
|
+
const path = this._keyToPath(row.key);
|
|
173
|
+
const value = JSON.parse(row.value);
|
|
174
|
+
this._setNestedValue(result, path, value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async _setRootObject(obj) {
|
|
181
|
+
// Clear existing data
|
|
182
|
+
this.db.exec('DELETE FROM deepbase');
|
|
183
|
+
|
|
184
|
+
// Flatten and insert
|
|
185
|
+
const entries = this._flattenObject(obj);
|
|
186
|
+
const insertMany = this.db.transaction((entries) => {
|
|
187
|
+
for (const [key, value] of entries) {
|
|
188
|
+
this.setStmt.run(key, JSON.stringify(value));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
insertMany(entries);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_buildObjectFromChildren(parentKey) {
|
|
196
|
+
const prefix = parentKey ? parentKey + '.' : '';
|
|
197
|
+
const rows = this.getKeysLikeStmt.all(prefix + '%');
|
|
198
|
+
const result = {};
|
|
199
|
+
|
|
200
|
+
for (const row of rows) {
|
|
201
|
+
const fullPath = this._keyToPath(row.key);
|
|
202
|
+
const relativePath = parentKey
|
|
203
|
+
? fullPath.slice(this._keyToPath(parentKey).length)
|
|
204
|
+
: fullPath;
|
|
205
|
+
|
|
206
|
+
const value = JSON.parse(row.value);
|
|
207
|
+
this._setNestedValue(result, relativePath, value);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_setNestedValue(obj, path, value) {
|
|
214
|
+
if (path.length === 1) {
|
|
215
|
+
obj[path[0]] = value;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const key = path[0];
|
|
220
|
+
if (!obj.hasOwnProperty(key) || typeof obj[key] !== "object") {
|
|
221
|
+
obj[key] = {};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this._setNestedValue(obj[key], path.slice(1), value);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_flattenObject(obj, prefix = '') {
|
|
228
|
+
const entries = [];
|
|
229
|
+
|
|
230
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
231
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
232
|
+
|
|
233
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
234
|
+
entries.push(...this._flattenObject(value, fullKey));
|
|
235
|
+
} else {
|
|
236
|
+
entries.push([fullKey, value]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return entries;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_findParentWithValue(path) {
|
|
244
|
+
// Try to find a parent path that has a stored value
|
|
245
|
+
// For example: if looking for ['users', 'abc', 'name']
|
|
246
|
+
// and 'users.abc' exists as a stored JSON object, return ['users', 'abc']
|
|
247
|
+
for (let i = path.length - 1; i > 0; i--) {
|
|
248
|
+
const parentPath = path.slice(0, i);
|
|
249
|
+
const parentKey = this._pathToKey(parentPath);
|
|
250
|
+
const row = this.getStmt.get(parentKey);
|
|
251
|
+
if (row) {
|
|
252
|
+
const value = JSON.parse(row.value);
|
|
253
|
+
// Only return if it's an object (not a primitive)
|
|
254
|
+
if (value !== null && typeof value === 'object') {
|
|
255
|
+
return parentPath;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_getFromObject(obj, path) {
|
|
263
|
+
if (path.length === 0) return obj;
|
|
264
|
+
if (path.length === 1) {
|
|
265
|
+
return obj === null || obj[path[0]] === undefined ? null : obj[path[0]];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const key = path[0];
|
|
269
|
+
if (!obj.hasOwnProperty(key)) return null;
|
|
270
|
+
|
|
271
|
+
return this._getFromObject(obj[key], path.slice(1));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_expandParentObjects(path) {
|
|
275
|
+
// Check each parent level (not including the path itself) to see if it exists as an object that needs expanding
|
|
276
|
+
// For example, if path is ['config', 'theme'], check if 'config' exists as an object
|
|
277
|
+
for (let i = 1; i < path.length; i++) {
|
|
278
|
+
const parentPath = path.slice(0, i);
|
|
279
|
+
const parentKey = this._pathToKey(parentPath);
|
|
280
|
+
const parentRow = this.getStmt.get(parentKey);
|
|
281
|
+
|
|
282
|
+
if (parentRow) {
|
|
283
|
+
const parentValue = JSON.parse(parentRow.value);
|
|
284
|
+
|
|
285
|
+
// If it's an object (not array or primitive), expand it
|
|
286
|
+
if (parentValue !== null && typeof parentValue === 'object' && !Array.isArray(parentValue)) {
|
|
287
|
+
// Delete the parent key
|
|
288
|
+
this.delStmt.run(parentKey);
|
|
289
|
+
|
|
290
|
+
// Insert all properties as individual keys
|
|
291
|
+
const entries = this._flattenObject(parentValue, parentKey);
|
|
292
|
+
for (const [key, value] of entries) {
|
|
293
|
+
this.setStmt.run(key, JSON.stringify(value));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export default SqliteDriver;
|
|
302
|
+
|
|
303
|
+
|
package/src/index.js
ADDED
package/test/test.js
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { DeepBase } from '../../core/src/index.js';
|
|
6
|
+
import { SqliteDriver } from '../src/SqliteDriver.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const testDataPath = path.join(__dirname, 'test-data');
|
|
10
|
+
|
|
11
|
+
describe('SqliteDriver', function() {
|
|
12
|
+
let db;
|
|
13
|
+
let testCounter = 0;
|
|
14
|
+
|
|
15
|
+
before(function() {
|
|
16
|
+
// Clean up test data before all tests
|
|
17
|
+
if (fs.existsSync(testDataPath)) {
|
|
18
|
+
fs.rmSync(testDataPath, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
beforeEach(async function() {
|
|
23
|
+
// Use unique name for each test to avoid singleton conflicts
|
|
24
|
+
testCounter++;
|
|
25
|
+
db = new DeepBase(new SqliteDriver({
|
|
26
|
+
name: `test-${testCounter}`,
|
|
27
|
+
path: testDataPath
|
|
28
|
+
}));
|
|
29
|
+
await db.connect();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(async function() {
|
|
33
|
+
await db.disconnect();
|
|
34
|
+
if (fs.existsSync(testDataPath)) {
|
|
35
|
+
fs.rmSync(testDataPath, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('Basic Operations', function() {
|
|
40
|
+
it('should set and get a simple value', async function() {
|
|
41
|
+
await db.set('key', 'value');
|
|
42
|
+
const result = await db.get('key');
|
|
43
|
+
assert.strictEqual(result, 'value');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should set and get nested values', async function() {
|
|
47
|
+
await db.set('user', 'name', 'Alice');
|
|
48
|
+
await db.set('user', 'age', 30);
|
|
49
|
+
|
|
50
|
+
const name = await db.get('user', 'name');
|
|
51
|
+
const age = await db.get('user', 'age');
|
|
52
|
+
|
|
53
|
+
assert.strictEqual(name, 'Alice');
|
|
54
|
+
assert.strictEqual(age, 30);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should get entire nested object', async function() {
|
|
58
|
+
await db.set('user', 'name', 'Bob');
|
|
59
|
+
await db.set('user', 'age', 25);
|
|
60
|
+
|
|
61
|
+
const user = await db.get('user');
|
|
62
|
+
assert.deepStrictEqual(user, { name: 'Bob', age: 25 });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should return null for non-existent keys', async function() {
|
|
66
|
+
const result = await db.get('nonexistent');
|
|
67
|
+
assert.strictEqual(result, null);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should handle numbers', async function() {
|
|
71
|
+
await db.set('number', 42);
|
|
72
|
+
const result = await db.get('number');
|
|
73
|
+
assert.strictEqual(result, 42);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should handle booleans', async function() {
|
|
77
|
+
await db.set('flag', true);
|
|
78
|
+
const result = await db.get('flag');
|
|
79
|
+
assert.strictEqual(result, true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle arrays', async function() {
|
|
83
|
+
await db.set('list', [1, 2, 3]);
|
|
84
|
+
const result = await db.get('list');
|
|
85
|
+
assert.deepStrictEqual(result, [1, 2, 3]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle complex objects', async function() {
|
|
89
|
+
const complexObj = {
|
|
90
|
+
name: 'Test',
|
|
91
|
+
items: [1, 2, 3],
|
|
92
|
+
meta: { created: '2024-01-01', active: true }
|
|
93
|
+
};
|
|
94
|
+
await db.set('complex', complexObj);
|
|
95
|
+
const result = await db.get('complex');
|
|
96
|
+
assert.deepStrictEqual(result, complexObj);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('Delete Operations', function() {
|
|
101
|
+
it('should delete a key', async function() {
|
|
102
|
+
await db.set('temp', 'value');
|
|
103
|
+
await db.del('temp');
|
|
104
|
+
|
|
105
|
+
const result = await db.get('temp');
|
|
106
|
+
assert.strictEqual(result, null);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should delete nested key', async function() {
|
|
110
|
+
await db.set('user', 'name', 'Alice');
|
|
111
|
+
await db.set('user', 'age', 30);
|
|
112
|
+
await db.del('user', 'age');
|
|
113
|
+
|
|
114
|
+
const user = await db.get('user');
|
|
115
|
+
assert.deepStrictEqual(user, { name: 'Alice' });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should delete parent and all children', async function() {
|
|
119
|
+
await db.set('parent', 'child1', 'value1');
|
|
120
|
+
await db.set('parent', 'child2', 'value2');
|
|
121
|
+
await db.set('parent', 'nested', 'deep', 'value3');
|
|
122
|
+
|
|
123
|
+
await db.del('parent');
|
|
124
|
+
|
|
125
|
+
const result = await db.get('parent');
|
|
126
|
+
assert.strictEqual(result, null);
|
|
127
|
+
|
|
128
|
+
const child1 = await db.get('parent', 'child1');
|
|
129
|
+
assert.strictEqual(child1, null);
|
|
130
|
+
|
|
131
|
+
const nested = await db.get('parent', 'nested', 'deep');
|
|
132
|
+
assert.strictEqual(nested, null);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should clear all data', async function() {
|
|
136
|
+
await db.set('key1', 'value1');
|
|
137
|
+
await db.set('key2', 'value2');
|
|
138
|
+
await db.del();
|
|
139
|
+
|
|
140
|
+
const all = await db.get();
|
|
141
|
+
assert.deepStrictEqual(all, {});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('Add Operation', function() {
|
|
146
|
+
it('should add item with auto-generated ID', async function() {
|
|
147
|
+
const path = await db.add('users', { name: 'Charlie' });
|
|
148
|
+
|
|
149
|
+
assert.strictEqual(path.length, 2);
|
|
150
|
+
assert.strictEqual(path[0], 'users');
|
|
151
|
+
assert.strictEqual(typeof path[1], 'string');
|
|
152
|
+
assert.strictEqual(path[1].length, 10);
|
|
153
|
+
|
|
154
|
+
const user = await db.get(...path);
|
|
155
|
+
assert.deepStrictEqual(user, { name: 'Charlie' });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should add multiple items with unique IDs', async function() {
|
|
159
|
+
const path1 = await db.add('items', { value: 1 });
|
|
160
|
+
const path2 = await db.add('items', { value: 2 });
|
|
161
|
+
|
|
162
|
+
assert.notStrictEqual(path1[1], path2[1]);
|
|
163
|
+
|
|
164
|
+
const item1 = await db.get(...path1);
|
|
165
|
+
const item2 = await db.get(...path2);
|
|
166
|
+
|
|
167
|
+
assert.deepStrictEqual(item1, { value: 1 });
|
|
168
|
+
assert.deepStrictEqual(item2, { value: 2 });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should add items at nested paths', async function() {
|
|
172
|
+
const path = await db.add('categories', 'electronics', 'items', { name: 'Laptop' });
|
|
173
|
+
|
|
174
|
+
assert.strictEqual(path.length, 4);
|
|
175
|
+
assert.strictEqual(path[0], 'categories');
|
|
176
|
+
assert.strictEqual(path[1], 'electronics');
|
|
177
|
+
assert.strictEqual(path[2], 'items');
|
|
178
|
+
|
|
179
|
+
const item = await db.get(...path);
|
|
180
|
+
assert.deepStrictEqual(item, { name: 'Laptop' });
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('Increment/Decrement', function() {
|
|
185
|
+
it('should increment a value', async function() {
|
|
186
|
+
await db.set('counter', 10);
|
|
187
|
+
await db.inc('counter', 5);
|
|
188
|
+
|
|
189
|
+
const result = await db.get('counter');
|
|
190
|
+
assert.strictEqual(result, 15);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should decrement a value', async function() {
|
|
194
|
+
await db.set('counter', 20);
|
|
195
|
+
await db.dec('counter', 8);
|
|
196
|
+
|
|
197
|
+
const result = await db.get('counter');
|
|
198
|
+
assert.strictEqual(result, 12);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should increment nested value', async function() {
|
|
202
|
+
await db.set('user', 'balance', 100);
|
|
203
|
+
await db.inc('user', 'balance', 50);
|
|
204
|
+
|
|
205
|
+
const balance = await db.get('user', 'balance');
|
|
206
|
+
assert.strictEqual(balance, 150);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should handle negative increments', async function() {
|
|
210
|
+
await db.set('counter', 10);
|
|
211
|
+
await db.inc('counter', -3);
|
|
212
|
+
|
|
213
|
+
const result = await db.get('counter');
|
|
214
|
+
assert.strictEqual(result, 7);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('Update Operation', function() {
|
|
219
|
+
it('should update value with function', async function() {
|
|
220
|
+
await db.set('name', 'alice');
|
|
221
|
+
await db.upd('name', name => name.toUpperCase());
|
|
222
|
+
|
|
223
|
+
const result = await db.get('name');
|
|
224
|
+
assert.strictEqual(result, 'ALICE');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should update nested value with function', async function() {
|
|
228
|
+
await db.set('user', 'age', 25);
|
|
229
|
+
await db.upd('user', 'age', age => age + 1);
|
|
230
|
+
|
|
231
|
+
const age = await db.get('user', 'age');
|
|
232
|
+
assert.strictEqual(age, 26);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should update object with function', async function() {
|
|
236
|
+
await db.set('user', { name: 'Alice', age: 30 });
|
|
237
|
+
await db.upd('user', user => ({ ...user, age: user.age + 1 }));
|
|
238
|
+
|
|
239
|
+
const user = await db.get('user');
|
|
240
|
+
assert.deepStrictEqual(user, { name: 'Alice', age: 31 });
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('Keys, Values, Entries', function() {
|
|
245
|
+
beforeEach(async function() {
|
|
246
|
+
await db.set('users', 'alice', { age: 30 });
|
|
247
|
+
await db.set('users', 'bob', { age: 25 });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should get keys', async function() {
|
|
251
|
+
const keys = await db.keys('users');
|
|
252
|
+
assert.deepStrictEqual(keys.sort(), ['alice', 'bob']);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should get values', async function() {
|
|
256
|
+
const values = await db.values('users');
|
|
257
|
+
assert.strictEqual(values.length, 2);
|
|
258
|
+
assert.ok(values.some(v => v.age === 30));
|
|
259
|
+
assert.ok(values.some(v => v.age === 25));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should get entries', async function() {
|
|
263
|
+
const entries = await db.entries('users');
|
|
264
|
+
assert.strictEqual(entries.length, 2);
|
|
265
|
+
assert.ok(entries.some(([k, v]) => k === 'alice' && v.age === 30));
|
|
266
|
+
assert.ok(entries.some(([k, v]) => k === 'bob' && v.age === 25));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should return empty arrays for non-object values', async function() {
|
|
270
|
+
await db.set('simple', 'string');
|
|
271
|
+
|
|
272
|
+
const keys = await db.keys('simple');
|
|
273
|
+
const values = await db.values('simple');
|
|
274
|
+
const entries = await db.entries('simple');
|
|
275
|
+
|
|
276
|
+
assert.deepStrictEqual(keys, []);
|
|
277
|
+
assert.deepStrictEqual(values, []);
|
|
278
|
+
assert.deepStrictEqual(entries, []);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('Persistence', function() {
|
|
283
|
+
it('should persist data to database file', async function() {
|
|
284
|
+
// Use a separate db for this test
|
|
285
|
+
const persistDb = new DeepBase(new SqliteDriver({
|
|
286
|
+
name: 'persist-test',
|
|
287
|
+
path: testDataPath
|
|
288
|
+
}));
|
|
289
|
+
await persistDb.connect();
|
|
290
|
+
await persistDb.set('persistent', 'data');
|
|
291
|
+
await persistDb.disconnect();
|
|
292
|
+
|
|
293
|
+
const filePath = path.join(testDataPath, 'persist-test.db');
|
|
294
|
+
assert.ok(fs.existsSync(filePath));
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should load existing data on connect', async function() {
|
|
298
|
+
// Use a separate db for this test
|
|
299
|
+
const db1 = new DeepBase(new SqliteDriver({
|
|
300
|
+
name: 'reload-test',
|
|
301
|
+
path: testDataPath
|
|
302
|
+
}));
|
|
303
|
+
await db1.connect();
|
|
304
|
+
await db1.set('existing', 'value');
|
|
305
|
+
await db1.set('nested', 'key', 'data');
|
|
306
|
+
await db1.disconnect();
|
|
307
|
+
|
|
308
|
+
// Create new instance pointing to same file
|
|
309
|
+
const db2 = new DeepBase(new SqliteDriver({
|
|
310
|
+
name: 'reload-test',
|
|
311
|
+
path: testDataPath
|
|
312
|
+
}));
|
|
313
|
+
await db2.connect();
|
|
314
|
+
|
|
315
|
+
const result = await db2.get('existing');
|
|
316
|
+
assert.strictEqual(result, 'value');
|
|
317
|
+
|
|
318
|
+
const nested = await db2.get('nested', 'key');
|
|
319
|
+
assert.strictEqual(nested, 'data');
|
|
320
|
+
|
|
321
|
+
await db2.disconnect();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should handle reconnection', async function() {
|
|
325
|
+
await db.set('before', 'disconnect');
|
|
326
|
+
await db.disconnect();
|
|
327
|
+
|
|
328
|
+
await db.connect();
|
|
329
|
+
const result = await db.get('before');
|
|
330
|
+
assert.strictEqual(result, 'disconnect');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('Singleton Pattern', function() {
|
|
335
|
+
it('should return same instance for same file', function() {
|
|
336
|
+
const driver1 = new SqliteDriver({ name: 'singleton', path: testDataPath });
|
|
337
|
+
const driver2 = new SqliteDriver({ name: 'singleton', path: testDataPath });
|
|
338
|
+
|
|
339
|
+
assert.strictEqual(driver1, driver2);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should return different instances for different files', function() {
|
|
343
|
+
const driver1 = new SqliteDriver({ name: 'file1', path: testDataPath });
|
|
344
|
+
const driver2 = new SqliteDriver({ name: 'file2', path: testDataPath });
|
|
345
|
+
|
|
346
|
+
assert.notStrictEqual(driver1, driver2);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe('Root Object Operations', function() {
|
|
351
|
+
it('should set entire root object', async function() {
|
|
352
|
+
const data = {
|
|
353
|
+
users: { alice: { age: 30 }, bob: { age: 25 } },
|
|
354
|
+
config: { theme: 'dark', lang: 'en' }
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
await db.set(data);
|
|
358
|
+
const result = await db.get();
|
|
359
|
+
assert.deepStrictEqual(result, data);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should get entire root object', async function() {
|
|
363
|
+
await db.set('key1', 'value1');
|
|
364
|
+
await db.set('key2', 'value2');
|
|
365
|
+
await db.set('nested', 'key', 'value');
|
|
366
|
+
|
|
367
|
+
const result = await db.get();
|
|
368
|
+
assert.deepStrictEqual(result, {
|
|
369
|
+
key1: 'value1',
|
|
370
|
+
key2: 'value2',
|
|
371
|
+
nested: { key: 'value' }
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should replace root object on set', async function() {
|
|
376
|
+
await db.set('old', 'data');
|
|
377
|
+
|
|
378
|
+
const newData = { new: 'data' };
|
|
379
|
+
await db.set(newData);
|
|
380
|
+
|
|
381
|
+
const result = await db.get();
|
|
382
|
+
assert.deepStrictEqual(result, newData);
|
|
383
|
+
|
|
384
|
+
const old = await db.get('old');
|
|
385
|
+
assert.strictEqual(old, null);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe('Deep Nesting', function() {
|
|
390
|
+
it('should handle deeply nested paths', async function() {
|
|
391
|
+
await db.set('a', 'b', 'c', 'd', 'e', 'deep value');
|
|
392
|
+
const result = await db.get('a', 'b', 'c', 'd', 'e');
|
|
393
|
+
assert.strictEqual(result, 'deep value');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should get partial deep objects', async function() {
|
|
397
|
+
await db.set('a', 'b', 'c', 'value1');
|
|
398
|
+
await db.set('a', 'b', 'd', 'value2');
|
|
399
|
+
await db.set('a', 'e', 'value3');
|
|
400
|
+
|
|
401
|
+
const resultB = await db.get('a', 'b');
|
|
402
|
+
assert.deepStrictEqual(resultB, { c: 'value1', d: 'value2' });
|
|
403
|
+
|
|
404
|
+
const resultA = await db.get('a');
|
|
405
|
+
assert.deepStrictEqual(resultA, {
|
|
406
|
+
b: { c: 'value1', d: 'value2' },
|
|
407
|
+
e: 'value3'
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('Overwriting Object with Nested Properties', function() {
|
|
413
|
+
it('should handle setting object first then nested properties', async function() {
|
|
414
|
+
// This is the scenario from example 05-sqlite.js
|
|
415
|
+
await db.set('config', { lang: 'en', theme: 'dark' });
|
|
416
|
+
await db.set('config', 'lang', 'en');
|
|
417
|
+
await db.set('config', 'theme', 'light');
|
|
418
|
+
|
|
419
|
+
const config = await db.get('config');
|
|
420
|
+
assert.deepStrictEqual(config, { lang: 'en', theme: 'light' });
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should prioritize nested properties over initial object', async function() {
|
|
424
|
+
await db.set('settings', { a: 1, b: 2, c: 3 });
|
|
425
|
+
await db.set('settings', 'b', 99);
|
|
426
|
+
await db.set('settings', 'd', 4);
|
|
427
|
+
|
|
428
|
+
const settings = await db.get('settings');
|
|
429
|
+
assert.deepStrictEqual(settings, { a: 1, b: 99, c: 3, d: 4 });
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should handle nested object then deeper nesting', async function() {
|
|
433
|
+
await db.set('user', { name: 'Alice', meta: { role: 'admin' } });
|
|
434
|
+
await db.set('user', 'meta', 'role', 'user');
|
|
435
|
+
await db.set('user', 'meta', 'active', true);
|
|
436
|
+
|
|
437
|
+
const user = await db.get('user');
|
|
438
|
+
assert.deepStrictEqual(user, {
|
|
439
|
+
name: 'Alice',
|
|
440
|
+
meta: { role: 'user', active: true }
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
|