pure.db 1.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/README.md ADDED
@@ -0,0 +1,147 @@
1
+
2
+ # pure.db
3
+
4
+ A simple, robust, and synchronous JSON-based key-value database for Node.js.
5
+ Perfect for Discord bots and small projects where you need persistent storage without the hassle of setting up a database server.
6
+
7
+ ## Features
8
+
9
+ - 🚀 **Easy to use**: Simple `set`, `get`, `push`, `pull` API.
10
+ - âš¡ **Fast**: Synchronous operations using native filesystem.
11
+ - 📂 **No Dependencies**: Uses Node.js `fs` module, so no native compilation errors.
12
+ - 💾 **JSON Storage**: Data is stored in a human-readable `database.json` file.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install pure.db
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```javascript
23
+ const { PureDB } = require('pure.db');
24
+
25
+ // Initialize the database (creates 'database.json' by default)
26
+ const db = new PureDB();
27
+
28
+ // Or specify a file path
29
+ // const db = new PureDB('my-db.json');
30
+
31
+ // --- Basic Operations ---
32
+
33
+ // Set a value
34
+ db.set('user.name', 'Alice');
35
+ db.set('user.balance', 100);
36
+
37
+ // Get a value
38
+ console.log(db.get('user.name')); // Output: "Alice"
39
+ console.log(db.get('user.balance')); // Output: 100
40
+
41
+ // Check availability
42
+ if (db.has('user.balance')) {
43
+ console.log('User has a balance!');
44
+ }
45
+
46
+ // Delete a value
47
+ db.delete('user.name');
48
+
49
+ // --- Math Operations ---
50
+
51
+ // Add to a number
52
+ db.add('user.balance', 50); // Balance is now 150
53
+
54
+ // Subtract from a number
55
+ db.subtract('user.balance', 25); // Balance is now 125
56
+
57
+ // --- Array Operations ---
58
+
59
+ // Push to an array
60
+ db.push('guild.roles', 'Admin');
61
+ db.push('guild.roles', 'Moderator');
62
+
63
+ // Pull (remove) from an array
64
+ db.pull('guild.roles', 'Moderator'); // Removes 'Moderator'
65
+
66
+ // --- varied ---
67
+
68
+ // Get all data
69
+ console.log(db.all());
70
+
71
+ // Clear the entire database
72
+ db.clear();
73
+ ```
74
+
75
+
76
+ ## Professional Features 🚀
77
+
78
+ `pure.db` is now equipped with high-performance features suitable for production environments.
79
+
80
+ ### âš¡ In-Memory Caching
81
+ All data is cached in memory for **instant** read speeds (`O(1)`). Writes are synchronous and atomic to ensure data safety.
82
+
83
+ ### 🔒 Encryption
84
+ Secure your data at rest with AES-256-CBC encryption.
85
+
86
+ ```javascript
87
+ const db = new PureDB({
88
+ filePath: 'secure.db',
89
+ encryptionKey: '12345678901234567890123456789012' // Must be 32 chars
90
+ });
91
+ ```
92
+ *Note: If you lose the key, data is unrecoverable.*
93
+
94
+ ### 📡 Event System
95
+ Listen to database changes in real-time.
96
+
97
+ ```javascript
98
+ db.on('set', (key, value) => {
99
+ console.log(`Key "${key}" was set to:`, value);
100
+ });
101
+
102
+ db.on('delete', (key) => {
103
+ console.log(`Key "${key}" was deleted.`);
104
+ });
105
+ ```
106
+
107
+ ### 🧮 Advanced Math
108
+ Perform complex calculations directly on keys.
109
+
110
+ ```javascript
111
+ // Supported: '+', '-', '*', '/', '%'
112
+ db.math('user.xp', '*', 2); // Double XP!
113
+ db.math('user.health', '/', 2); // Halve health
114
+ ```
115
+
116
+ ### 📦 Backups
117
+ Create instant backups of your database.
118
+
119
+ ```javascript
120
+ db.backup('backup_date.json');
121
+ ```
122
+
123
+ ## API
124
+
125
+ ### `new PureDB(options?)`
126
+ - `options` (object | string): Configuration object or file path string.
127
+ - `filePath` (string): Path to JSON file.
128
+ - `encryptionKey` (string): 32-char key for encryption.
129
+ - `pretty` (boolean): Indent JSON output (default: true).
130
+ - `debug` (boolean): meaningful error logs.
131
+
132
+ ### Methods
133
+ - `set(key, value)`
134
+ - `get(key, defaultValue?)`
135
+ - `has(key)`
136
+ - `delete(key)`
137
+ - `math(key, operator, number)`
138
+ - `push(key, ...elements)`
139
+ - `pull(key, elementOrFilter)`
140
+ - `all()`
141
+ - `clear()`
142
+ - `backup(path)`
143
+
144
+
145
+ ## License
146
+
147
+ MIT
@@ -0,0 +1,96 @@
1
+ import { EventEmitter } from 'events';
2
+ interface PureDBOptions {
3
+ /**
4
+ * File path for the database. generic 'database.json' by default.
5
+ */
6
+ filePath?: string;
7
+ /**
8
+ * Secret key for encryption. If provided, the database will be encrypted.
9
+ * Must be 32 characters long.
10
+ */
11
+ encryptionKey?: string;
12
+ /**
13
+ * Format the JSON file with indentation for readability. Default: true.
14
+ * Set to false for smaller file size.
15
+ */
16
+ pretty?: boolean;
17
+ /**
18
+ * Enable debug logging.
19
+ */
20
+ debug?: boolean;
21
+ }
22
+ export declare class PureDB extends EventEmitter {
23
+ filePath: string;
24
+ options: PureDBOptions;
25
+ private _cache;
26
+ private _algorithm;
27
+ private _ivLength;
28
+ constructor(options?: PureDBOptions | string);
29
+ /**
30
+ * Loads the database from disk into memory.
31
+ */
32
+ private _load;
33
+ /**
34
+ * Saves the current memory cache to disk.
35
+ */
36
+ private _save;
37
+ private _encrypt;
38
+ private _decrypt;
39
+ private _setDeep;
40
+ private _getDeep;
41
+ private _deleteDeep;
42
+ /**
43
+ * Set a value in the database.
44
+ * @param key The key to set (supports dot notation)
45
+ * @param value The value to store
46
+ */
47
+ set<T = any>(key: string, value: T): T;
48
+ /**
49
+ * Get a value from the database.
50
+ * @param key The key to look for (supports dot notation)
51
+ * @param defaultValue Optional default value if key not found
52
+ */
53
+ get<T = any>(key: string, defaultValue?: T): T | undefined;
54
+ /**
55
+ * Check if a key exists in the database.
56
+ */
57
+ has(key: string): boolean;
58
+ /**
59
+ * Delete a key from the database.
60
+ */
61
+ delete(key: string): boolean;
62
+ /**
63
+ * Add a number to a key.
64
+ */
65
+ add(key: string, count: number): number;
66
+ /**
67
+ * Subtract a number from a key.
68
+ */
69
+ subtract(key: string, count: number): number;
70
+ /**
71
+ * Perform mathematical operations on a key.
72
+ */
73
+ math(key: string, operator: '+' | '-' | '*' | '/' | '%', operand: number): number;
74
+ /**
75
+ * Push an element to an array.
76
+ */
77
+ push<T = any>(key: string, ...elements: T[]): T[];
78
+ /**
79
+ * Remove (pull) elements from an array which match the given filter or value.
80
+ */
81
+ pull<T = any>(key: string, elementOrFilter: T | ((item: T) => boolean)): T[];
82
+ /**
83
+ * Returns the entire database object.
84
+ */
85
+ all(): any;
86
+ /**
87
+ * Clears the entire database.
88
+ */
89
+ clear(): void;
90
+ /**
91
+ * Creates a backup of the current database file.
92
+ * @param destPath Path to save the backup to.
93
+ */
94
+ backup(destPath: string): boolean;
95
+ }
96
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PureDB = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const events_1 = require("events");
10
+ const crypto_1 = __importDefault(require("crypto"));
11
+ class PureDB extends events_1.EventEmitter {
12
+ constructor(options = {}) {
13
+ super();
14
+ this._cache = {};
15
+ this._algorithm = 'aes-256-cbc';
16
+ this._ivLength = 16;
17
+ if (typeof options === 'string') {
18
+ options = { filePath: options };
19
+ }
20
+ this.options = {
21
+ filePath: 'database.json',
22
+ pretty: true,
23
+ ...options
24
+ };
25
+ this.filePath = path_1.default.resolve(this.options.filePath);
26
+ // Validate Encryption Key
27
+ if (this.options.encryptionKey) {
28
+ if (this.options.encryptionKey.length !== 32) {
29
+ throw new Error('Encryption key must be exactly 32 characters long.');
30
+ }
31
+ }
32
+ this._load();
33
+ }
34
+ /**
35
+ * Loads the database from disk into memory.
36
+ */
37
+ _load() {
38
+ try {
39
+ if (!fs_1.default.existsSync(this.filePath)) {
40
+ this._cache = {};
41
+ this._save(); // Create file
42
+ return;
43
+ }
44
+ const fileContent = fs_1.default.readFileSync(this.filePath, 'utf-8');
45
+ if (!fileContent.trim()) {
46
+ this._cache = {};
47
+ return;
48
+ }
49
+ if (this.options.encryptionKey) {
50
+ try {
51
+ this._cache = JSON.parse(this._decrypt(fileContent));
52
+ }
53
+ catch (e) {
54
+ // Fallback check: maybe it wasn't encrypted before?
55
+ // Or wrong key.
56
+ if (this.options.debug)
57
+ console.error("Decryption failed. attempting plain text read.", e);
58
+ try {
59
+ this._cache = JSON.parse(fileContent);
60
+ }
61
+ catch (e2) {
62
+ this._cache = {}; // Corrupt file or wrong key
63
+ }
64
+ }
65
+ }
66
+ else {
67
+ this._cache = JSON.parse(fileContent);
68
+ }
69
+ }
70
+ catch (err) {
71
+ if (this.options.debug)
72
+ console.error("Load Error:", err);
73
+ this._cache = {};
74
+ }
75
+ this.emit('ready', this);
76
+ }
77
+ /**
78
+ * Saves the current memory cache to disk.
79
+ */
80
+ _save() {
81
+ try {
82
+ let dataToWrite;
83
+ if (this.options.encryptionKey) {
84
+ dataToWrite = this._encrypt(JSON.stringify(this._cache));
85
+ }
86
+ else {
87
+ dataToWrite = JSON.stringify(this._cache, null, this.options.pretty ? 2 : undefined);
88
+ }
89
+ // Write to temp file then rename (atomic write prevention of corruption)
90
+ const tempPath = `${this.filePath}.tmp`;
91
+ fs_1.default.writeFileSync(tempPath, dataToWrite);
92
+ fs_1.default.renameSync(tempPath, this.filePath);
93
+ }
94
+ catch (err) {
95
+ if (this.options.debug)
96
+ console.error("Write Error:", err);
97
+ this.emit('error', err);
98
+ }
99
+ }
100
+ // --- Encryption Helpers ---
101
+ _encrypt(text) {
102
+ if (!this.options.encryptionKey)
103
+ return text;
104
+ const iv = crypto_1.default.randomBytes(this._ivLength);
105
+ const cipher = crypto_1.default.createCipheriv(this._algorithm, Buffer.from(this.options.encryptionKey), iv);
106
+ let encrypted = cipher.update(text);
107
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
108
+ return iv.toString('hex') + ':' + encrypted.toString('hex');
109
+ }
110
+ _decrypt(text) {
111
+ if (!this.options.encryptionKey)
112
+ return text;
113
+ const textParts = text.split(':');
114
+ const iv = Buffer.from(textParts.shift(), 'hex');
115
+ const encryptedText = Buffer.from(textParts.join(':'), 'hex');
116
+ const decipher = crypto_1.default.createDecipheriv(this._algorithm, Buffer.from(this.options.encryptionKey), iv);
117
+ let decrypted = decipher.update(encryptedText);
118
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
119
+ return decrypted.toString();
120
+ }
121
+ // --- Deep Object Helpers ---
122
+ _setDeep(path, value) {
123
+ const keys = path.split('.');
124
+ let current = this._cache;
125
+ for (let i = 0; i < keys.length - 1; i++) {
126
+ const key = keys[i];
127
+ if (!(key in current) || typeof current[key] !== 'object') {
128
+ current[key] = {};
129
+ }
130
+ current = current[key];
131
+ }
132
+ current[keys[keys.length - 1]] = value;
133
+ }
134
+ _getDeep(path) {
135
+ const keys = path.split('.');
136
+ let current = this._cache;
137
+ for (const key of keys) {
138
+ if (current === undefined || current === null || typeof current !== 'object')
139
+ return undefined;
140
+ current = current[key];
141
+ }
142
+ return current;
143
+ }
144
+ _deleteDeep(path) {
145
+ const keys = path.split('.');
146
+ let current = this._cache;
147
+ for (let i = 0; i < keys.length - 1; i++) {
148
+ const key = keys[i];
149
+ if (current === undefined || current === null || typeof current !== 'object')
150
+ return false;
151
+ current = current[key];
152
+ }
153
+ if (current && typeof current === 'object') {
154
+ const lastKey = keys[keys.length - 1];
155
+ if (lastKey in current) {
156
+ delete current[lastKey];
157
+ return true;
158
+ }
159
+ }
160
+ return false;
161
+ }
162
+ // --- Public API ---
163
+ /**
164
+ * Set a value in the database.
165
+ * @param key The key to set (supports dot notation)
166
+ * @param value The value to store
167
+ */
168
+ set(key, value) {
169
+ this._setDeep(key, value);
170
+ this._save();
171
+ this.emit('set', key, value);
172
+ return value;
173
+ }
174
+ /**
175
+ * Get a value from the database.
176
+ * @param key The key to look for (supports dot notation)
177
+ * @param defaultValue Optional default value if key not found
178
+ */
179
+ get(key, defaultValue) {
180
+ const val = this._getDeep(key);
181
+ return val === undefined ? defaultValue : val;
182
+ }
183
+ /**
184
+ * Check if a key exists in the database.
185
+ */
186
+ has(key) {
187
+ return this.get(key) !== undefined;
188
+ }
189
+ /**
190
+ * Delete a key from the database.
191
+ */
192
+ delete(key) {
193
+ const deleted = this._deleteDeep(key);
194
+ if (deleted) {
195
+ this._save();
196
+ this.emit('delete', key);
197
+ }
198
+ return deleted;
199
+ }
200
+ /**
201
+ * Add a number to a key.
202
+ */
203
+ add(key, count) {
204
+ const current = this.get(key, 0);
205
+ if (typeof current !== 'number')
206
+ throw new Error(`Value at key "${key}" is not a number.`);
207
+ const newVal = current + count;
208
+ this.set(key, newVal);
209
+ return newVal;
210
+ }
211
+ /**
212
+ * Subtract a number from a key.
213
+ */
214
+ subtract(key, count) {
215
+ return this.add(key, -count);
216
+ }
217
+ /**
218
+ * Perform mathematical operations on a key.
219
+ */
220
+ math(key, operator, operand) {
221
+ const current = this.get(key, 0);
222
+ if (typeof current !== 'number')
223
+ throw new Error(`Value at key "${key}" is not a number.`);
224
+ let newVal;
225
+ switch (operator) {
226
+ case '+':
227
+ newVal = current + operand;
228
+ break;
229
+ case '-':
230
+ newVal = current - operand;
231
+ break;
232
+ case '*':
233
+ newVal = current * operand;
234
+ break;
235
+ case '/':
236
+ newVal = current / operand;
237
+ break;
238
+ case '%':
239
+ newVal = current % operand;
240
+ break;
241
+ default: throw new Error(`Invalid operator: ${operator}`);
242
+ }
243
+ this.set(key, newVal);
244
+ return newVal;
245
+ }
246
+ /**
247
+ * Push an element to an array.
248
+ */
249
+ push(key, ...elements) {
250
+ const current = this.get(key, []);
251
+ if (!Array.isArray(current))
252
+ throw new Error(`Value at key "${key}" is not an array.`);
253
+ current.push(...elements);
254
+ this.set(key, current);
255
+ return current;
256
+ }
257
+ /**
258
+ * Remove (pull) elements from an array which match the given filter or value.
259
+ */
260
+ pull(key, elementOrFilter) {
261
+ const current = this.get(key, []);
262
+ if (!Array.isArray(current))
263
+ throw new Error(`Value at key "${key}" is not an array.`);
264
+ let newArr;
265
+ if (typeof elementOrFilter === 'function') {
266
+ // @ts-ignore
267
+ newArr = current.filter(item => !elementOrFilter(item));
268
+ }
269
+ else {
270
+ newArr = current.filter(item => JSON.stringify(item) !== JSON.stringify(elementOrFilter));
271
+ }
272
+ this.set(key, newArr);
273
+ return newArr;
274
+ }
275
+ /**
276
+ * Returns the entire database object.
277
+ */
278
+ all() {
279
+ return this._cache;
280
+ }
281
+ /**
282
+ * Clears the entire database.
283
+ */
284
+ clear() {
285
+ this._cache = {};
286
+ this._save();
287
+ this.emit('clear');
288
+ }
289
+ /**
290
+ * Creates a backup of the current database file.
291
+ * @param destPath Path to save the backup to.
292
+ */
293
+ backup(destPath) {
294
+ try {
295
+ fs_1.default.copyFileSync(this.filePath, path_1.default.resolve(destPath));
296
+ return true;
297
+ }
298
+ catch (err) {
299
+ return false;
300
+ }
301
+ }
302
+ }
303
+ exports.PureDB = PureDB;
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "pure.db",
3
+ "version": "1.0.0",
4
+ "description": "A simple, robust, and synchronous JSON-based key-value database for Node.js.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "ts-node test/comprehensive_test.ts",
10
+ "prepublishOnly": "npm run build"
11
+ },
12
+ "keywords": [
13
+ "db",
14
+ "database",
15
+ "json",
16
+ "store",
17
+ "key-value",
18
+ "quick.db",
19
+ "snap.db"
20
+ ],
21
+ "author": "Thendra",
22
+ "license": "MIT",
23
+ "devDependencies": {
24
+ "@types/node": "^22.13.1",
25
+ "ts-node": "^10.9.2",
26
+ "typescript": "^5.7.3"
27
+ },
28
+ "directories": {
29
+ "test": "test"
30
+ },
31
+ "dependencies": {
32
+ "undici-types": "^7.16.0"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/justthendra/pure.db.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/justthendra/pure.db/issues"
40
+ },
41
+ "homepage": "https://github.com/justthendra/pure.db#readme",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,330 @@
1
+
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { EventEmitter } from 'events';
5
+ import crypto, { BinaryLike, CipherKey } from 'crypto';
6
+
7
+ interface PureDBOptions {
8
+ /**
9
+ * File path for the database. generic 'database.json' by default.
10
+ */
11
+ filePath?: string;
12
+ /**
13
+ * Secret key for encryption. If provided, the database will be encrypted.
14
+ * Must be 32 characters long.
15
+ */
16
+ encryptionKey?: string;
17
+ /**
18
+ * Format the JSON file with indentation for readability. Default: true.
19
+ * Set to false for smaller file size.
20
+ */
21
+ pretty?: boolean;
22
+ /**
23
+ * Enable debug logging.
24
+ */
25
+ debug?: boolean;
26
+ }
27
+
28
+ export class PureDB extends EventEmitter {
29
+ public filePath: string;
30
+ public options: PureDBOptions;
31
+ private _cache: any = {};
32
+ private _algorithm = 'aes-256-cbc';
33
+ private _ivLength = 16;
34
+
35
+ constructor(options: PureDBOptions | string = {}) {
36
+ super();
37
+
38
+ if (typeof options === 'string') {
39
+ options = { filePath: options };
40
+ }
41
+
42
+ this.options = {
43
+ filePath: 'database.json',
44
+ pretty: true,
45
+ ...options
46
+ };
47
+
48
+ this.filePath = path.resolve(this.options.filePath!);
49
+
50
+ // Validate Encryption Key
51
+ if (this.options.encryptionKey) {
52
+ if (this.options.encryptionKey.length !== 32) {
53
+ throw new Error('Encryption key must be exactly 32 characters long.');
54
+ }
55
+ }
56
+
57
+ this._load();
58
+ }
59
+
60
+ /**
61
+ * Loads the database from disk into memory.
62
+ */
63
+ private _load(): void {
64
+ try {
65
+ if (!fs.existsSync(this.filePath)) {
66
+ this._cache = {};
67
+ this._save(); // Create file
68
+ return;
69
+ }
70
+
71
+ const fileContent = fs.readFileSync(this.filePath, 'utf-8');
72
+
73
+ if (!fileContent.trim()) {
74
+ this._cache = {};
75
+ return;
76
+ }
77
+
78
+ if (this.options.encryptionKey) {
79
+ try {
80
+ this._cache = JSON.parse(this._decrypt(fileContent));
81
+ } catch (e) {
82
+ // Fallback check: maybe it wasn't encrypted before?
83
+ // Or wrong key.
84
+ if (this.options.debug) console.error("Decryption failed. attempting plain text read.", e);
85
+ try {
86
+ this._cache = JSON.parse(fileContent);
87
+ } catch (e2) {
88
+ this._cache = {}; // Corrupt file or wrong key
89
+ }
90
+ }
91
+ } else {
92
+ this._cache = JSON.parse(fileContent);
93
+ }
94
+
95
+ } catch (err) {
96
+ if (this.options.debug) console.error("Load Error:", err);
97
+ this._cache = {};
98
+ }
99
+ this.emit('ready', this);
100
+ }
101
+
102
+ /**
103
+ * Saves the current memory cache to disk.
104
+ */
105
+ private _save(): void {
106
+ try {
107
+ let dataToWrite: string;
108
+
109
+ if (this.options.encryptionKey) {
110
+ dataToWrite = this._encrypt(JSON.stringify(this._cache));
111
+ } else {
112
+ dataToWrite = JSON.stringify(this._cache, null, this.options.pretty ? 2 : undefined);
113
+ }
114
+
115
+ // Write to temp file then rename (atomic write prevention of corruption)
116
+ const tempPath = `${this.filePath}.tmp`;
117
+ fs.writeFileSync(tempPath, dataToWrite);
118
+ fs.renameSync(tempPath, this.filePath);
119
+
120
+ } catch (err) {
121
+ if (this.options.debug) console.error("Write Error:", err);
122
+ this.emit('error', err);
123
+ }
124
+ }
125
+
126
+ // --- Encryption Helpers ---
127
+ private _encrypt(text: string): string {
128
+ if (!this.options.encryptionKey) return text;
129
+ const iv = crypto.randomBytes(this._ivLength);
130
+ const cipher = crypto.createCipheriv(this._algorithm, Buffer.from(this.options.encryptionKey), iv);
131
+ let encrypted = cipher.update(text);
132
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
133
+ return iv.toString('hex') + ':' + encrypted.toString('hex');
134
+ }
135
+
136
+ private _decrypt(text: string): string {
137
+ if (!this.options.encryptionKey) return text;
138
+ const textParts = text.split(':');
139
+ const iv = Buffer.from(textParts.shift()!, 'hex');
140
+ const encryptedText = Buffer.from(textParts.join(':'), 'hex');
141
+ const decipher = crypto.createDecipheriv(this._algorithm, Buffer.from(this.options.encryptionKey), iv);
142
+ let decrypted = decipher.update(encryptedText);
143
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
144
+ return decrypted.toString();
145
+ }
146
+
147
+ // --- Deep Object Helpers ---
148
+ private _setDeep(path: string, value: any): void {
149
+ const keys = path.split('.');
150
+ let current = this._cache;
151
+ for (let i = 0; i < keys.length - 1; i++) {
152
+ const key = keys[i];
153
+ if (!(key in current) || typeof current[key] !== 'object') {
154
+ current[key] = {};
155
+ }
156
+ current = current[key];
157
+ }
158
+ current[keys[keys.length - 1]] = value;
159
+ }
160
+
161
+ private _getDeep(path: string): any {
162
+ const keys = path.split('.');
163
+ let current = this._cache;
164
+ for (const key of keys) {
165
+ if (current === undefined || current === null || typeof current !== 'object') return undefined;
166
+ current = current[key];
167
+ }
168
+ return current;
169
+ }
170
+
171
+ private _deleteDeep(path: string): boolean {
172
+ const keys = path.split('.');
173
+ let current = this._cache;
174
+ for (let i = 0; i < keys.length - 1; i++) {
175
+ const key = keys[i];
176
+ if (current === undefined || current === null || typeof current !== 'object') return false;
177
+ current = current[key];
178
+ }
179
+ if (current && typeof current === 'object') {
180
+ const lastKey = keys[keys.length - 1];
181
+ if (lastKey in current) {
182
+ delete current[lastKey];
183
+ return true;
184
+ }
185
+ }
186
+ return false;
187
+ }
188
+
189
+ // --- Public API ---
190
+
191
+ /**
192
+ * Set a value in the database.
193
+ * @param key The key to set (supports dot notation)
194
+ * @param value The value to store
195
+ */
196
+ public set<T = any>(key: string, value: T): T {
197
+ this._setDeep(key, value);
198
+ this._save();
199
+ this.emit('set', key, value);
200
+ return value;
201
+ }
202
+
203
+ /**
204
+ * Get a value from the database.
205
+ * @param key The key to look for (supports dot notation)
206
+ * @param defaultValue Optional default value if key not found
207
+ */
208
+ public get<T = any>(key: string, defaultValue?: T): T | undefined {
209
+ const val = this._getDeep(key);
210
+ return val === undefined ? defaultValue : val;
211
+ }
212
+
213
+ /**
214
+ * Check if a key exists in the database.
215
+ */
216
+ public has(key: string): boolean {
217
+ return this.get(key) !== undefined;
218
+ }
219
+
220
+ /**
221
+ * Delete a key from the database.
222
+ */
223
+ public delete(key: string): boolean {
224
+ const deleted = this._deleteDeep(key);
225
+ if (deleted) {
226
+ this._save();
227
+ this.emit('delete', key);
228
+ }
229
+ return deleted;
230
+ }
231
+
232
+ /**
233
+ * Add a number to a key.
234
+ */
235
+ public add(key: string, count: number): number {
236
+ const current = this.get<number>(key, 0);
237
+ if (typeof current !== 'number') throw new Error(`Value at key "${key}" is not a number.`);
238
+ const newVal = current + count;
239
+ this.set(key, newVal);
240
+ return newVal;
241
+ }
242
+
243
+ /**
244
+ * Subtract a number from a key.
245
+ */
246
+ public subtract(key: string, count: number): number {
247
+ return this.add(key, -count);
248
+ }
249
+
250
+ /**
251
+ * Perform mathematical operations on a key.
252
+ */
253
+ public math(key: string, operator: '+' | '-' | '*' | '/' | '%', operand: number): number {
254
+ const current = this.get<number>(key, 0);
255
+ if (typeof current !== 'number') throw new Error(`Value at key "${key}" is not a number.`);
256
+
257
+ let newVal: number;
258
+ switch(operator) {
259
+ case '+': newVal = current + operand; break;
260
+ case '-': newVal = current - operand; break;
261
+ case '*': newVal = current * operand; break;
262
+ case '/': newVal = current / operand; break;
263
+ case '%': newVal = current % operand; break;
264
+ default: throw new Error(`Invalid operator: ${operator}`);
265
+ }
266
+
267
+ this.set(key, newVal);
268
+ return newVal;
269
+ }
270
+
271
+ /**
272
+ * Push an element to an array.
273
+ */
274
+ public push<T = any>(key: string, ...elements: T[]): T[] {
275
+ const current = this.get<T[]>(key, []);
276
+ if (!Array.isArray(current)) throw new Error(`Value at key "${key}" is not an array.`);
277
+
278
+ current.push(...elements);
279
+ this.set(key, current);
280
+ return current;
281
+ }
282
+
283
+ /**
284
+ * Remove (pull) elements from an array which match the given filter or value.
285
+ */
286
+ public pull<T = any>(key: string, elementOrFilter: T | ((item: T) => boolean)): T[] {
287
+ const current = this.get<T[]>(key, []);
288
+ if (!Array.isArray(current)) throw new Error(`Value at key "${key}" is not an array.`);
289
+
290
+ let newArr: T[];
291
+ if (typeof elementOrFilter === 'function') {
292
+ // @ts-ignore
293
+ newArr = current.filter(item => !elementOrFilter(item));
294
+ } else {
295
+ newArr = current.filter(item => JSON.stringify(item) !== JSON.stringify(elementOrFilter));
296
+ }
297
+
298
+ this.set(key, newArr);
299
+ return newArr;
300
+ }
301
+
302
+ /**
303
+ * Returns the entire database object.
304
+ */
305
+ public all(): any {
306
+ return this._cache;
307
+ }
308
+
309
+ /**
310
+ * Clears the entire database.
311
+ */
312
+ public clear(): void {
313
+ this._cache = {};
314
+ this._save();
315
+ this.emit('clear');
316
+ }
317
+
318
+ /**
319
+ * Creates a backup of the current database file.
320
+ * @param destPath Path to save the backup to.
321
+ */
322
+ public backup(destPath: string): boolean {
323
+ try {
324
+ fs.copyFileSync(this.filePath, path.resolve(destPath));
325
+ return true;
326
+ } catch (err) {
327
+ return false;
328
+ }
329
+ }
330
+ }
@@ -0,0 +1,63 @@
1
+
2
+ import { PureDB } from '../src/index';
3
+ import fs from 'fs';
4
+ import assert from 'assert';
5
+
6
+ const FILE_PLAIN = 'test_plain.json';
7
+ const FILE_ENC = 'test_enc.json';
8
+
9
+ // Cleanup
10
+ if (fs.existsSync(FILE_PLAIN)) fs.unlinkSync(FILE_PLAIN);
11
+ if (fs.existsSync(FILE_ENC)) fs.unlinkSync(FILE_ENC);
12
+
13
+ async function runTests() {
14
+ console.log('--- Advanced Features Test ---');
15
+
16
+ console.log('1. Testing Events...');
17
+ const db = new PureDB({ filePath: FILE_PLAIN });
18
+
19
+ let eventTriggered = false;
20
+ db.on('set', (key, value) => {
21
+ console.log(`Event 'set' received: ${key} = ${value}`);
22
+ eventTriggered = true;
23
+ });
24
+
25
+ db.set('foo', 'bar');
26
+ assert.strictEqual(eventTriggered, true, "Event was not triggered");
27
+ console.log('✅ Events Passed');
28
+
29
+
30
+ console.log('2. Testing In-Memory Caching...');
31
+ // Mechanically, if I manually edit the file, the DB should NOT see it until reload
32
+ // because it reads from cache.
33
+ const originalValue = db.get('foo');
34
+ fs.writeFileSync(FILE_PLAIN, JSON.stringify({ foo: "hacked" }));
35
+ const cachedValue = db.get('foo');
36
+ assert.strictEqual(cachedValue, 'bar', "Cache bypassed! Should have returned memory value.");
37
+ console.log('✅ Caching Passed');
38
+
39
+
40
+ console.log('3. Testing Encryption...');
41
+ const secret = '12345678901234567890123456789012'; // 32 chars
42
+ const dbEnc = new PureDB({ filePath: FILE_ENC, encryptionKey: secret });
43
+
44
+ dbEnc.set('secretDetails', { plan: 'world_domination' });
45
+
46
+ // Check file content - should be unreadable
47
+ const rawContent = fs.readFileSync(FILE_ENC, 'utf-8');
48
+ console.log('Encrypted File Content Start:', rawContent.substring(0, 20) + '...');
49
+ assert.doesNotMatch(rawContent, /world_domination/, "File is not encrypted!");
50
+
51
+ // Check read
52
+ const readVal = dbEnc.get('secretDetails');
53
+ assert.deepStrictEqual(readVal, { plan: 'world_domination' }, "Decryption failed!");
54
+ console.log('✅ Encryption Passed');
55
+
56
+ // Cleanup
57
+ if (fs.existsSync(FILE_PLAIN)) fs.unlinkSync(FILE_PLAIN);
58
+ if (fs.existsSync(FILE_ENC)) fs.unlinkSync(FILE_ENC);
59
+
60
+ console.log('--- All Advanced Tests Passed ---');
61
+ }
62
+
63
+ runTests().catch(console.error);
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+
2
+ {
3
+ "compilerOptions": {
4
+ "target": "ES2020",
5
+ "module": "commonjs",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "**/*.test.ts"]
16
+ }