snowbase 6.0.2 → 6.0.6
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 -34
- package/LICENSE +7 -7
- package/README.md +229 -229
- package/bin/snowbase.js +286 -315
- package/package.json +2 -2
- package/src/Snowbase.js +403 -403
- package/src/helpers/crypto.js +69 -69
- package/src/helpers/utilsAsync.js +228 -228
- package/src/storages/FileStorage.js +170 -170
- package/src/storages/FileStorageAsync.js +167 -167
package/src/helpers/crypto.js
CHANGED
|
@@ -1,69 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,229 +1,229 @@
|
|
|
1
|
-
import { decrypt, encrypt } from "./crypto.js";
|
|
2
|
-
import { createHash } from "crypto";
|
|
3
|
-
import { readFile, writeFile, appendFile, copyFile } from "node:fs/promises";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @param {string} path
|
|
8
|
-
* @returns {Promise<string>}
|
|
9
|
-
*/
|
|
10
|
-
export async function hash(path) {
|
|
11
|
-
const fileBuffer = await readFile(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 {Promise<string>} - Stable JSON string
|
|
19
|
-
*/
|
|
20
|
-
export async function stableStringify(value) {
|
|
21
|
-
if (value === null || typeof value !== "object") {
|
|
22
|
-
return JSON.stringify(value);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (Array.isArray(value)) {
|
|
26
|
-
return `[${(await Promise.all(value.map((v) => stableStringify(v)))).join(",")}]`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const keys = Object.keys(value).sort();
|
|
30
|
-
|
|
31
|
-
return `{${Promise.all(
|
|
32
|
-
keys
|
|
33
|
-
.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`)
|
|
34
|
-
.join(","),
|
|
35
|
-
)}}`;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Normalizes a chunk of data
|
|
39
|
-
* @param {string|Object} chunk - Chunk to normalize
|
|
40
|
-
* @returns {Promise<{raw: Object, stable: string}>} - Normalized chunk
|
|
41
|
-
*/
|
|
42
|
-
export async function normalize(chunk) {
|
|
43
|
-
const obj = typeof chunk === "string" ? JSON.parse(chunk) : chunk;
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
raw: obj,
|
|
47
|
-
stable: await stableStringify(obj),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Splits a path like "user/name/first" into ["user", "name", "first"] for nested access
|
|
52
|
-
* @param {string|undefined|Function} path - Path to parse
|
|
53
|
-
* @param {function} error - Error callback
|
|
54
|
-
* @returns {Promise<string[]>} - Array of path parts
|
|
55
|
-
*/
|
|
56
|
-
export async function parsePath(path, error) {
|
|
57
|
-
if (path == null || path === "") {
|
|
58
|
-
error("[INVALID_PATH] Invalid path");
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
if (typeof path !== "string") {
|
|
62
|
-
error("[INVALID_PATH] Path must be a string");
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
const parts = path.split("/").filter(Boolean);
|
|
66
|
-
const forbidden = new Set(["prototype", "__proto__", "constructor"]); //protecting against prototype pollution
|
|
67
|
-
if (parts.some((part) => forbidden.has(part)))
|
|
68
|
-
error("[FORBIDDEN_KEY] Path contains forbidden key");
|
|
69
|
-
if (parts.some((part) => part.includes(".")))
|
|
70
|
-
error("[INVALID_PATH] Path parts cannot contain dots");
|
|
71
|
-
if (parts.length === 0) error("[INVALID_PATH] Path is empty (just /)");
|
|
72
|
-
return parts;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Reads the database file
|
|
77
|
-
* @param {string} path - Database file path
|
|
78
|
-
* @param {function} error - Error callback
|
|
79
|
-
* @param {boolean} encryption - Is encryption enabled
|
|
80
|
-
* @param {string} [secret] - Encryption secret
|
|
81
|
-
* @returns {Promise<Record<string, any>>} - Parsed database data
|
|
82
|
-
*/
|
|
83
|
-
export async function read(path, error, encryption, secret) {
|
|
84
|
-
const raw = await readFile(path, "utf8");
|
|
85
|
-
let data;
|
|
86
|
-
try {
|
|
87
|
-
data = encryption ? decrypt(raw, secret || "") : JSON.parse(raw);
|
|
88
|
-
} catch {
|
|
89
|
-
error(
|
|
90
|
-
"[INVALID_DATA] Database file contains invalid JSON or invalid encryption",
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (data == null || typeof data !== "object" || Array.isArray(data)) {
|
|
95
|
-
error("[INVALID_DATA] Database root must be an object");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
//validate dangerous keys
|
|
99
|
-
const forbidden = new Set(["prototype", "__proto__", "constructor"]);
|
|
100
|
-
/** @param {Record<string, any>} obj */
|
|
101
|
-
const hasForbiddenKey = (obj) => {
|
|
102
|
-
for (const key of Object.keys(obj)) {
|
|
103
|
-
if (forbidden.has(key)) {
|
|
104
|
-
error("[FORBIDDEN_KEY] Database contains forbidden key: " + key);
|
|
105
|
-
}
|
|
106
|
-
if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
107
|
-
hasForbiddenKey(obj[key]);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
hasForbiddenKey(data);
|
|
113
|
-
return data;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Logs an operation
|
|
118
|
-
* @param {string} operation - Operation to log
|
|
119
|
-
* @param {Record<string, any>|undefined} details - Details of the operation
|
|
120
|
-
* @param {string|undefined} logsPath - Path to log file
|
|
121
|
-
* @param {function} emit - Emit event
|
|
122
|
-
* @param {boolean} logging - Enable logging
|
|
123
|
-
* @returns {Promise<string>} - Timestamp
|
|
124
|
-
*/
|
|
125
|
-
export async function log(operation, details, logsPath, emit, logging) {
|
|
126
|
-
let timestamp = new Date().toISOString();
|
|
127
|
-
if (!logging) {
|
|
128
|
-
await emit(operation, details ? details : undefined, timestamp);
|
|
129
|
-
return timestamp;
|
|
130
|
-
}
|
|
131
|
-
if (!logsPath) {
|
|
132
|
-
await emit(operation, details ? details : undefined, timestamp);
|
|
133
|
-
return timestamp;
|
|
134
|
-
}
|
|
135
|
-
if (!details)
|
|
136
|
-
await appendFile(logsPath, `[${timestamp}] ${operation}\n`, "utf8");
|
|
137
|
-
if (operation) {
|
|
138
|
-
switch (operation) {
|
|
139
|
-
case "get":
|
|
140
|
-
await appendFile(
|
|
141
|
-
logsPath,
|
|
142
|
-
`[${timestamp}] ${operation} - Path: ${details?.path || "/"}\n`,
|
|
143
|
-
"utf8",
|
|
144
|
-
);
|
|
145
|
-
break;
|
|
146
|
-
case "remove":
|
|
147
|
-
await appendFile(
|
|
148
|
-
logsPath,
|
|
149
|
-
`[${timestamp}] ${operation} - Path: ${details?.path || "/"}\n`,
|
|
150
|
-
"utf8",
|
|
151
|
-
);
|
|
152
|
-
break;
|
|
153
|
-
case "set":
|
|
154
|
-
await appendFile(
|
|
155
|
-
logsPath,
|
|
156
|
-
`[${timestamp}] ${operation} - Path: ${details?.path || "/"}\n`,
|
|
157
|
-
"utf8",
|
|
158
|
-
);
|
|
159
|
-
break;
|
|
160
|
-
case "clear":
|
|
161
|
-
await appendFile(
|
|
162
|
-
logsPath,
|
|
163
|
-
`[${timestamp}] ${operation} - Cleared all data\n`,
|
|
164
|
-
"utf8",
|
|
165
|
-
);
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
emit(operation, details ? details : undefined, timestamp);
|
|
170
|
-
return timestamp;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Gets a target value from the data by path
|
|
175
|
-
* @param {Record<string, any>} data - Data to search in
|
|
176
|
-
* @param {string} path - Path to get target from
|
|
177
|
-
* @param {function} error - Error callback
|
|
178
|
-
* @returns {Promise<any>} - Found value or undefined
|
|
179
|
-
*/
|
|
180
|
-
export async function getTargetFromData(data, path, error) {
|
|
181
|
-
const parts = await parsePath(path, error);
|
|
182
|
-
let current = data;
|
|
183
|
-
|
|
184
|
-
for (const part of parts) {
|
|
185
|
-
if (
|
|
186
|
-
current == null ||
|
|
187
|
-
typeof current !== "object" ||
|
|
188
|
-
!Object.hasOwn(current, part)
|
|
189
|
-
) {
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
192
|
-
current = current[part];
|
|
193
|
-
}
|
|
194
|
-
return current;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Writes data to the database file
|
|
199
|
-
* @param {string} path - Database file path
|
|
200
|
-
* @param {Record<string, any>} data - Data to write
|
|
201
|
-
* @param {function} error - Error callback
|
|
202
|
-
* @param {function} emit - Emit event
|
|
203
|
-
* @param {boolean} encryption - Is encryption enabled
|
|
204
|
-
* @param {string|undefined} secret - Encryption secret
|
|
205
|
-
*/
|
|
206
|
-
export async function write(path, data, error, emit, encryption, secret) {
|
|
207
|
-
let content = "";
|
|
208
|
-
try {
|
|
209
|
-
content = encryption
|
|
210
|
-
? encrypt(data, secret || "")
|
|
211
|
-
: await stableStringify(data);
|
|
212
|
-
} catch {
|
|
213
|
-
await error("[INVALID_DATA] Data must be JSON serializable or encryptable");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
await writeFile(path, content, "utf8");
|
|
217
|
-
emit("_write", data, new Date().toISOString());
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function randomId5() {
|
|
221
|
-
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
222
|
-
let id = "";
|
|
223
|
-
|
|
224
|
-
for (let i = 0; i < 5; i++) {
|
|
225
|
-
id += chars[Math.floor(Math.random() * chars.length)];
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return id;
|
|
1
|
+
import { decrypt, encrypt } from "./crypto.js";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { readFile, writeFile, appendFile, copyFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} path
|
|
8
|
+
* @returns {Promise<string>}
|
|
9
|
+
*/
|
|
10
|
+
export async function hash(path) {
|
|
11
|
+
const fileBuffer = await readFile(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 {Promise<string>} - Stable JSON string
|
|
19
|
+
*/
|
|
20
|
+
export async function stableStringify(value) {
|
|
21
|
+
if (value === null || typeof value !== "object") {
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return `[${(await Promise.all(value.map((v) => stableStringify(v)))).join(",")}]`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const keys = Object.keys(value).sort();
|
|
30
|
+
|
|
31
|
+
return `{${Promise.all(
|
|
32
|
+
keys
|
|
33
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`)
|
|
34
|
+
.join(","),
|
|
35
|
+
)}}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Normalizes a chunk of data
|
|
39
|
+
* @param {string|Object} chunk - Chunk to normalize
|
|
40
|
+
* @returns {Promise<{raw: Object, stable: string}>} - Normalized chunk
|
|
41
|
+
*/
|
|
42
|
+
export async function normalize(chunk) {
|
|
43
|
+
const obj = typeof chunk === "string" ? JSON.parse(chunk) : chunk;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
raw: obj,
|
|
47
|
+
stable: await stableStringify(obj),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Splits a path like "user/name/first" into ["user", "name", "first"] for nested access
|
|
52
|
+
* @param {string|undefined|Function} path - Path to parse
|
|
53
|
+
* @param {function} error - Error callback
|
|
54
|
+
* @returns {Promise<string[]>} - Array of path parts
|
|
55
|
+
*/
|
|
56
|
+
export async function parsePath(path, error) {
|
|
57
|
+
if (path == null || path === "") {
|
|
58
|
+
error("[INVALID_PATH] Invalid path");
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
if (typeof path !== "string") {
|
|
62
|
+
error("[INVALID_PATH] Path must be a string");
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const parts = path.split("/").filter(Boolean);
|
|
66
|
+
const forbidden = new Set(["prototype", "__proto__", "constructor"]); //protecting against prototype pollution
|
|
67
|
+
if (parts.some((part) => forbidden.has(part)))
|
|
68
|
+
error("[FORBIDDEN_KEY] Path contains forbidden key");
|
|
69
|
+
if (parts.some((part) => part.includes(".")))
|
|
70
|
+
error("[INVALID_PATH] Path parts cannot contain dots");
|
|
71
|
+
if (parts.length === 0) error("[INVALID_PATH] Path is empty (just /)");
|
|
72
|
+
return parts;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Reads the database file
|
|
77
|
+
* @param {string} path - Database file path
|
|
78
|
+
* @param {function} error - Error callback
|
|
79
|
+
* @param {boolean} encryption - Is encryption enabled
|
|
80
|
+
* @param {string} [secret] - Encryption secret
|
|
81
|
+
* @returns {Promise<Record<string, any>>} - Parsed database data
|
|
82
|
+
*/
|
|
83
|
+
export async function read(path, error, encryption, secret) {
|
|
84
|
+
const raw = await readFile(path, "utf8");
|
|
85
|
+
let data;
|
|
86
|
+
try {
|
|
87
|
+
data = encryption ? decrypt(raw, secret || "") : JSON.parse(raw);
|
|
88
|
+
} catch {
|
|
89
|
+
error(
|
|
90
|
+
"[INVALID_DATA] Database file contains invalid JSON or invalid encryption",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (data == null || typeof data !== "object" || Array.isArray(data)) {
|
|
95
|
+
error("[INVALID_DATA] Database root must be an object");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
//validate dangerous keys
|
|
99
|
+
const forbidden = new Set(["prototype", "__proto__", "constructor"]);
|
|
100
|
+
/** @param {Record<string, any>} obj */
|
|
101
|
+
const hasForbiddenKey = (obj) => {
|
|
102
|
+
for (const key of Object.keys(obj)) {
|
|
103
|
+
if (forbidden.has(key)) {
|
|
104
|
+
error("[FORBIDDEN_KEY] Database contains forbidden key: " + key);
|
|
105
|
+
}
|
|
106
|
+
if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
107
|
+
hasForbiddenKey(obj[key]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
hasForbiddenKey(data);
|
|
113
|
+
return data;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Logs an operation
|
|
118
|
+
* @param {string} operation - Operation to log
|
|
119
|
+
* @param {Record<string, any>|undefined} details - Details of the operation
|
|
120
|
+
* @param {string|undefined} logsPath - Path to log file
|
|
121
|
+
* @param {function} emit - Emit event
|
|
122
|
+
* @param {boolean} logging - Enable logging
|
|
123
|
+
* @returns {Promise<string>} - Timestamp
|
|
124
|
+
*/
|
|
125
|
+
export async function log(operation, details, logsPath, emit, logging) {
|
|
126
|
+
let timestamp = new Date().toISOString();
|
|
127
|
+
if (!logging) {
|
|
128
|
+
await emit(operation, details ? details : undefined, timestamp);
|
|
129
|
+
return timestamp;
|
|
130
|
+
}
|
|
131
|
+
if (!logsPath) {
|
|
132
|
+
await emit(operation, details ? details : undefined, timestamp);
|
|
133
|
+
return timestamp;
|
|
134
|
+
}
|
|
135
|
+
if (!details)
|
|
136
|
+
await appendFile(logsPath, `[${timestamp}] ${operation}\n`, "utf8");
|
|
137
|
+
if (operation) {
|
|
138
|
+
switch (operation) {
|
|
139
|
+
case "get":
|
|
140
|
+
await appendFile(
|
|
141
|
+
logsPath,
|
|
142
|
+
`[${timestamp}] ${operation} - Path: ${details?.path || "/"}\n`,
|
|
143
|
+
"utf8",
|
|
144
|
+
);
|
|
145
|
+
break;
|
|
146
|
+
case "remove":
|
|
147
|
+
await appendFile(
|
|
148
|
+
logsPath,
|
|
149
|
+
`[${timestamp}] ${operation} - Path: ${details?.path || "/"}\n`,
|
|
150
|
+
"utf8",
|
|
151
|
+
);
|
|
152
|
+
break;
|
|
153
|
+
case "set":
|
|
154
|
+
await appendFile(
|
|
155
|
+
logsPath,
|
|
156
|
+
`[${timestamp}] ${operation} - Path: ${details?.path || "/"}\n`,
|
|
157
|
+
"utf8",
|
|
158
|
+
);
|
|
159
|
+
break;
|
|
160
|
+
case "clear":
|
|
161
|
+
await appendFile(
|
|
162
|
+
logsPath,
|
|
163
|
+
`[${timestamp}] ${operation} - Cleared all data\n`,
|
|
164
|
+
"utf8",
|
|
165
|
+
);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
emit(operation, details ? details : undefined, timestamp);
|
|
170
|
+
return timestamp;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Gets a target value from the data by path
|
|
175
|
+
* @param {Record<string, any>} data - Data to search in
|
|
176
|
+
* @param {string} path - Path to get target from
|
|
177
|
+
* @param {function} error - Error callback
|
|
178
|
+
* @returns {Promise<any>} - Found value or undefined
|
|
179
|
+
*/
|
|
180
|
+
export async function getTargetFromData(data, path, error) {
|
|
181
|
+
const parts = await parsePath(path, error);
|
|
182
|
+
let current = data;
|
|
183
|
+
|
|
184
|
+
for (const part of parts) {
|
|
185
|
+
if (
|
|
186
|
+
current == null ||
|
|
187
|
+
typeof current !== "object" ||
|
|
188
|
+
!Object.hasOwn(current, part)
|
|
189
|
+
) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
current = current[part];
|
|
193
|
+
}
|
|
194
|
+
return current;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Writes data to the database file
|
|
199
|
+
* @param {string} path - Database file path
|
|
200
|
+
* @param {Record<string, any>} data - Data to write
|
|
201
|
+
* @param {function} error - Error callback
|
|
202
|
+
* @param {function} emit - Emit event
|
|
203
|
+
* @param {boolean} encryption - Is encryption enabled
|
|
204
|
+
* @param {string|undefined} secret - Encryption secret
|
|
205
|
+
*/
|
|
206
|
+
export async function write(path, data, error, emit, encryption, secret) {
|
|
207
|
+
let content = "";
|
|
208
|
+
try {
|
|
209
|
+
content = encryption
|
|
210
|
+
? encrypt(data, secret || "")
|
|
211
|
+
: await stableStringify(data);
|
|
212
|
+
} catch {
|
|
213
|
+
await error("[INVALID_DATA] Data must be JSON serializable or encryptable");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await writeFile(path, content, "utf8");
|
|
217
|
+
emit("_write", data, new Date().toISOString());
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function randomId5() {
|
|
221
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
222
|
+
let id = "";
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < 5; i++) {
|
|
225
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return id;
|
|
229
229
|
}
|