snowbase 5.1.0 → 6.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.
@@ -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
+ }