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 +147 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +303 -0
- package/package.json +45 -0
- package/src/index.ts +330 -0
- package/test/advanced_test.ts +63 -0
- package/tsconfig.json +16 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|