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.
@@ -0,0 +1,402 @@
1
+ import { stat, mkdir, readdir, unlink, copyFile } from "node:fs/promises";
2
+ import { encrypt, decrypt } from "./helpers/crypto.js";
3
+ import { parsePath, log, getTargetFromData } from "./helpers/utilsAsync.js";
4
+ import FileStorageAsync from "./storages/FileStorageAsync.js";
5
+ import { resolve, join } from "path";
6
+ import { EventEmitter } from "events";
7
+
8
+ /**
9
+ * Snowbase database in JSON, with encription, logging and backup
10
+ * @class SnowbaseAsync
11
+ * @extends EventEmitter
12
+ *
13
+ * @example
14
+ * import Snowbase from "snowbase/async";
15
+ *
16
+ * const db = new Snowbase({
17
+ * baseDir: "./database"// Default is './snowbase'
18
+ * encryption: true,
19
+ * secret: "password123",
20
+ * logging: true,
21
+ * backup: true
22
+ * });
23
+ *
24
+ * db.on("ready", () => {
25
+ * console.log("Database is ready");
26
+ * });
27
+ *
28
+ * db.on("error", (err) => {
29
+ * console.error(err);
30
+ * });
31
+ * await db.set("user/name", "Pedro");
32
+ * console.log(await db.get("user/name"))// Pedro
33
+ */
34
+ export default class SnowbaseAsync 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 FileStorageAsync({
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
+ }
80
+
81
+ /**
82
+ * @param {any} data
83
+ * @param {string} [secret]
84
+ * @returns {Promise<string>}
85
+ */
86
+ async _encrypt(data, secret) {
87
+ return encrypt(data, secret ? secret : this.secret || "");
88
+ }
89
+
90
+ /**
91
+ * @param {string} raw
92
+ * @param {string} [secret]
93
+ * @returns {Promise<any>}
94
+ */
95
+ async _decrypt(raw, secret) {
96
+ return decrypt(raw, secret ? secret : this.secret || "");
97
+ }
98
+
99
+ /**
100
+ * @param {any} err
101
+ * @returns {Promise<never>}
102
+ */
103
+ async _error(err) {
104
+ const error = new Error(String(err));
105
+ if (this.listenerCount("error") > 0) this.emit("error", error);
106
+ throw error;
107
+ }
108
+
109
+ /**
110
+ * @param {string|Function|undefined} path - Path to parse
111
+ * @returns {Promise<string[]|any>} - Path parts or full data if path is undefined or function
112
+ */
113
+ async _parsePath(path) {
114
+ return await parsePath(path, this._error.bind(this));
115
+ }
116
+ /**
117
+ * @param {string|undefined|Function} path - Path to parse
118
+ * @returns {Promise<any>} - Target value
119
+ */
120
+ async _getTarget(path) {
121
+ const parts = await this._parsePath(path);
122
+ let current = await this.storage.readAsync();
123
+ for (const part of parts) {
124
+ if (
125
+ current == null ||
126
+ typeof current !== "object" ||
127
+ !Object.hasOwn(current, part)
128
+ ) {
129
+ return undefined;
130
+ }
131
+ current = current[part];
132
+ }
133
+ return current;
134
+ }
135
+
136
+ /**
137
+ * @param {Object} data - Data to get target from
138
+ * @param {string} path - Path to get target from
139
+ * @returns {Promise<*>} Target value
140
+ */
141
+ async _getTargetFromData(data, path) {
142
+ return await getTargetFromData(data, path, this._error.bind(this));
143
+ }
144
+
145
+ /**
146
+ * @param {string} operation - Operation to log
147
+ * @param {Object} [details] - Details of the operation
148
+ * @returns {Promise<string>}
149
+ */
150
+ async _log(operation, details) {
151
+ return await log(
152
+ operation,
153
+ details,
154
+ this.logsPath || "",
155
+ this.emit.bind(this),
156
+ this.logging,
157
+ );
158
+ }
159
+ /**
160
+ * @param {string} [path]
161
+ * @returns {Promise<any>}
162
+ * @example
163
+ * await db.get("user/name"); // returns "Pedro"
164
+ *
165
+ * await db.get(); // returns the entire database
166
+ */
167
+ async get(path) {
168
+ const data = await this.storage.readAsync();
169
+ if (path === undefined) {
170
+ this.emit("get", { path: undefined, data });
171
+ return data;
172
+ }
173
+ const value = await this._getTargetFromData(data, path);
174
+ this.emit("get", { path, value });
175
+ return value;
176
+ }
177
+
178
+ /**
179
+ * @param {string} [path]
180
+ * @returns {Promise<boolean>}
181
+ *
182
+ * @example
183
+ * await db.get("user")// { name: "Pedro" }
184
+ *
185
+ * await db.remove("user/name"); // returns true
186
+ *
187
+ * await db.remove("user/age"); // returns false
188
+ */
189
+ async remove(path) {
190
+ if (path === undefined)
191
+ this._error("[INVALID_PATH] Invalid path to remove");
192
+ const parts = await this._parsePath(path);
193
+ const data = await this.storage.readAsync();
194
+ let current = data;
195
+
196
+ for (let i = 0; i < parts.length - 1; i++) {
197
+ const key = parts[i];
198
+ if (
199
+ current == null ||
200
+ typeof current !== "object" ||
201
+ !Object.hasOwn(current, key)
202
+ ) {
203
+ await this._log("remove", {
204
+ path,
205
+ value: undefined,
206
+ removed: false,
207
+ });
208
+ return false; //returns false if the path does not exist
209
+ }
210
+ current = current[key];
211
+ }
212
+ const lastKey = parts[parts.length - 1];
213
+ if (!Object.hasOwn(current, lastKey)) {
214
+ this._log("remove", { path, value: undefined, removed: false });
215
+ return false;
216
+ }
217
+ this._log("remove", { path, value: current[lastKey], removed: true });
218
+ delete current[lastKey];
219
+ await this.storage.writeAsync(data);
220
+ return true;
221
+ }
222
+
223
+ /**
224
+ * Clears the entire database
225
+ * @returns {Promise<boolean>} Returns `true` if the database was cleared
226
+ *
227
+ * @example
228
+ * await db.clear(); // returns true
229
+ */
230
+ async clear() {
231
+ const data = await this.storage.readAsync();
232
+ await this.storage.writeAsync({});
233
+ await this._log("clear", { value: data });
234
+ return true;
235
+ }
236
+
237
+ /**
238
+ * @param {string} path
239
+ * @param {any} value
240
+ * @returns {Promise<void>}
241
+ *
242
+ * @example
243
+ * await db.set("user/name", "Pedro"); // set the value of user/name to "Pedro"
244
+ */
245
+ async set(path, value) {
246
+ if (!path) this._error("[MISSING_KEY] Missing a key to set");
247
+ if (value === undefined)
248
+ this._error("[MISSING_VALUE] Missing value to set");
249
+ const parts = await this._parsePath(path);
250
+ const data = await this.storage.readAsync();
251
+ let current = data;
252
+ for (let i = 0; i < parts.length - 1; i++) {
253
+ const key = parts[i];
254
+ if (current[key] == null || typeof current[key] !== "object") {
255
+ current[key] = {};
256
+ }
257
+ current = current[key];
258
+ }
259
+ current[parts[parts.length - 1]] = value;
260
+ await this.storage.writeAsync(data);
261
+ this._log("set", { path, key: parts[parts.length - 1], value });
262
+ }
263
+
264
+ /**
265
+ * @param {string} path
266
+ * @param {any} value
267
+ * @returns {Promise<void>}
268
+ *
269
+ * @example
270
+ * await db.set("user/name", "Pedro"); // set the value of user/name to "Pedro"
271
+ */
272
+ async save(path, value) {
273
+ await this.set(path, value);
274
+ }
275
+
276
+ /**
277
+ * Return all values from a specified path with a filter.
278
+ *
279
+ * If dont provide path, it will search in the entire database.
280
+ * 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).
281
+ *
282
+ * @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
283
+ * @param {((value: { key: string; value: any; }, index: number, array: { key: string; value: any; }[]) => any)} [filter] - Filter to apply to the values
284
+ * @returns {Promise<{ key: string; value: any; }[]>} Array of key-value pairs
285
+ *
286
+ * @example
287
+ * await db.get("users")// { pedro: {name: "Pedro"}, laura: {name: "Laura"}, john: {name: "John"}}
288
+ * await db.get("animals")// {dog: {name: "Duke"}, cat: {name: "Princess"}}
289
+ *
290
+ * await db.all("users", (x) => x.key.includes("r")) // returns all values with key that includes "r" ( [ { key: "pedro", value: { ... } }, { key: "laura", value: { ... } } ] )
291
+ * await db.all((x) => x.key.includes("ani")) // returns all values with key that includes "ani" ( [ { key: "animals", value: { ... } } ] )
292
+ */
293
+ async all(path, filter) {
294
+ if (typeof path === "function") {
295
+ filter = path;
296
+ path = undefined;
297
+ }
298
+
299
+ if (filter !== undefined && typeof filter !== "function") {
300
+ this._error("[INVALID_FILTER] Filter must be a function");
301
+ }
302
+
303
+ let target =
304
+ path !== undefined
305
+ ? await this._getTarget(path)
306
+ : await this.storage.readAsync();
307
+ if (target == null || typeof target !== "object") return [];
308
+
309
+ const formatted = Object.entries(target).map(([key, value]) => ({
310
+ key,
311
+ value,
312
+ }));
313
+ return filter ? formatted.filter(filter) : formatted;
314
+ }
315
+
316
+ /**
317
+ * Checks if a path exists
318
+ * @param {string} path - Path to check
319
+ * @returns {Promise<boolean>} Returns `true` if the path exists, `false` otherwise
320
+ *
321
+ * @example
322
+ * await db.get("user")// { name: "Pedro" }
323
+ *
324
+ * await db.has("user/name"); // true
325
+ *
326
+ * await db.has("user/age"); // false
327
+ */
328
+ async has(path) {
329
+ if (!path) this._error("[INVALID_PATH] Invalid path to check");
330
+ return (await this._getTarget(path)) !== undefined;
331
+ }
332
+
333
+ /**
334
+ * Return the number of values at a specified path
335
+ *
336
+ * If dont provide path, it will search in the entire database.
337
+ * 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).
338
+ *
339
+ * @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
340
+ * @param {(value: { key: string; value: any; }, index: number, array: { key: string; value: any; }[]) => any} [filter] - Filter to apply to the values
341
+ * @returns {Promise<number>} Number of values at the specified path
342
+ *
343
+ * @example
344
+ * await db.get("users")// { pedro: {name: "Pedro"}, laura: {name: "Laura"}, john: {name: "John"}}
345
+ *
346
+ * await db.count("users"); // 3
347
+ *
348
+ * await db.count("users", (x) => x.key.includes("r")); // 2
349
+ */
350
+ async count(path, filter) {
351
+ if (path === undefined && filter === undefined)
352
+ return (await this.all()).length;
353
+ if (typeof path === "function") {
354
+ filter = path;
355
+ path = undefined;
356
+ }
357
+ //how many entries are in the database, with an optional filter function to count only specific entries
358
+ return (await this.all(path, filter)).length;
359
+ }
360
+
361
+ /**
362
+ * Return the first value that satisfies the predicate function
363
+ *
364
+ * If dont provide path, it will search in the entire database.
365
+ * 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).
366
+ *
367
+ * @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
368
+ * @param {(value: { key: string; value: any; }, index: number, array: { key: string; value: any; }[]) => any} [predicate] - Predicate function to apply to the values
369
+ * @returns {Promise<{ key: string; value: any; }|null>} Value at the specified path
370
+ *
371
+ * @example
372
+ * await db.get("users")// { laura: {name: "Laura"}, john: {name: "John"}, pedro: {name: "Pedro"} }
373
+ *
374
+ * await db.find("users", (x) => x.key.includes("r")); // { key: "laura", value: { ... } }
375
+ *
376
+ * await db.find((x) => x.key.includes("u")); // { key: "user", value: { ... } }
377
+ *
378
+ * await db.find((x) => x.key.includes("z")) // null
379
+ */
380
+ async find(path, predicate) {
381
+ if (path === undefined && predicate === undefined) {
382
+ return null;
383
+ }
384
+ if (typeof path === "function") {
385
+ predicate = path;
386
+ path = undefined;
387
+ }
388
+ if (typeof predicate !== "function")
389
+ this._error("[INVALID_PREDICATE] Predicate must be a function");
390
+ const data =
391
+ path !== undefined
392
+ ? await this._getTarget(path)
393
+ : await this.storage.readAsync();
394
+ if (data === undefined || typeof data !== "object") return null;
395
+ const entries = Object.entries(data).map(([key, value]) => ({
396
+ key,
397
+ value,
398
+ }));
399
+
400
+ return (predicate && entries.find(predicate)) || null;
401
+ }
402
+ }
@@ -0,0 +1,69 @@
1
+ import {
2
+ randomBytes,
3
+ createCipheriv,
4
+ createDecipheriv,
5
+ scryptSync,
6
+ } from "crypto";
7
+ import { stableStringify } from "./utils.js";
8
+
9
+ /**
10
+ * @param {string} password
11
+ * @param {Buffer} salt
12
+ * @returns {Buffer}
13
+ */
14
+ export function getKey(password, salt) {
15
+ return scryptSync(password, salt, 32);
16
+ }
17
+
18
+ /**
19
+ * @param {any} data
20
+ * @param {string} secret
21
+ * @returns {string}
22
+ */
23
+ export function encrypt(data, secret) {
24
+ const iv = randomBytes(12);
25
+ const salt = randomBytes(16);
26
+ const key = getKey(secret, salt);
27
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
28
+
29
+ const encrypted = Buffer.concat([
30
+ cipher.update(stableStringify(data), "utf8"),
31
+ cipher.final(),
32
+ ]);
33
+
34
+ const tag = cipher.getAuthTag();
35
+
36
+ return JSON.stringify({
37
+ salt: salt.toString("base64"),
38
+ iv: iv.toString("base64"),
39
+ tag: tag.toString("base64"),
40
+ data: encrypted.toString("base64"),
41
+ });
42
+ }
43
+
44
+ /**
45
+ * @param {string} raw
46
+ * @param {string} secret
47
+ * @returns {any}
48
+ */
49
+ export function decrypt(raw, secret) {
50
+ const payload = JSON.parse(raw);
51
+
52
+ const salt = Buffer.from(payload.salt, "base64");
53
+ const key = getKey(secret, salt);
54
+
55
+ const decipher = createDecipheriv(
56
+ "aes-256-gcm",
57
+ key,
58
+ Buffer.from(payload.iv, "base64"),
59
+ );
60
+
61
+ decipher.setAuthTag(Buffer.from(payload.tag, "base64"));
62
+
63
+ const decrypted = Buffer.concat([
64
+ decipher.update(Buffer.from(payload.data, "base64")),
65
+ decipher.final(),
66
+ ]);
67
+
68
+ return JSON.parse(decrypted.toString("utf8"));
69
+ }
@@ -0,0 +1,234 @@
1
+ import { decrypt, encrypt } from "./crypto.js";
2
+ import { createHash } from "crypto";
3
+ import { readFileSync, writeFileSync, appendFileSync, copyFileSync } from "fs";
4
+ import { join } from "path";
5
+
6
+ /**
7
+ * @param {string} path
8
+ * @returns {string}
9
+ */
10
+ export function hash(path) {
11
+ const fileBuffer = readFileSync(path);
12
+ return createHash("sha256").update(fileBuffer).digest("hex");
13
+ }
14
+
15
+ /**
16
+ * Converts a value to a stable JSON string
17
+ * @param {any} value - Value to convert
18
+ * @returns {string} - Stable JSON string
19
+ */
20
+ export function stableStringify(value) {
21
+ if (value === null || typeof value !== "object") {
22
+ return JSON.stringify(value);
23
+ }
24
+
25
+ if (Array.isArray(value)) {
26
+ return `[${value.map((v) => stableStringify(v)).join(",")}]`;
27
+ }
28
+
29
+ const keys = Object.keys(value).sort();
30
+
31
+ return `{${keys
32
+ .map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`)
33
+ .join(",")}}`;
34
+ }
35
+ /**
36
+ * Normalizes a chunk of data
37
+ * @param {string|Object} chunk - Chunk to normalize
38
+ * @returns {{raw: Object, stable: string}} - Normalized chunk
39
+ */
40
+ export function normalize(chunk) {
41
+ const obj = typeof chunk === "string" ? JSON.parse(chunk) : chunk;
42
+
43
+ return {
44
+ raw: obj,
45
+ stable: stableStringify(obj),
46
+ };
47
+ }
48
+ /**
49
+ * Splits a path like "user/name/first" into ["user", "name", "first"] for nested access
50
+ * @param {string|undefined|Function} path - Path to parse
51
+ * @param {function} error - Error callback
52
+ * @returns {string[]} - Array of path parts
53
+ */
54
+ export function parsePath(path, error) {
55
+ if (path == null || path === "") {
56
+ error("[INVALID_PATH] Invalid path");
57
+ return [];
58
+ }
59
+ if (typeof path !== "string") {
60
+ error("[INVALID_PATH] Path must be a string");
61
+ return [];
62
+ }
63
+ const parts = path.split("/").filter(Boolean);
64
+ const forbidden = new Set(["prototype", "__proto__", "constructor"]); //protecting against prototype pollution
65
+ if (parts.some((part) => forbidden.has(part)))
66
+ error("[FORBIDDEN_KEY] Path contains forbidden key");
67
+ if (parts.some((part) => part.includes(".")))
68
+ error("[INVALID_PATH] Path parts cannot contain dots");
69
+ if (parts.length === 0) error("[INVALID_PATH] Path is empty (just /)");
70
+ return parts;
71
+ }
72
+
73
+ /**
74
+ * Reads the database file
75
+ * @param {string} path - Database file path
76
+ * @param {function} error - Error callback
77
+ * @param {boolean} encryption - Is encryption enabled
78
+ * @param {string} [secret] - Encryption secret
79
+ * @returns {Record<string, any>} - Parsed database data
80
+ */
81
+ export function read(path, error, encryption, secret) {
82
+ const raw = readFileSync(path, "utf8");
83
+ let data;
84
+ try {
85
+ data = encryption ? decrypt(raw, secret || "") : JSON.parse(raw);
86
+ } catch {
87
+ if (
88
+ !JSON.parse(raw).salt ||
89
+ !JSON.parse(raw).data ||
90
+ !JSON.parse(raw).tag ||
91
+ !JSON.parse(raw).iv
92
+ )
93
+ error(
94
+ "[INVALID_DATA] Database file is not encrypted or invalid JSON (data, salt, tag or iv not found)",
95
+ );
96
+ error(
97
+ "[INVALID_DATA] Database file contains invalid JSON or invalid encryption (perhaps invalid secret)",
98
+ );
99
+ }
100
+
101
+ if (data == null || typeof data !== "object" || Array.isArray(data)) {
102
+ error("[INVALID_DATA] Database root must be an object");
103
+ }
104
+
105
+ //validate dangerous keys
106
+ const forbidden = new Set(["prototype", "__proto__", "constructor"]);
107
+ /** @param {Record<string, any>} obj */
108
+ const hasForbiddenKey = (obj) => {
109
+ for (const key of Object.keys(obj)) {
110
+ if (forbidden.has(key)) {
111
+ error("[FORBIDDEN_KEY] Database contains forbidden key: " + key);
112
+ }
113
+ if (typeof obj[key] === "object" && obj[key] !== null) {
114
+ hasForbiddenKey(obj[key]);
115
+ }
116
+ }
117
+ };
118
+
119
+ hasForbiddenKey(data);
120
+ return data;
121
+ }
122
+
123
+ /**
124
+ * Logs an operation
125
+ * @param {string} operation - Operation to log
126
+ * @param {Record<string, any>|undefined} details - Details of the operation
127
+ * @param {string|undefined} logsPath - Path to log file
128
+ * @param {function} emit - Emit event
129
+ * @param {boolean} logging - Enable logging
130
+ * @returns {string} - Timestamp
131
+ */
132
+ export function log(operation, details, logsPath, emit, logging) {
133
+ let timestamp = new Date().toISOString();
134
+ if (!logging) {
135
+ emit(operation, details ? details : undefined, timestamp);
136
+ return timestamp;
137
+ }
138
+ if (!logsPath) {
139
+ emit(operation, details ? details : undefined, timestamp);
140
+ return timestamp;
141
+ }
142
+ if (!details)
143
+ appendFileSync(logsPath, `[${timestamp}] ${operation}\n`, "utf8");
144
+ if (operation) {
145
+ switch (operation) {
146
+ case "get":
147
+ appendFileSync(
148
+ logsPath,
149
+ `[${timestamp}] ${operation} - Path: ${details?.path || "/"}\n`,
150
+ "utf8",
151
+ );
152
+ break;
153
+ case "remove":
154
+ appendFileSync(
155
+ logsPath,
156
+ `[${timestamp}] ${operation} - Path: ${details?.path || "/"}\n`,
157
+ "utf8",
158
+ );
159
+ break;
160
+ case "set":
161
+ appendFileSync(
162
+ logsPath,
163
+ `[${timestamp}] ${operation} - Path: ${details?.path || "/"}\n`,
164
+ "utf8",
165
+ );
166
+ break;
167
+ case "clear":
168
+ appendFileSync(
169
+ logsPath,
170
+ `[${timestamp}] ${operation} - Cleared all data\n`,
171
+ "utf8",
172
+ );
173
+ break;
174
+ }
175
+ }
176
+ emit(operation, details ? details : undefined, timestamp);
177
+ return timestamp;
178
+ }
179
+
180
+ /**
181
+ * Gets a target value from the data by path
182
+ * @param {Record<string, any>} data - Data to search in
183
+ * @param {string} path - Path to get target from
184
+ * @param {function} error - Error callback
185
+ * @returns {any} - Found value or undefined
186
+ */
187
+ export function getTargetFromData(data, path, error) {
188
+ const parts = parsePath(path, error);
189
+ let current = data;
190
+
191
+ for (const part of parts) {
192
+ if (
193
+ current == null ||
194
+ typeof current !== "object" ||
195
+ !Object.hasOwn(current, part)
196
+ ) {
197
+ return undefined;
198
+ }
199
+ current = current[part];
200
+ }
201
+ return current;
202
+ }
203
+
204
+ /**
205
+ * Writes data to the database file
206
+ * @param {string} path - Database file path
207
+ * @param {Record<string, any>} data - Data to write
208
+ * @param {function} error - Error callback
209
+ * @param {function} emit - Emit event
210
+ * @param {boolean} encryption - Is encryption enabled
211
+ * @param {string|undefined} secret - Encryption secret
212
+ */
213
+ export function write(path, data, error, emit, encryption, secret) {
214
+ let content = "";
215
+ try {
216
+ content = encryption ? encrypt(data, secret || "") : stableStringify(data);
217
+ } catch {
218
+ error("[INVALID_DATA] Data must be JSON serializable or encryptable");
219
+ }
220
+
221
+ writeFileSync(path, content, "utf8");
222
+ emit("_write", data, new Date().toISOString());
223
+ }
224
+
225
+ export function randomId5() {
226
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
227
+ let id = "";
228
+
229
+ for (let i = 0; i < 5; i++) {
230
+ id += chars[Math.floor(Math.random() * chars.length)];
231
+ }
232
+
233
+ return id;
234
+ }