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,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
@@ -1,5 +0,0 @@
1
- /*module.exports*/const data = require('./src/index.js');
2
- let db = new data.Server({
3
- login: 'frozen_test',
4
- password: 'PHSBP3012'
5
- });
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
- }