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.
- package/CHANGELOG.md +34 -0
- package/LICENSE +7 -0
- package/README.md +224 -31
- package/bin/snowbase.js +291 -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
|
@@ -0,0 +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;
|
|
229
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
writeFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
copyFileSync,
|
|
6
|
+
statSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
} from "fs";
|
|
10
|
+
import { read, write, randomId5 } from "../helpers/utils.js";
|
|
11
|
+
import { encrypt, decrypt } from "../helpers/crypto.js";
|
|
12
|
+
import { resolve, join } from "path";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @fileoverview FileStorage is a storage class that stores data in a file.
|
|
16
|
+
* @class FileStorage
|
|
17
|
+
*/
|
|
18
|
+
export default class FileStorage {
|
|
19
|
+
/**
|
|
20
|
+
* @description Creates a new instance of FileStorage.
|
|
21
|
+
* @param {Object} options - Options for the FileStorage.
|
|
22
|
+
* @param {string} options.baseDir - The base directory for the database.
|
|
23
|
+
* @param {boolean} [options.encryption] - Whether to encrypt the data.
|
|
24
|
+
* @param {string} [options.secret] - The secret key for encryption.
|
|
25
|
+
* @param {boolean} [options.backup] - Whether to backup the data.
|
|
26
|
+
* @param {boolean} [options.logging] - Whether to log the data.
|
|
27
|
+
* @param {function(any): never} options.error - Function to handle errors.
|
|
28
|
+
* @param {function(...any): void} options.emit - Function to emit events.
|
|
29
|
+
* @param {function(string, any): void} options.log - Function to log the data.
|
|
30
|
+
* @memberof FileStorage
|
|
31
|
+
* @constructor
|
|
32
|
+
*/
|
|
33
|
+
constructor({
|
|
34
|
+
baseDir,
|
|
35
|
+
encryption = false,
|
|
36
|
+
secret = "",
|
|
37
|
+
backup = false,
|
|
38
|
+
logging = false,
|
|
39
|
+
error,
|
|
40
|
+
emit,
|
|
41
|
+
log,
|
|
42
|
+
}) {
|
|
43
|
+
this.baseDir = resolve(baseDir) || resolve("./snowbase");
|
|
44
|
+
this.encryption = encryption;
|
|
45
|
+
this.secret = secret;
|
|
46
|
+
this.backup = backup;
|
|
47
|
+
this.logging = logging;
|
|
48
|
+
this._error = error;
|
|
49
|
+
this.emit = emit;
|
|
50
|
+
this._log = log;
|
|
51
|
+
this.path = join(this.baseDir, "snowbase.json");
|
|
52
|
+
|
|
53
|
+
if (this.logging) {
|
|
54
|
+
this.logsDir = join(this.baseDir, "logs");
|
|
55
|
+
}
|
|
56
|
+
if (this.backup) {
|
|
57
|
+
this.backupDir = join(this.baseDir, "backups");
|
|
58
|
+
}
|
|
59
|
+
this.path = join(this.baseDir, "snowbase.json");
|
|
60
|
+
if (this.logsDir) this.logsPath = join(this.logsDir, "snowbase.log");
|
|
61
|
+
|
|
62
|
+
//Creating directories if they don't exist
|
|
63
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
64
|
+
if (this.backup && this.backupDir)
|
|
65
|
+
mkdirSync(this.backupDir, { recursive: true }); //only if the user wants backup
|
|
66
|
+
if (this.logging && this.logsDir)
|
|
67
|
+
mkdirSync(this.logsDir, { recursive: true }); //only if the user wants logging
|
|
68
|
+
|
|
69
|
+
//Creating the database file if it doesn't exist
|
|
70
|
+
if (!existsSync(this.path)) {
|
|
71
|
+
const initialData = this.encryption
|
|
72
|
+
? encrypt({}, this.secret)
|
|
73
|
+
: JSON.stringify({});
|
|
74
|
+
writeFileSync(this.path, initialData, "utf8");
|
|
75
|
+
/** @type {Record<string, any>|null} */
|
|
76
|
+
this._cache = {};
|
|
77
|
+
/** @type {import("node:fs").Stats} */
|
|
78
|
+
const stat = statSync(this.path);
|
|
79
|
+
/** @type {number} Last known modification time of the database file */
|
|
80
|
+
this._cacheMtime = stat.mtimeMs;
|
|
81
|
+
/** @type {number} Last known size of the database file */
|
|
82
|
+
this._cacheSize = stat.size;
|
|
83
|
+
} else {
|
|
84
|
+
/** @type {Record<string, any>|null} */
|
|
85
|
+
this._cache = null;
|
|
86
|
+
/** @type {number} */
|
|
87
|
+
this._cacheMtime = 0;
|
|
88
|
+
}
|
|
89
|
+
if (this.logging && this.logsPath && !existsSync(this.logsPath))
|
|
90
|
+
writeFileSync(this.logsPath, "", "utf8");
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* @returns {Record<string, any>} The data from the database
|
|
94
|
+
*/
|
|
95
|
+
read() {
|
|
96
|
+
// Validate cache freshness by comparing file mtime
|
|
97
|
+
const stat = statSync(this.path);
|
|
98
|
+
const currentMtime = stat.mtimeMs;
|
|
99
|
+
if (
|
|
100
|
+
this._cache !== null &&
|
|
101
|
+
currentMtime === this._cacheMtime &&
|
|
102
|
+
stat.size === this._cacheSize
|
|
103
|
+
) {
|
|
104
|
+
return this._cache;
|
|
105
|
+
}
|
|
106
|
+
const data = read(
|
|
107
|
+
this.path,
|
|
108
|
+
this._error.bind(this),
|
|
109
|
+
this.encryption,
|
|
110
|
+
this.secret || "",
|
|
111
|
+
);
|
|
112
|
+
if (data === null || Array.isArray(data) || typeof data !== "object") {
|
|
113
|
+
this._error("[INVALID_DATABASE] The database root must be an object.");
|
|
114
|
+
}
|
|
115
|
+
this._cache = data;
|
|
116
|
+
this._cacheMtime = currentMtime;
|
|
117
|
+
this._cacheSize = stat.size;
|
|
118
|
+
return data;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {Object} data - Data to write
|
|
123
|
+
* @returns {void}
|
|
124
|
+
*/
|
|
125
|
+
write(data) {
|
|
126
|
+
write(
|
|
127
|
+
this.path,
|
|
128
|
+
data,
|
|
129
|
+
this._error.bind(this),
|
|
130
|
+
this.emit.bind(this),
|
|
131
|
+
this.encryption,
|
|
132
|
+
this.secret,
|
|
133
|
+
);
|
|
134
|
+
this.createBackup();
|
|
135
|
+
this._cache = data;
|
|
136
|
+
this._cacheMtime = statSync(this.path).mtimeMs;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
createBackup() {
|
|
140
|
+
if (!this.backup || !this.backupDir) return false;
|
|
141
|
+
let maxBackups = 20;
|
|
142
|
+
const backupDir = this.backupDir || join(this.baseDir, "backups");
|
|
143
|
+
mkdirSync(backupDir, { recursive: true });
|
|
144
|
+
//Getting all backup files
|
|
145
|
+
const files = readdirSync(backupDir)
|
|
146
|
+
.filter((f) => f.startsWith("backup_"))
|
|
147
|
+
.map((file) => {
|
|
148
|
+
const fullPath = join(backupDir, file);
|
|
149
|
+
const stats = statSync(fullPath);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
path: fullPath,
|
|
153
|
+
time: stats.mtimeMs,
|
|
154
|
+
};
|
|
155
|
+
})
|
|
156
|
+
.sort((a, b) => a.time - b.time);
|
|
157
|
+
|
|
158
|
+
if (files.length - maxBackups > 0) {
|
|
159
|
+
for (let i = 0; i < files.length - maxBackups; i++) {
|
|
160
|
+
unlinkSync(files[i].path);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//Creates a new backup
|
|
164
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
165
|
+
const backupFile = join(backupDir, `backup_${stamp}-${randomId5()}.json`);
|
|
166
|
+
copyFileSync(this.path, backupFile);
|
|
167
|
+
this._log("create backup", { backupFile });
|
|
168
|
+
return backupFile;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { writeFileSync, existsSync, mkdirSync, statSync } from "fs";
|
|
2
|
+
import { stat, mkdir, readdir, unlink, copyFile } from "node:fs/promises";
|
|
3
|
+
import { read, write, randomId5 } from "../helpers/utilsAsync.js";
|
|
4
|
+
import { encrypt, decrypt } from "../helpers/crypto.js";
|
|
5
|
+
import { resolve, join } from "path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview FileStorage is a storage class that stores data in a file.
|
|
9
|
+
* @class FileStorage
|
|
10
|
+
*/
|
|
11
|
+
export default class FileStorageAsync {
|
|
12
|
+
/**
|
|
13
|
+
* @description Creates a new instance of FileStorage.
|
|
14
|
+
* @param {Object} options - Options for the FileStorage.
|
|
15
|
+
* @param {string} options.baseDir - The base directory for the database.
|
|
16
|
+
* @param {boolean} [options.encryption] - Whether to encrypt the data.
|
|
17
|
+
* @param {string} [options.secret] - The secret key for encryption.
|
|
18
|
+
* @param {boolean} [options.backup] - Whether to backup the data.
|
|
19
|
+
* @param {boolean} [options.logging] - Whether to log the data.
|
|
20
|
+
* @param {*} options.error - Function to handle errors.
|
|
21
|
+
* @param {function(...any): void} options.emit - Function to emit events.
|
|
22
|
+
* @param {function(string, any): void} options.log - Function to log the data.
|
|
23
|
+
* @memberof FileStorage
|
|
24
|
+
* @constructor
|
|
25
|
+
*/
|
|
26
|
+
constructor({
|
|
27
|
+
baseDir,
|
|
28
|
+
encryption = false,
|
|
29
|
+
secret = "",
|
|
30
|
+
backup = false,
|
|
31
|
+
logging = false,
|
|
32
|
+
error,
|
|
33
|
+
emit,
|
|
34
|
+
log,
|
|
35
|
+
}) {
|
|
36
|
+
this.baseDir = resolve(baseDir) || resolve("./snowbase");
|
|
37
|
+
this.encryption = encryption;
|
|
38
|
+
this.secret = secret;
|
|
39
|
+
this.backup = backup;
|
|
40
|
+
this.logging = logging;
|
|
41
|
+
this._error = error;
|
|
42
|
+
this.emit = emit;
|
|
43
|
+
this._log = log;
|
|
44
|
+
this.path = join(this.baseDir, "snowbase.json");
|
|
45
|
+
|
|
46
|
+
if (this.logging) {
|
|
47
|
+
this.logsDir = join(this.baseDir, "logs");
|
|
48
|
+
}
|
|
49
|
+
if (this.backup) {
|
|
50
|
+
this.backupDir = join(this.baseDir, "backups");
|
|
51
|
+
}
|
|
52
|
+
this.path = join(this.baseDir, "snowbase.json");
|
|
53
|
+
if (this.logsDir) this.logsPath = join(this.logsDir, "snowbase.log");
|
|
54
|
+
|
|
55
|
+
//Creating directories if they don't exist
|
|
56
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
57
|
+
if (this.backup && this.backupDir)
|
|
58
|
+
mkdirSync(this.backupDir, { recursive: true }); //only if the user wants backup
|
|
59
|
+
if (this.logging && this.logsDir)
|
|
60
|
+
mkdirSync(this.logsDir, { recursive: true }); //only if the user wants logging
|
|
61
|
+
|
|
62
|
+
//Creating the database file if it doesn't exist
|
|
63
|
+
if (!existsSync(this.path)) {
|
|
64
|
+
const initialData = this.encryption
|
|
65
|
+
? encrypt({}, this.secret)
|
|
66
|
+
: JSON.stringify({});
|
|
67
|
+
writeFileSync(this.path, initialData, "utf8");
|
|
68
|
+
/** @type {Record<string, any>|null} */
|
|
69
|
+
this._cache = {};
|
|
70
|
+
/** @type {import("node:fs").Stats} */
|
|
71
|
+
const stat = statSync(this.path);
|
|
72
|
+
/** @type {number} Last known modification time of the database file */
|
|
73
|
+
this._cacheMtime = stat.mtimeMs;
|
|
74
|
+
/** @type {number} Last known size of the database file */
|
|
75
|
+
this._cacheSize = stat.size;
|
|
76
|
+
} else {
|
|
77
|
+
/** @type {Record<string, any>|null} */
|
|
78
|
+
this._cache = null;
|
|
79
|
+
/** @type {number} */
|
|
80
|
+
this._cacheMtime = 0;
|
|
81
|
+
}
|
|
82
|
+
if (this.logging && this.logsPath && !existsSync(this.logsPath))
|
|
83
|
+
writeFileSync(this.logsPath, "", "utf8");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* @returns {Promise<Record<string, any>>} The data from the database
|
|
87
|
+
*/
|
|
88
|
+
async readAsync() {
|
|
89
|
+
// Validate cache freshness by comparing file mtime
|
|
90
|
+
const fileStat = await stat(this.path);
|
|
91
|
+
const currentMtime = fileStat.mtimeMs;
|
|
92
|
+
if (
|
|
93
|
+
this._cache !== null &&
|
|
94
|
+
currentMtime === this._cacheMtime &&
|
|
95
|
+
fileStat.size === this._cacheSize
|
|
96
|
+
) {
|
|
97
|
+
return this._cache;
|
|
98
|
+
}
|
|
99
|
+
const data = await read(
|
|
100
|
+
this.path,
|
|
101
|
+
this._error.bind(this),
|
|
102
|
+
this.encryption,
|
|
103
|
+
this.secret || "",
|
|
104
|
+
);
|
|
105
|
+
if (data === null || Array.isArray(data) || typeof data !== "object") {
|
|
106
|
+
this._error("[INVALID_DATABASE] The database root must be an object.");
|
|
107
|
+
}
|
|
108
|
+
this._cache = data;
|
|
109
|
+
this._cacheMtime = currentMtime;
|
|
110
|
+
this._cacheSize = fileStat.size;
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {Object} data - Data to write
|
|
116
|
+
* @returns {Promise<void>}
|
|
117
|
+
*/
|
|
118
|
+
async writeAsync(data) {
|
|
119
|
+
await write(
|
|
120
|
+
this.path,
|
|
121
|
+
data,
|
|
122
|
+
this._error.bind(this),
|
|
123
|
+
this.emit.bind(this),
|
|
124
|
+
this.encryption,
|
|
125
|
+
this.secret,
|
|
126
|
+
);
|
|
127
|
+
await this.createBackupAsync();
|
|
128
|
+
this._cache = data;
|
|
129
|
+
this._cacheMtime = (await stat(this.path)).mtimeMs;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async createBackupAsync() {
|
|
133
|
+
if (!this.backup || !this.backupDir) return;
|
|
134
|
+
const maxBackups = 20;
|
|
135
|
+
const backupDir = this.backupDir || join(this.baseDir, "backups");
|
|
136
|
+
await mkdir(backupDir, { recursive: true });
|
|
137
|
+
//Getting all backup files
|
|
138
|
+
const files = (
|
|
139
|
+
await Promise.all(
|
|
140
|
+
(await readdir(backupDir))
|
|
141
|
+
.filter((f) => f.startsWith("backup_"))
|
|
142
|
+
.map(async (file) => {
|
|
143
|
+
const fullPath = join(backupDir, file);
|
|
144
|
+
const stats = await stat(fullPath);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
path: fullPath,
|
|
148
|
+
time: stats.mtimeMs,
|
|
149
|
+
};
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
).sort((a, b) => a.time - b.time);
|
|
153
|
+
|
|
154
|
+
if (files.length - maxBackups > 0) {
|
|
155
|
+
for (let i = 0; i < files.length - maxBackups; i++) {
|
|
156
|
+
await unlink(files[i].path);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//Creates a new backup
|
|
160
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
161
|
+
const backupFile = join(backupDir, `backup_${stamp}-${randomId5()}.json`);
|
|
162
|
+
|
|
163
|
+
await copyFile(this.path, backupFile);
|
|
164
|
+
this._log("create backup", { backupFile });
|
|
165
|
+
return backupFile;
|
|
166
|
+
}
|
|
167
|
+
}
|
package/.upm/store.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":2,"languages":{"nodejs-npm":{"specfileHash":"5d5decf1c69e3a2b4980b0627502cc86","lockfileHash":"603978d5629b271eba01c863d286f4e7","guessedImports":["node-fetch"],"guessedImportsHash":"56c1dadb55ef9ad867462f97e0673488"}}}
|
package/index.js
DELETED
package/src/Local.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
const {readFileSync, writeFileSync} = require('fs')
|
|
2
|
-
|
|
3
|
-
module.exports = class LocalDatabase {
|
|
4
|
-
constructor(path) {
|
|
5
|
-
if (!path) throw new Error('Invalid path')
|
|
6
|
-
this.path = path;
|
|
7
|
-
};
|
|
8
|
-
get(key) {
|
|
9
|
-
if (!key) throw new Error('Invalid key to get');
|
|
10
|
-
let to;
|
|
11
|
-
const fileToRead = readFileSync(this.path, 'utf8');
|
|
12
|
-
let data = JSON.parse(fileToRead);
|
|
13
|
-
return data[key];
|
|
14
|
-
};
|
|
15
|
-
remove(key) {
|
|
16
|
-
if (!key) throw new Error('Invalid key to delete');
|
|
17
|
-
const fileToRead = readFileSync(this.path, 'utf8');
|
|
18
|
-
let data = JSON.parse(fileToRead);
|
|
19
|
-
delete data[key];
|
|
20
|
-
data = JSON.stringify(data);
|
|
21
|
-
writeFileSync(this.path, data);
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
save(key, value) {
|
|
25
|
-
if (!value) throw new Error('Missing Params');
|
|
26
|
-
const fileToRead = readFileSync(this.path, 'utf8');
|
|
27
|
-
let data = JSON.parse(fileToRead);
|
|
28
|
-
data[key] = value;
|
|
29
|
-
data = JSON.stringify(data);
|
|
30
|
-
writeFileSync(this.path, data)
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
all(filter) {
|
|
34
|
-
const fileToRead = readFileSync(this.path, 'utf8');
|
|
35
|
-
let data = JSON.parse(fileToRead);
|
|
36
|
-
data = Object.entries(data).map(d => {
|
|
37
|
-
return {
|
|
38
|
-
key: d[0],
|
|
39
|
-
value: d[1]
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
if (filter) return data.filter(filter);
|
|
43
|
-
return data;
|
|
44
|
-
}
|
|
45
|
-
}
|