deepbase-json 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 ADDED
@@ -0,0 +1,21 @@
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.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # deepbase-json
2
+
3
+ JSON filesystem driver for DeepBase.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install deepbase
9
+ # deepbase-json is included automatically as a dependency
10
+ ```
11
+
12
+ ## Description
13
+
14
+ Stores data in JSON files on the filesystem. Perfect for:
15
+
16
+ - ✅ Development and testing
17
+ - ✅ Small to medium datasets
18
+ - ✅ Human-readable data
19
+ - ✅ No external dependencies needed
20
+ - ✅ Version control friendly
21
+
22
+ ## Usage
23
+
24
+ ```javascript
25
+ import DeepBase, { JsonDriver } from 'deepbase';
26
+
27
+ const db = new DeepBase(new JsonDriver({
28
+ path: './data',
29
+ name: 'mydb'
30
+ }));
31
+
32
+ await db.connect();
33
+
34
+ await db.set('users', 'alice', { name: 'Alice', age: 30 });
35
+ const alice = await db.get('users', 'alice');
36
+ ```
37
+
38
+ ## Options
39
+
40
+ ```javascript
41
+ new JsonDriver({
42
+ path: './data', // Directory to store JSON files
43
+ name: 'default', // Filename (without .json)
44
+ nidAlphabet: 'ABC...', // Alphabet for ID generation
45
+ nidLength: 10, // Length of generated IDs
46
+ stringify: JSON.stringify, // Custom JSON serializer
47
+ parse: JSON.parse // Custom JSON parser
48
+ })
49
+ ```
50
+
51
+ ## Features
52
+
53
+ ### Singleton Pattern
54
+
55
+ Multiple instances pointing to the same file will share the same data:
56
+
57
+ ```javascript
58
+ const db1 = new DeepBase(new JsonDriver({ name: 'mydb' }));
59
+ const db2 = new DeepBase(new JsonDriver({ name: 'mydb' }));
60
+ // Both use the same underlying data
61
+ ```
62
+
63
+ ### Custom Serialization
64
+
65
+ Support for circular references and custom serialization:
66
+
67
+ ```javascript
68
+ import CircularJSON from 'circular-json';
69
+
70
+ const db = new DeepBase(new JsonDriver({
71
+ stringify: (obj) => CircularJSON.stringify(obj, null, 4),
72
+ parse: CircularJSON.parse
73
+ }));
74
+ ```
75
+
76
+ ### Atomic Writes
77
+
78
+ Uses `steno` for atomic file writes, preventing data corruption.
79
+
80
+ ## File Structure
81
+
82
+ Data is stored as JSON files:
83
+
84
+ ```
85
+ data/
86
+ mydb.json
87
+ users.json
88
+ config.json
89
+ ```
90
+
91
+ ## Example Data
92
+
93
+ ```json
94
+ {
95
+ "users": {
96
+ "alice": {
97
+ "name": "Alice",
98
+ "age": 30
99
+ },
100
+ "bob": {
101
+ "name": "Bob",
102
+ "age": 25
103
+ }
104
+ },
105
+ "config": {
106
+ "theme": "dark",
107
+ "lang": "en"
108
+ }
109
+ }
110
+ ```
111
+
112
+ ## Use Cases
113
+
114
+ - **Development**: Quick prototyping without database setup
115
+ - **Testing**: Easy to inspect and modify test data
116
+ - **Small Apps**: Perfect for configuration and small datasets
117
+ - **Backup**: Use as secondary driver for data backup
118
+ - **Version Control**: Human-readable, git-friendly format
119
+
120
+ ## Migration
121
+
122
+ Easy to migrate from JSON to other drivers:
123
+
124
+ ```javascript
125
+ import DeepBase, { JsonDriver } from 'deepbase';
126
+ import MongoDriver from 'deepbase-mongodb';
127
+
128
+ const db = new DeepBase([
129
+ new JsonDriver({ path: './data' }),
130
+ new MongoDriver({ url: 'mongodb://localhost:27017' })
131
+ ]);
132
+
133
+ await db.connect();
134
+ await db.migrate(0, 1); // Migrate JSON to MongoDB
135
+ ```
136
+
137
+ ## License
138
+
139
+ MIT - Copyright (c) Martin Clasen
140
+
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "deepbase-json",
3
+ "version": "3.0.0",
4
+ "description": "⚡ DeepBase JSON - filesystem 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
+ "steno": "^0.4.4"
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
+ "json",
32
+ "filesystem",
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
+
@@ -0,0 +1,141 @@
1
+ import { DeepBaseDriver } from 'deepbase';
2
+ import steno from 'steno';
3
+ import fs from 'fs';
4
+ import * as pathModule from 'path';
5
+
6
+ export class JsonDriver extends DeepBaseDriver {
7
+ static _instances = {};
8
+
9
+ constructor({name, path, stringify, parse, ...opts} = {}) {
10
+ super(opts);
11
+
12
+ this.name = name || "default";
13
+ this.path = path || './db';
14
+ this.stringify = stringify || ((obj) => JSON.stringify(obj, null, 4));
15
+ this.parse = parse || JSON.parse;
16
+
17
+ this.path = pathModule.resolve(this.path);
18
+ this.fileName = pathModule.join(this.path, `${this.name}.json`);
19
+
20
+ // Singleton pattern per file
21
+ if (JsonDriver._instances[this.fileName]) {
22
+ return JsonDriver._instances[this.fileName];
23
+ }
24
+
25
+ this.obj = {};
26
+ JsonDriver._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
+ if (fs.existsSync(this.fileName)) {
35
+ const fileContent = fs.readFileSync(this.fileName, "utf8");
36
+ this.obj = fileContent ? this.parse(fileContent) : {};
37
+ }
38
+ }
39
+
40
+ async disconnect() {
41
+ await this._saveToFile();
42
+ }
43
+
44
+ async get(...args) {
45
+ const value = this._getRecursive(this.obj, args.slice());
46
+ return typeof value === 'object' && value !== null
47
+ ? this.parse(this.stringify(value))
48
+ : value;
49
+ }
50
+
51
+ async set(...args) {
52
+ if (args.length < 2) {
53
+ this.obj = args[0];
54
+ await this._saveToFile();
55
+ return [];
56
+ }
57
+
58
+ const keys = args.slice(0, -1);
59
+ const value = args[args.length - 1];
60
+
61
+ this._setRecursive(this.obj, keys, value);
62
+ await this._saveToFile();
63
+ return keys;
64
+ }
65
+
66
+ async del(...keys) {
67
+ if (keys.length === 0) {
68
+ this.obj = {};
69
+ return this._saveToFile();
70
+ }
71
+
72
+ const key = keys.pop();
73
+ const parentObj = this._getRecursive(this.obj, keys.slice());
74
+
75
+ if (parentObj && parentObj.hasOwnProperty(key)) {
76
+ delete parentObj[key];
77
+ return this._saveToFile();
78
+ }
79
+ }
80
+
81
+ async inc(...args) {
82
+ const i = args.pop();
83
+ return this.upd(...args, n => n + i);
84
+ }
85
+
86
+ async dec(...args) {
87
+ const i = args.pop();
88
+ return this.upd(...args, n => n - i);
89
+ }
90
+
91
+ async add(...keys) {
92
+ const value = keys.pop();
93
+ const id = this.nanoid();
94
+ await this.set(...[...keys, id], value);
95
+ return [...keys, id];
96
+ }
97
+
98
+ async upd(...args) {
99
+ const func = args.pop();
100
+ return this.set(...args, func(await this.get(...args)));
101
+ }
102
+
103
+ _setRecursive(obj, keys, value) {
104
+ if (keys.length === 1) {
105
+ obj[keys[0]] = value;
106
+ return;
107
+ }
108
+
109
+ const key = keys.shift();
110
+ if (!obj.hasOwnProperty(key) || typeof obj[key] !== "object") {
111
+ obj[key] = {};
112
+ }
113
+
114
+ this._setRecursive(obj[key], keys, value);
115
+ }
116
+
117
+ _getRecursive(obj, keys) {
118
+ if (keys.length === 0) return obj;
119
+ if (keys.length === 1) {
120
+ return obj === null || obj[keys[0]] === undefined ? null : obj[keys[0]];
121
+ }
122
+
123
+ const key = keys.shift();
124
+ if (!obj.hasOwnProperty(key)) return null;
125
+
126
+ return this._getRecursive(obj[key], keys);
127
+ }
128
+
129
+ async _saveToFile() {
130
+ return new Promise((resolve, reject) => {
131
+ const serializedData = this.stringify(this.obj);
132
+ steno.writeFile(this.fileName, serializedData, err => {
133
+ if (err) reject(err);
134
+ else resolve();
135
+ });
136
+ });
137
+ }
138
+ }
139
+
140
+ export default JsonDriver;
141
+
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import { JsonDriver } from './JsonDriver.js';
2
+ export { JsonDriver } from './JsonDriver.js';
3
+ export default JsonDriver;
4
+
package/test/test.js ADDED
@@ -0,0 +1,248 @@
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 { JsonDriver } from '../src/JsonDriver.js';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const testDataPath = path.join(__dirname, 'test-data');
10
+
11
+ describe('JsonDriver', 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 JsonDriver({
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
+
71
+ describe('Delete Operations', function() {
72
+ it('should delete a key', async function() {
73
+ await db.set('temp', 'value');
74
+ await db.del('temp');
75
+
76
+ const result = await db.get('temp');
77
+ assert.strictEqual(result, null);
78
+ });
79
+
80
+ it('should delete nested key', async function() {
81
+ await db.set('user', 'name', 'Alice');
82
+ await db.set('user', 'age', 30);
83
+ await db.del('user', 'age');
84
+
85
+ const user = await db.get('user');
86
+ assert.deepStrictEqual(user, { name: 'Alice' });
87
+ });
88
+
89
+ it('should clear all data', async function() {
90
+ await db.set('key1', 'value1');
91
+ await db.set('key2', 'value2');
92
+ await db.del();
93
+
94
+ const all = await db.get();
95
+ assert.deepStrictEqual(all, {});
96
+ });
97
+ });
98
+
99
+ describe('Add Operation', function() {
100
+ it('should add item with auto-generated ID', async function() {
101
+ const path = await db.add('users', { name: 'Charlie' });
102
+
103
+ assert.strictEqual(path.length, 2);
104
+ assert.strictEqual(path[0], 'users');
105
+ assert.strictEqual(typeof path[1], 'string');
106
+ assert.strictEqual(path[1].length, 10);
107
+
108
+ const user = await db.get(...path);
109
+ assert.deepStrictEqual(user, { name: 'Charlie' });
110
+ });
111
+
112
+ it('should add multiple items with unique IDs', async function() {
113
+ const path1 = await db.add('items', { value: 1 });
114
+ const path2 = await db.add('items', { value: 2 });
115
+
116
+ assert.notStrictEqual(path1[1], path2[1]);
117
+ });
118
+ });
119
+
120
+ describe('Increment/Decrement', function() {
121
+ it('should increment a value', async function() {
122
+ await db.set('counter', 10);
123
+ await db.inc('counter', 5);
124
+
125
+ const result = await db.get('counter');
126
+ assert.strictEqual(result, 15);
127
+ });
128
+
129
+ it('should decrement a value', async function() {
130
+ await db.set('counter', 20);
131
+ await db.dec('counter', 8);
132
+
133
+ const result = await db.get('counter');
134
+ assert.strictEqual(result, 12);
135
+ });
136
+
137
+ it('should increment nested value', async function() {
138
+ await db.set('user', 'balance', 100);
139
+ await db.inc('user', 'balance', 50);
140
+
141
+ const balance = await db.get('user', 'balance');
142
+ assert.strictEqual(balance, 150);
143
+ });
144
+ });
145
+
146
+ describe('Update Operation', function() {
147
+ it('should update value with function', async function() {
148
+ await db.set('name', 'alice');
149
+ await db.upd('name', name => name.toUpperCase());
150
+
151
+ const result = await db.get('name');
152
+ assert.strictEqual(result, 'ALICE');
153
+ });
154
+
155
+ it('should update nested value with function', async function() {
156
+ await db.set('user', 'age', 25);
157
+ await db.upd('user', 'age', age => age + 1);
158
+
159
+ const age = await db.get('user', 'age');
160
+ assert.strictEqual(age, 26);
161
+ });
162
+ });
163
+
164
+ describe('Keys, Values, Entries', function() {
165
+ beforeEach(async function() {
166
+ await db.set('users', 'alice', { age: 30 });
167
+ await db.set('users', 'bob', { age: 25 });
168
+ });
169
+
170
+ it('should get keys', async function() {
171
+ const keys = await db.keys('users');
172
+ assert.deepStrictEqual(keys.sort(), ['alice', 'bob']);
173
+ });
174
+
175
+ it('should get values', async function() {
176
+ const values = await db.values('users');
177
+ assert.strictEqual(values.length, 2);
178
+ assert.ok(values.some(v => v.age === 30));
179
+ assert.ok(values.some(v => v.age === 25));
180
+ });
181
+
182
+ it('should get entries', async function() {
183
+ const entries = await db.entries('users');
184
+ assert.strictEqual(entries.length, 2);
185
+ assert.ok(entries.some(([k, v]) => k === 'alice' && v.age === 30));
186
+ assert.ok(entries.some(([k, v]) => k === 'bob' && v.age === 25));
187
+ });
188
+ });
189
+
190
+ describe('Persistence', function() {
191
+ it('should persist data to file', async function() {
192
+ // Use a separate db for this test
193
+ const persistDb = new DeepBase(new JsonDriver({
194
+ name: 'persist-test',
195
+ path: testDataPath
196
+ }));
197
+ await persistDb.connect();
198
+ await persistDb.set('persistent', 'data');
199
+ await persistDb.disconnect();
200
+
201
+ const filePath = path.join(testDataPath, 'persist-test.json');
202
+ assert.ok(fs.existsSync(filePath));
203
+
204
+ const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
205
+ assert.deepStrictEqual(content, { persistent: 'data' });
206
+ });
207
+
208
+ it('should load existing data on connect', async function() {
209
+ // Use a separate db for this test
210
+ const db1 = new DeepBase(new JsonDriver({
211
+ name: 'reload-test',
212
+ path: testDataPath
213
+ }));
214
+ await db1.connect();
215
+ await db1.set('existing', 'value');
216
+ await db1.disconnect();
217
+
218
+ // Create new instance pointing to same file
219
+ const db2 = new DeepBase(new JsonDriver({
220
+ name: 'reload-test',
221
+ path: testDataPath
222
+ }));
223
+ await db2.connect();
224
+
225
+ const result = await db2.get('existing');
226
+ assert.strictEqual(result, 'value');
227
+
228
+ await db2.disconnect();
229
+ });
230
+ });
231
+
232
+ describe('Singleton Pattern', function() {
233
+ it('should return same instance for same file', function() {
234
+ const driver1 = new JsonDriver({ name: 'singleton', path: testDataPath });
235
+ const driver2 = new JsonDriver({ name: 'singleton', path: testDataPath });
236
+
237
+ assert.strictEqual(driver1, driver2);
238
+ });
239
+
240
+ it('should return different instances for different files', function() {
241
+ const driver1 = new JsonDriver({ name: 'file1', path: testDataPath });
242
+ const driver2 = new JsonDriver({ name: 'file2', path: testDataPath });
243
+
244
+ assert.notStrictEqual(driver1, driver2);
245
+ });
246
+ });
247
+ });
248
+