snowbase 5.1.0 → 6.0.1
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/CHANGELOG.md +34 -0
- package/LICENSE +7 -0
- package/README.md +229 -31
- package/bin/snowbase.js +315 -0
- package/package.json +50 -16
- package/src/Snowbase.js +403 -0
- package/src/SnowbaseAsync.js +402 -0
- package/src/helpers/crypto.js +69 -0
- package/src/helpers/utils.js +234 -0
- package/src/helpers/utilsAsync.js +229 -0
- package/src/storages/FileStorage.js +170 -0
- package/src/storages/FileStorageAsync.js +167 -0
- package/.upm/store.json +0 -1
- package/index.js +0 -5
- package/src/Local.js +0 -45
- package/src/Server.js +0 -121
- package/src/index.js +0 -4
package/src/Snowbase.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { encrypt, decrypt } from "./helpers/crypto.js";
|
|
2
|
+
import { parsePath, log, getTargetFromData } from "./helpers/utils.js";
|
|
3
|
+
import { resolve, join } from "path";
|
|
4
|
+
import { EventEmitter } from "events";
|
|
5
|
+
import FileStorage from "./storages/FileStorage.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Snowbase database in JSON, with encription, logging and backup
|
|
9
|
+
* @class Snowbase
|
|
10
|
+
* @extends EventEmitter
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* import Snowbase from "snowbase";
|
|
14
|
+
*
|
|
15
|
+
* const db = new Snowbase({
|
|
16
|
+
* baseDir: "./database"// Default is './snowbase'
|
|
17
|
+
* encryption: true,
|
|
18
|
+
* secret: "password123",
|
|
19
|
+
* logging: true,
|
|
20
|
+
* backup: true
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* db.on("ready", () => {
|
|
24
|
+
* console.log("Database is ready");
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* db.on("error", (err) => {
|
|
28
|
+
* console.error(err);
|
|
29
|
+
* });
|
|
30
|
+
* db.set("user/name", "Pedro");
|
|
31
|
+
* console.log(db.get("user/name"))// Pedro
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export default class Snowbase extends EventEmitter {
|
|
35
|
+
/**
|
|
36
|
+
* @param {Object} [args]
|
|
37
|
+
* @param {string} [args.baseDir='./snowbase'] - The base directory for the database.
|
|
38
|
+
* @param {boolean} [args.encryption=false] - Enable encryption
|
|
39
|
+
* @param {string} [args.secret] - Encryption secret
|
|
40
|
+
* @param {boolean} [args.logging=false] - Enable logging
|
|
41
|
+
* @param {boolean} [args.backup=false] - Enable backup
|
|
42
|
+
*/
|
|
43
|
+
constructor(args = {}) {
|
|
44
|
+
super();
|
|
45
|
+
this.baseDir = args.baseDir ? resolve(args.baseDir) : resolve("./snowbase");
|
|
46
|
+
if (args.encryption === true) {
|
|
47
|
+
if (!args.secret)
|
|
48
|
+
this._error(
|
|
49
|
+
"[INVALID_SECRET] Encryption secret is required when encryption is enabled",
|
|
50
|
+
);
|
|
51
|
+
this.secret = args.secret;
|
|
52
|
+
this.encryption = true;
|
|
53
|
+
} else {
|
|
54
|
+
this.encryption = false;
|
|
55
|
+
}
|
|
56
|
+
if (args.logging == true) {
|
|
57
|
+
this.logsDir = join(this.baseDir, "logs");
|
|
58
|
+
this.logging = true;
|
|
59
|
+
} else this.logging = false;
|
|
60
|
+
|
|
61
|
+
if (args.backup == true) {
|
|
62
|
+
this.backupDir = join(this.baseDir, "backups");
|
|
63
|
+
this.backup = true;
|
|
64
|
+
} else this.backup = false;
|
|
65
|
+
|
|
66
|
+
this.path = join(this.baseDir, "snowbase.json"); //Default path for the database file
|
|
67
|
+
if (this.logsDir) this.logsPath = join(this.logsDir, "snowbase.log");
|
|
68
|
+
|
|
69
|
+
this.storage = new FileStorage({
|
|
70
|
+
baseDir: this.baseDir,
|
|
71
|
+
encryption: this.encryption,
|
|
72
|
+
secret: this.secret || "",
|
|
73
|
+
backup: this.backup,
|
|
74
|
+
logging: this.logging,
|
|
75
|
+
error: this._error.bind(this),
|
|
76
|
+
emit: this.emit.bind(this),
|
|
77
|
+
log: this._log.bind(this),
|
|
78
|
+
});
|
|
79
|
+
this.emit("ready");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {any} data
|
|
84
|
+
* @param {string} [secret]
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
_encrypt(data, secret) {
|
|
88
|
+
return encrypt(data, secret ? secret : this.secret || "");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {string} raw
|
|
93
|
+
* @param {string} [secret]
|
|
94
|
+
* @returns {any}
|
|
95
|
+
*/
|
|
96
|
+
_decrypt(raw, secret) {
|
|
97
|
+
return decrypt(raw, secret ? secret : this.secret || "");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {any} err
|
|
102
|
+
* @returns {never}
|
|
103
|
+
*/
|
|
104
|
+
_error(err) {
|
|
105
|
+
const error = new Error(String(err));
|
|
106
|
+
if (this.listenerCount("error") > 0) this.emit("error", error);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string|undefined|Function} path - Path to parse
|
|
112
|
+
* @returns {string[]} Array of path parts
|
|
113
|
+
*/
|
|
114
|
+
_parsePath(path) {
|
|
115
|
+
return parsePath(path, this._error.bind(this));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {string|undefined|Function} path - Path to get target
|
|
120
|
+
* @returns {*} Target value
|
|
121
|
+
*/
|
|
122
|
+
_getTarget(path) {
|
|
123
|
+
const parts = this._parsePath(path);
|
|
124
|
+
let current = this.storage.read();
|
|
125
|
+
for (const part of parts) {
|
|
126
|
+
if (
|
|
127
|
+
current == null ||
|
|
128
|
+
typeof current !== "object" ||
|
|
129
|
+
!Object.hasOwn(current, part)
|
|
130
|
+
) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
current = current[part];
|
|
134
|
+
}
|
|
135
|
+
return current;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {Object} data - Data to get target from
|
|
140
|
+
* @param {string} path - Path to get target from
|
|
141
|
+
* @returns {*} Target value
|
|
142
|
+
*/
|
|
143
|
+
_getTargetFromData(data, path) {
|
|
144
|
+
return getTargetFromData(data, path, this._error.bind(this));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {string} operation - Operation to log
|
|
149
|
+
* @param {Object} [details] - Details of the operation
|
|
150
|
+
* @returns {string}
|
|
151
|
+
*/
|
|
152
|
+
_log(operation, details) {
|
|
153
|
+
return log(
|
|
154
|
+
operation,
|
|
155
|
+
details,
|
|
156
|
+
this.logsPath || "",
|
|
157
|
+
this.emit.bind(this),
|
|
158
|
+
this.logging,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Gets a value at a specified path
|
|
164
|
+
* @param {string} [path] - Path to get value
|
|
165
|
+
* @returns {*} Value at the specified path
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* db.get("user/name"); // returns "Pedro"
|
|
169
|
+
*
|
|
170
|
+
* db.get(); // returns the entire database
|
|
171
|
+
*/
|
|
172
|
+
get(path) {
|
|
173
|
+
const data = this.storage.read();
|
|
174
|
+
if (arguments.length === 0) {
|
|
175
|
+
this._log("get", { path: undefined, data });
|
|
176
|
+
return data;
|
|
177
|
+
}
|
|
178
|
+
if (path === undefined) {
|
|
179
|
+
this._error("[INVALID_PATH] Invalid path to get");
|
|
180
|
+
}
|
|
181
|
+
const value = this._getTargetFromData(data, path);
|
|
182
|
+
this._log("get", { path, value });
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Removes a value at a specified path
|
|
188
|
+
* @param {string} path - Path to remove value
|
|
189
|
+
* @returns {boolean} Returns `true` if the value was removed, `false` otherwise
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* db.get("user")// { name: "Pedro" }
|
|
193
|
+
*
|
|
194
|
+
* db.remove("user/name"); // returns true
|
|
195
|
+
*
|
|
196
|
+
* db.remove("user/age"); // returns false
|
|
197
|
+
*/
|
|
198
|
+
remove(path) {
|
|
199
|
+
if (path === undefined)
|
|
200
|
+
this._error("[INVALID_PATH] Invalid path to remove");
|
|
201
|
+
const parts = this._parsePath(path);
|
|
202
|
+
const data = this.storage.read();
|
|
203
|
+
let current = data;
|
|
204
|
+
|
|
205
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
206
|
+
const key = parts[i];
|
|
207
|
+
if (
|
|
208
|
+
current == null ||
|
|
209
|
+
typeof current !== "object" ||
|
|
210
|
+
!Object.hasOwn(current, key)
|
|
211
|
+
) {
|
|
212
|
+
this._log("remove", { path, value: undefined, removed: false });
|
|
213
|
+
return false; //returns false if the path does not exist
|
|
214
|
+
}
|
|
215
|
+
current = current[key];
|
|
216
|
+
}
|
|
217
|
+
const lastKey = parts[parts.length - 1];
|
|
218
|
+
if (!Object.hasOwn(current, lastKey)) {
|
|
219
|
+
this._log("remove", { path, value: undefined, removed: false });
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
this._log("remove", { path, value: current[lastKey], removed: true });
|
|
223
|
+
delete current[lastKey];
|
|
224
|
+
this.storage.write(data);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Clears the entire database
|
|
230
|
+
* @returns {boolean} Returns `true` if the database was cleared
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* db.clear(); // returns true
|
|
234
|
+
*/
|
|
235
|
+
clear() {
|
|
236
|
+
const data = this.storage.read();
|
|
237
|
+
this.storage.write({});
|
|
238
|
+
this._log("clear", { value: data });
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Sets a value at a specified path
|
|
244
|
+
* @param {string} path - Path to set value
|
|
245
|
+
* @param {*} value - Value to set
|
|
246
|
+
* @returns {void}
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* db.set("user/name", "Pedro"); // set the value of user/name to "Pedro"
|
|
250
|
+
*/
|
|
251
|
+
set(path, value) {
|
|
252
|
+
if (!path) this._error("[MISSING_KEY] Missing key to set");
|
|
253
|
+
if (value === undefined)
|
|
254
|
+
this._error("[MISSING_VALUE] Missing value to set");
|
|
255
|
+
const parts = this._parsePath(path);
|
|
256
|
+
const data = this.storage.read();
|
|
257
|
+
let current = data;
|
|
258
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
259
|
+
const key = parts[i];
|
|
260
|
+
if (current[key] == null || typeof current[key] !== "object") {
|
|
261
|
+
current[key] = {};
|
|
262
|
+
}
|
|
263
|
+
current = current[key];
|
|
264
|
+
}
|
|
265
|
+
current[parts[parts.length - 1]] = value;
|
|
266
|
+
this.storage.write(data);
|
|
267
|
+
this._log("set", { path, key: parts[parts.length - 1], value });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Alias for set(path, value)
|
|
272
|
+
* @param {string} path - Path to save value
|
|
273
|
+
* @param {*} value - Value to save
|
|
274
|
+
* @returns {void}
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* db.save("user/name", "Pedro"); // save the value of user/name to "Pedro"
|
|
278
|
+
*/
|
|
279
|
+
save(path, value) {
|
|
280
|
+
return this.set(path, value);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Return all values from a specified path with a filter.
|
|
285
|
+
*
|
|
286
|
+
* If dont provide path, it will search in the entire database.
|
|
287
|
+
* It only search in the first level of the path (if you provide path='/users', it will only search in the users object, not in the nested objects).
|
|
288
|
+
*
|
|
289
|
+
* @param {string|((value: { key: string; value: any; }, index: number, array: { key: string; value: any; }[]) => any)} [path] - Path to get all values from or filter function if path is omitted
|
|
290
|
+
* @param {((value: { key: string; value: any; }, index: number, array: { key: string; value: any; }[]) => any)} [filter] - Filter to apply to the values
|
|
291
|
+
* @returns {{ key: string; value: any; }[]} Array of key-value pairs
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* db.get("users")// { pedro: {name: "Pedro"}, laura: {name: "Laura"}, john: {name: "John"}}
|
|
295
|
+
* db.get("animals")// {dog: {name: "Duke"}, cat: {name: "Princess"}}
|
|
296
|
+
*
|
|
297
|
+
* db.all("users", (x) => x.key.includes("r")) // returns all values with key that includes "r" ( [ { key: "pedro", value: { ... } }, { key: "laura", value: { ... } } ] )
|
|
298
|
+
* db.all((x) => x.key.includes("ani")) // returns all values with key that includes "ani" ( [ { key: "animals", value: { ... } } ] )
|
|
299
|
+
*/
|
|
300
|
+
all(path, filter) {
|
|
301
|
+
if (typeof path === "function") {
|
|
302
|
+
filter = path;
|
|
303
|
+
path = undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (filter !== undefined && typeof filter !== "function") {
|
|
307
|
+
this._error("[INVALID_FILTER] Filter must be a function");
|
|
308
|
+
}
|
|
309
|
+
let target =
|
|
310
|
+
path !== undefined ? this._getTarget(path) : this.storage.read();
|
|
311
|
+
if (target == null || typeof target !== "object") return [];
|
|
312
|
+
|
|
313
|
+
const formatted = Object.entries(target).map(([key, value]) => ({
|
|
314
|
+
key,
|
|
315
|
+
value,
|
|
316
|
+
}));
|
|
317
|
+
return filter ? formatted.filter(filter) : formatted;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Checks if a path exists
|
|
322
|
+
* @param {string} path - Path to check
|
|
323
|
+
* @returns {boolean} Returns `true` if the path exists, `false` otherwise
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* db.get("user")// { name: "Pedro" }
|
|
327
|
+
*
|
|
328
|
+
* db.has("user/name"); // true
|
|
329
|
+
*
|
|
330
|
+
* db.has("user/age"); // false
|
|
331
|
+
*/
|
|
332
|
+
has(path) {
|
|
333
|
+
if (!path) this._error("[INVALID_PATH] Invalid path to check");
|
|
334
|
+
return this._getTarget(path) !== undefined;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Return the number of values at a specified path
|
|
339
|
+
*
|
|
340
|
+
* If dont provide path, it will search in the entire database.
|
|
341
|
+
* It only search in the first level of the path (if you provide path='/users', it will only search in the users object, not in the nested objects).
|
|
342
|
+
*
|
|
343
|
+
* @param {string|((value: { key: string; value: any; }, index: number, array: { key: string; value: any; }[]) => any)} [path] - Path to count values from or filter function if path is omitted
|
|
344
|
+
* @param {(value: { key: string; value: any; }, index: number, array: { key: string; value: any; }[]) => any} [filter] - Filter to apply to the values
|
|
345
|
+
* @returns {number} Number of values at the specified path
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* db.get("users")// { pedro: {name: "Pedro"}, laura: {name: "Laura"}, john: {name: "John"}}
|
|
349
|
+
*
|
|
350
|
+
* db.count("users"); // 3
|
|
351
|
+
*
|
|
352
|
+
* db.count("users", (x) => x.key.includes("r")); // 2
|
|
353
|
+
*/
|
|
354
|
+
count(path, filter) {
|
|
355
|
+
if (path === undefined && filter === undefined) return this.all().length;
|
|
356
|
+
if (typeof path === "function") {
|
|
357
|
+
filter = path;
|
|
358
|
+
path = undefined;
|
|
359
|
+
}
|
|
360
|
+
//how many entries are in the database, with an optional filter function to count only specific entries
|
|
361
|
+
return this.all(path, filter).length;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Return the first value that satisfies the predicate function
|
|
366
|
+
*
|
|
367
|
+
* If dont provide path, it will search in the entire database.
|
|
368
|
+
* It only search in the first level of the path (if you provide path='/users', it will only search in the users object, not in the nested objects).
|
|
369
|
+
*
|
|
370
|
+
* @param {string|((value: { key: string; value: any; }, index: number, array: { key: string; value: any; }[]) => any)} [path] - Path to find value in or predicate function if path is omitted
|
|
371
|
+
* @param {(value: { key: string; value: any; }, index: number, array: { key: string; value: any; }[]) => any} [predicate] - Predicate function to apply to the values
|
|
372
|
+
* @returns {{ key: string; value: any; }|null} Value at the specified path
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* db.get("users")// { laura: {name: "Laura"}, john: {name: "John"}, pedro: {name: "Pedro"} }
|
|
376
|
+
*
|
|
377
|
+
* db.find("users", (x) => x.key.includes("r")); // { key: "laura", value: { ... } }
|
|
378
|
+
*
|
|
379
|
+
* db.find((x) => x.key.includes("u")); // { key: "user", value: { ... } }
|
|
380
|
+
*
|
|
381
|
+
* db.find((x) => x.key.includes("z")) // null
|
|
382
|
+
*/
|
|
383
|
+
find(path, predicate) {
|
|
384
|
+
if (path === undefined && predicate === undefined) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
if (typeof path === "function") {
|
|
388
|
+
predicate = path;
|
|
389
|
+
path = undefined;
|
|
390
|
+
}
|
|
391
|
+
if (typeof predicate !== "function")
|
|
392
|
+
this._error("[INVALID_PREDICATE] Predicate must be a function");
|
|
393
|
+
const data =
|
|
394
|
+
path !== undefined ? this._getTarget(path) : this.storage.read();
|
|
395
|
+
if (data === undefined || typeof data !== "object") return null;
|
|
396
|
+
const entries = Object.entries(data).map(([key, value]) => ({
|
|
397
|
+
key,
|
|
398
|
+
value,
|
|
399
|
+
}));
|
|
400
|
+
|
|
401
|
+
return (predicate && entries.find(predicate)) || null;
|
|
402
|
+
}
|
|
403
|
+
}
|