inibase 1.0.0-rc.11 → 1.0.0-rc.111

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/dist/index.js ADDED
@@ -0,0 +1,1425 @@
1
+ import "dotenv/config";
2
+ import { randomBytes, scryptSync } from "node:crypto";
3
+ import { appendFileSync, existsSync, readFileSync } from "node:fs";
4
+ import { glob, mkdir, readFile, readdir, rename, rm, unlink, writeFile, } from "node:fs/promises";
5
+ import { join, parse } from "node:path";
6
+ import { inspect } from "node:util";
7
+ import Inison from "inison";
8
+ import * as File from "./file.js";
9
+ import * as Utils from "./utils.js";
10
+ import * as UtilsServer from "./utils.server.js";
11
+ // hide ExperimentalWarning glob()
12
+ process.removeAllListeners("warning");
13
+ export default class Inibase {
14
+ pageInfo;
15
+ salt;
16
+ databasePath;
17
+ tables;
18
+ fileExtension = ".txt";
19
+ checkIFunique;
20
+ totalItems;
21
+ constructor(database, mainFolder = ".") {
22
+ this.databasePath = join(mainFolder, database);
23
+ this.tables = {};
24
+ this.totalItems = {};
25
+ this.pageInfo = {};
26
+ this.checkIFunique = {};
27
+ if (!process.env.INIBASE_SECRET) {
28
+ if (existsSync(".env") &&
29
+ readFileSync(".env").includes("INIBASE_SECRET="))
30
+ throw this.Error("NO_ENV");
31
+ this.salt = scryptSync(randomBytes(16), randomBytes(16), 32);
32
+ appendFileSync(".env", `\nINIBASE_SECRET=${this.salt.toString("hex")}\n`);
33
+ }
34
+ else
35
+ this.salt = Buffer.from(process.env.INIBASE_SECRET, "hex");
36
+ }
37
+ Error(code, variable, language = "en") {
38
+ const errorMessages = {
39
+ en: {
40
+ TABLE_EMPTY: "Table {variable} is empty",
41
+ TABLE_EXISTS: "Table {variable} already exists",
42
+ TABLE_NOT_EXISTS: "Table {variable} doesn't exist",
43
+ NO_SCHEMA: "Table {variable} does't have a schema",
44
+ FIELD_UNIQUE: "Field {variable} should be unique, got {variable} instead",
45
+ FIELD_REQUIRED: "Field {variable} is required",
46
+ INVALID_ID: "The given ID(s) is/are not valid(s)",
47
+ INVALID_TYPE: "Expect {variable} to be {variable}, got {variable} instead",
48
+ INVALID_PARAMETERS: "The given parameters are not valid",
49
+ NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
50
+ ? "please run with '--env-file=.env'"
51
+ : "please use dotenv",
52
+ },
53
+ // Add more languages and error messages as needed
54
+ };
55
+ const errorMessage = errorMessages[language][code];
56
+ if (!errorMessage)
57
+ return new Error("ERR");
58
+ return new Error(variable
59
+ ? Array.isArray(variable)
60
+ ? errorMessage.replace(/\{variable\}/g, () => variable.shift()?.toString() ?? "")
61
+ : errorMessage.replaceAll("{variable}", `'${variable.toString()}'`)
62
+ : errorMessage.replaceAll("{variable}", ""));
63
+ }
64
+ getFileExtension = (tableName) => {
65
+ let mainExtension = this.fileExtension;
66
+ // TODO: ADD ENCRYPTION
67
+ // if(this.tables[tableName].config.encryption)
68
+ // mainExtension += ".enc"
69
+ if (this.tables[tableName].config.compression)
70
+ mainExtension += ".gz";
71
+ return mainExtension;
72
+ };
73
+ _schemaToIdsPath = (tableName, schema, prefix = "") => {
74
+ const RETURN = {};
75
+ for (const field of schema)
76
+ if ((field.type === "array" || field.type === "object") &&
77
+ field.children &&
78
+ Utils.isArrayOfObjects(field.children)) {
79
+ Utils.deepMerge(RETURN, this._schemaToIdsPath(tableName, field.children, `${(prefix ?? "") + field.key}.`));
80
+ }
81
+ else if (field.id)
82
+ RETURN[field.id] = `${(prefix ?? "") + field.key}${this.getFileExtension(tableName)}`;
83
+ return RETURN;
84
+ };
85
+ /**
86
+ * Create a new table inside database, with predefined schema and config
87
+ *
88
+ * @param {string} tableName
89
+ * @param {Schema} [schema]
90
+ * @param {Config} [config]
91
+ */
92
+ async createTable(tableName, schema, config) {
93
+ const tablePath = join(this.databasePath, tableName);
94
+ if (await File.isExists(tablePath))
95
+ throw this.Error("TABLE_EXISTS", tableName);
96
+ await mkdir(join(tablePath, ".tmp"), { recursive: true });
97
+ await mkdir(join(tablePath, ".cache"));
98
+ // if config not set => load default global env config
99
+ if (!config)
100
+ config = {
101
+ compression: process.env.INIBASE_COMPRESSION == "true",
102
+ cache: process.env.INIBASE_CACHE === "true",
103
+ prepend: process.env.INIBASE_PREPEND === "true",
104
+ };
105
+ if (config) {
106
+ if (config.compression)
107
+ await writeFile(join(tablePath, ".compression.config"), "");
108
+ if (config.cache)
109
+ await writeFile(join(tablePath, ".cache.config"), "");
110
+ if (config.prepend)
111
+ await writeFile(join(tablePath, ".prepend.config"), "");
112
+ }
113
+ if (schema) {
114
+ const lastSchemaId = 0;
115
+ await writeFile(join(tablePath, "schema.json"), JSON.stringify(UtilsServer.addIdToSchema(schema, lastSchemaId, this.salt), null, 2));
116
+ await writeFile(join(tablePath, `${lastSchemaId}.schema`), "");
117
+ }
118
+ else
119
+ await writeFile(join(tablePath, "0.schema"), "");
120
+ await writeFile(join(tablePath, "0-0.pagination"), "");
121
+ }
122
+ // Function to replace the string in one schema.json file
123
+ async replaceStringInFile(filePath, targetString, replaceString) {
124
+ const data = await readFile(filePath, "utf8");
125
+ if (data.includes(targetString)) {
126
+ const updatedContent = data.replaceAll(targetString, replaceString);
127
+ await writeFile(filePath, updatedContent, "utf8");
128
+ }
129
+ }
130
+ /**
131
+ * Update table schema or config
132
+ *
133
+ * @param {string} tableName
134
+ * @param {Schema} [schema]
135
+ * @param {(Config&{name?: string})} [config]
136
+ */
137
+ async updateTable(tableName, schema, config) {
138
+ const table = await this.getTable(tableName), tablePath = join(this.databasePath, tableName);
139
+ if (schema) {
140
+ // remove id from schema
141
+ schema = schema.filter(({ key }) => !["id", "createdAt", "updatedAt"].includes(key));
142
+ let schemaIdFilePath;
143
+ for await (const filePath of glob("*.schema", { cwd: this.databasePath }))
144
+ schemaIdFilePath = filePath;
145
+ const lastSchemaId = schemaIdFilePath
146
+ ? Number(parse(schemaIdFilePath).name)
147
+ : 0;
148
+ if (await File.isExists(join(tablePath, "schema.json"))) {
149
+ // update columns files names based on field id
150
+ schema = UtilsServer.addIdToSchema(schema, lastSchemaId, this.salt);
151
+ if (table.schema?.length) {
152
+ const replaceOldPathes = Utils.findChangedProperties(this._schemaToIdsPath(tableName, table.schema), this._schemaToIdsPath(tableName, schema));
153
+ if (replaceOldPathes)
154
+ await Promise.all(Object.entries(replaceOldPathes).map(async ([oldPath, newPath]) => {
155
+ if (await File.isExists(join(tablePath, oldPath)))
156
+ await rename(join(tablePath, oldPath), join(tablePath, newPath));
157
+ }));
158
+ }
159
+ }
160
+ else
161
+ schema = UtilsServer.addIdToSchema(schema, lastSchemaId, this.salt);
162
+ await writeFile(join(tablePath, "schema.json"), JSON.stringify(schema, null, 2));
163
+ if (schemaIdFilePath)
164
+ await rename(schemaIdFilePath, join(tablePath, `${lastSchemaId}.schema`));
165
+ else
166
+ await writeFile(join(tablePath, `${lastSchemaId}.schema`), "");
167
+ }
168
+ if (config) {
169
+ if (config.compression !== undefined &&
170
+ config.compression !== table.config.compression) {
171
+ await UtilsServer.execFile("find", [
172
+ tableName,
173
+ "-type",
174
+ "f",
175
+ "-name",
176
+ `*${this.fileExtension}${config.compression ? "" : ".gz"}`,
177
+ "-exec",
178
+ config.compression ? "gzip" : "gunzip",
179
+ "-f",
180
+ "{}",
181
+ "+",
182
+ ], { cwd: this.databasePath });
183
+ if (config.compression)
184
+ await writeFile(join(tablePath, ".compression.config"), "");
185
+ else
186
+ await unlink(join(tablePath, ".compression.config"));
187
+ }
188
+ if (config.cache !== undefined && config.cache !== table.config.cache) {
189
+ if (config.cache)
190
+ await writeFile(join(tablePath, ".cache.config"), "");
191
+ else {
192
+ await this.clearCache(tableName);
193
+ await unlink(join(tablePath, ".cache.config"));
194
+ }
195
+ }
196
+ if (config.prepend !== undefined &&
197
+ config.prepend !== table.config.prepend) {
198
+ await UtilsServer.execFile("find", [
199
+ tableName,
200
+ "-type",
201
+ "f",
202
+ "-name",
203
+ `*${this.fileExtension}${config.compression ? ".gz" : ""}`,
204
+ "-exec",
205
+ "sh",
206
+ "-c",
207
+ `for file; do ${config.compression
208
+ ? 'zcat "$file" | tac | gzip > "$file.reversed" && mv "$file.reversed" "$file"'
209
+ : 'tac "$file" > "$file.reversed" && mv "$file.reversed" "$file"'}; done`,
210
+ "_",
211
+ "{}",
212
+ "+",
213
+ ], { cwd: this.databasePath });
214
+ if (config.prepend)
215
+ await writeFile(join(tablePath, ".prepend.config"), "");
216
+ else
217
+ await unlink(join(tablePath, ".prepend.config"));
218
+ }
219
+ if (config.name) {
220
+ await rename(tablePath, join(this.databasePath, config.name));
221
+ // replace table name in other linked tables (relationship)
222
+ for await (const schemaPath of glob("**/schema.json", {
223
+ cwd: this.databasePath,
224
+ }))
225
+ await this.replaceStringInFile(schemaPath, `"table": "${tableName}"`, `"table": "${config.name}"`);
226
+ }
227
+ }
228
+ delete this.tables[tableName];
229
+ }
230
+ /**
231
+ * Get table schema and config
232
+ *
233
+ * @param {string} tableName
234
+ * @return {*} {Promise<TableObject>}
235
+ */
236
+ async getTable(tableName) {
237
+ const tablePath = join(this.databasePath, tableName);
238
+ if (!(await File.isExists(tablePath)))
239
+ throw this.Error("TABLE_NOT_EXISTS", tableName);
240
+ if (!this.tables[tableName])
241
+ this.tables[tableName] = {
242
+ schema: await this.getTableSchema(tableName),
243
+ config: {
244
+ compression: await File.isExists(join(tablePath, ".compression.config")),
245
+ cache: await File.isExists(join(tablePath, ".cache.config")),
246
+ prepend: await File.isExists(join(tablePath, ".prepend.config")),
247
+ },
248
+ };
249
+ return this.tables[tableName];
250
+ }
251
+ async getTableSchema(tableName, encodeIDs = true) {
252
+ const tablePath = join(this.databasePath, tableName);
253
+ if (!(await File.isExists(join(tablePath, "schema.json"))))
254
+ return undefined;
255
+ const schemaFile = await readFile(join(tablePath, "schema.json"), "utf8");
256
+ if (!schemaFile)
257
+ return undefined;
258
+ let schema = JSON.parse(schemaFile);
259
+ schema = [
260
+ {
261
+ id: 0,
262
+ key: "id",
263
+ type: "id",
264
+ required: true,
265
+ },
266
+ ...schema,
267
+ {
268
+ id: -1,
269
+ key: "createdAt",
270
+ type: "date",
271
+ required: true,
272
+ },
273
+ {
274
+ id: -2,
275
+ key: "updatedAt",
276
+ type: "date",
277
+ },
278
+ ];
279
+ if (!encodeIDs)
280
+ return schema;
281
+ return UtilsServer.encodeSchemaID(schema, this.salt);
282
+ }
283
+ async throwErrorIfTableEmpty(tableName) {
284
+ const table = await this.getTable(tableName);
285
+ if (!table.schema)
286
+ throw this.Error("NO_SCHEMA", tableName);
287
+ if (!(await File.isExists(join(this.databasePath, tableName, `id${this.getFileExtension(tableName)}`))))
288
+ throw this.Error("TABLE_EMPTY", tableName);
289
+ return table;
290
+ }
291
+ validateData(data, schema, skipRequiredField = false) {
292
+ if (Utils.isArrayOfObjects(data))
293
+ for (const single_data of data)
294
+ this.validateData(single_data, schema, skipRequiredField);
295
+ else if (Utils.isObject(data)) {
296
+ for (const field of schema) {
297
+ if (!Object.hasOwn(data, field.key) ||
298
+ data[field.key] === null ||
299
+ data[field.key] === undefined ||
300
+ data[field.key] === "") {
301
+ if (field.required && !skipRequiredField)
302
+ throw this.Error("FIELD_REQUIRED", field.key);
303
+ return;
304
+ }
305
+ if (!Utils.validateFieldType(data[field.key], field.type, (field.type === "array" || field.type === "object") &&
306
+ field.children &&
307
+ !Utils.isArrayOfObjects(field.children)
308
+ ? field.children
309
+ : undefined))
310
+ throw this.Error("INVALID_TYPE", [
311
+ field.key,
312
+ Array.isArray(field.type) ? field.type.join(", ") : field.type,
313
+ data[field.key],
314
+ ]);
315
+ if ((field.type === "array" || field.type === "object") &&
316
+ field.children &&
317
+ Utils.isArrayOfObjects(field.children))
318
+ this.validateData(data[field.key], field.children, skipRequiredField);
319
+ else if (field.unique) {
320
+ if (!this.checkIFunique[field.key])
321
+ this.checkIFunique[field.key] = [];
322
+ this.checkIFunique[`${field.key}`].push(data[field.key]);
323
+ }
324
+ }
325
+ }
326
+ }
327
+ cleanObject(obj) {
328
+ const cleanedObject = Object.entries(obj).reduce((acc, [key, value]) => {
329
+ if (value !== undefined && value !== null && value !== "")
330
+ acc[key] = value;
331
+ return acc;
332
+ }, {});
333
+ return Object.keys(cleanedObject).length > 0 ? cleanedObject : null;
334
+ }
335
+ formatField(value, fieldType, fieldChildrenType, _formatOnlyAvailiableKeys) {
336
+ if (Array.isArray(fieldType))
337
+ fieldType = Utils.detectFieldType(value, fieldType) ?? fieldType[0];
338
+ if (!value)
339
+ return null;
340
+ if (Array.isArray(value) && !["array", "json"].includes(fieldType))
341
+ value = value[0];
342
+ switch (fieldType) {
343
+ case "array":
344
+ if (!fieldChildrenType)
345
+ return null;
346
+ if (!Array.isArray(value))
347
+ value = [value];
348
+ if (Utils.isArrayOfObjects(fieldChildrenType))
349
+ return this.formatData(value, fieldChildrenType);
350
+ if (!value.length)
351
+ return null;
352
+ return value.map((_value) => this.formatField(_value, fieldChildrenType));
353
+ case "object":
354
+ if (Utils.isArrayOfObjects(fieldChildrenType))
355
+ return this.formatData(value, fieldChildrenType, _formatOnlyAvailiableKeys);
356
+ break;
357
+ case "table":
358
+ if (Utils.isObject(value)) {
359
+ if (Object.hasOwn(value, "id") &&
360
+ (Utils.isValidID(value.id) ||
361
+ Utils.isNumber(value.id)))
362
+ return Utils.isNumber(value.id)
363
+ ? Number(value.id)
364
+ : UtilsServer.decodeID(value.id, this.salt);
365
+ }
366
+ else if (Utils.isValidID(value) || Utils.isNumber(value))
367
+ return Utils.isNumber(value)
368
+ ? Number(value)
369
+ : UtilsServer.decodeID(value, this.salt);
370
+ break;
371
+ case "password":
372
+ return Utils.isPassword(value)
373
+ ? value
374
+ : UtilsServer.hashPassword(String(value));
375
+ case "number":
376
+ return Utils.isNumber(value) ? Number(value) : null;
377
+ case "id":
378
+ return Utils.isNumber(value)
379
+ ? value
380
+ : UtilsServer.decodeID(value, this.salt);
381
+ case "json": {
382
+ if (typeof value === "string" && Utils.isStringified(value))
383
+ return value;
384
+ if (Utils.isObject(value)) {
385
+ const cleanedObject = this.cleanObject(value);
386
+ if (cleanedObject)
387
+ return Inison.stringify(cleanedObject);
388
+ }
389
+ else
390
+ return Inison.stringify(value);
391
+ return null;
392
+ }
393
+ default:
394
+ return value;
395
+ }
396
+ return null;
397
+ }
398
+ async checkUnique(tableName, schema) {
399
+ const tablePath = join(this.databasePath, tableName);
400
+ for await (const [key, values] of Object.entries(this.checkIFunique)) {
401
+ const field = Utils.getField(key, schema);
402
+ if (!field)
403
+ continue;
404
+ const [searchResult, totalLines] = await File.search(join(tablePath, `${key}${this.getFileExtension(tableName)}`), Array.isArray(values) ? "=" : "[]", values, undefined, field.type, field.children, 1, undefined, false, this.salt);
405
+ if (searchResult && totalLines > 0)
406
+ throw this.Error("FIELD_UNIQUE", [
407
+ field.key,
408
+ Array.isArray(values) ? values.join(", ") : values,
409
+ ]);
410
+ }
411
+ this.checkIFunique = {};
412
+ }
413
+ formatData(data, schema, formatOnlyAvailiableKeys) {
414
+ if (Utils.isArrayOfObjects(data))
415
+ return data.map((single_data) => this.formatData(single_data, schema, formatOnlyAvailiableKeys));
416
+ if (Utils.isObject(data)) {
417
+ for (const field of schema) {
418
+ if (!Object.hasOwn(data, field.key)) {
419
+ if (formatOnlyAvailiableKeys)
420
+ continue;
421
+ data[field.key] = this.getDefaultValue(field);
422
+ continue;
423
+ }
424
+ data[field.key] = this.formatField(data[field.key], field.type, field.children, formatOnlyAvailiableKeys);
425
+ }
426
+ return data;
427
+ }
428
+ return [];
429
+ }
430
+ getDefaultValue(field) {
431
+ if (Array.isArray(field.type))
432
+ return this.getDefaultValue({
433
+ ...field,
434
+ type: field.type.sort((a, b) => Number(b === "array") - Number(a === "array") ||
435
+ Number(a === "string") - Number(b === "string") ||
436
+ Number(a === "number") - Number(b === "number"))[0],
437
+ });
438
+ switch (field.type) {
439
+ case "array":
440
+ case "object": {
441
+ if (!field.children || !Utils.isArrayOfObjects(field.children))
442
+ return null;
443
+ const RETURN = {};
444
+ for (const f of field.children)
445
+ RETURN[f.key] = this.getDefaultValue(f);
446
+ return RETURN;
447
+ }
448
+ case "boolean":
449
+ return false;
450
+ default:
451
+ return null;
452
+ }
453
+ }
454
+ _combineObjectsToArray(input) {
455
+ return input.reduce((result, current) => {
456
+ for (const [key, value] of Object.entries(current))
457
+ if (Object.hasOwn(result, key) && Array.isArray(result[key]))
458
+ result[key].push(value);
459
+ else
460
+ result[key] = [value];
461
+ return result;
462
+ }, {});
463
+ }
464
+ _CombineData(data, prefix) {
465
+ if (Utils.isArrayOfObjects(data))
466
+ return this._combineObjectsToArray(data.map((single_data) => this._CombineData(single_data)));
467
+ const RETURN = {};
468
+ for (const [key, value] of Object.entries(data)) {
469
+ if (Utils.isObject(value))
470
+ Object.assign(RETURN, this._CombineData(value, `${key}.`));
471
+ else if (Utils.isArrayOfObjects(value)) {
472
+ Object.assign(RETURN, this._CombineData(this._combineObjectsToArray(value), `${(prefix ?? "") + key}.`));
473
+ }
474
+ else if (Utils.isArrayOfArrays(value) &&
475
+ value.every(Utils.isArrayOfObjects))
476
+ Object.assign(RETURN, this._CombineData(this._combineObjectsToArray(value.map(this._combineObjectsToArray)), `${(prefix ?? "") + key}.`));
477
+ else
478
+ RETURN[(prefix ?? "") + key] = File.encode(value);
479
+ }
480
+ return RETURN;
481
+ }
482
+ joinPathesContents(tableName, data) {
483
+ const tablePath = join(this.databasePath, tableName), combinedData = this._CombineData(data);
484
+ const newCombinedData = {};
485
+ for (const [key, value] of Object.entries(combinedData))
486
+ newCombinedData[join(tablePath, `${key}${this.getFileExtension(tableName)}`)] = value;
487
+ return newCombinedData;
488
+ }
489
+ _processSchemaDataHelper(RETURN, item, index, field) {
490
+ // If the item is an object, we need to process its children
491
+ if (Utils.isObject(item)) {
492
+ if (!RETURN[index])
493
+ RETURN[index] = {}; // Ensure the index exists
494
+ if (!RETURN[index][field.key])
495
+ RETURN[index][field.key] = [];
496
+ // Process children fields (recursive if needed)
497
+ for (const child_field of field.children.filter((children) => children.type === "array" &&
498
+ Utils.isArrayOfObjects(children.children))) {
499
+ if (Utils.isObject(item[child_field.key])) {
500
+ for (const [key, value] of Object.entries(item[child_field.key])) {
501
+ for (let _i = 0; _i < value.length; _i++) {
502
+ if ((Array.isArray(value[_i]) && Utils.isArrayOfNulls(value[_i])) ||
503
+ value[_i] === null)
504
+ continue;
505
+ if (!RETURN[index][field.key][_i])
506
+ RETURN[index][field.key][_i] = {};
507
+ if (!RETURN[index][field.key][_i][child_field.key])
508
+ RETURN[index][field.key][_i][child_field.key] = [];
509
+ if (!Array.isArray(value[_i])) {
510
+ if (!RETURN[index][field.key][_i][child_field.key][0])
511
+ RETURN[index][field.key][_i][child_field.key][0] = {};
512
+ RETURN[index][field.key][_i][child_field.key][0][key] =
513
+ value[_i];
514
+ }
515
+ else {
516
+ value[_i].forEach((_element, _index) => {
517
+ // Recursive call to handle nested structure
518
+ this._processSchemaDataHelper(RETURN, _element, _index, child_field);
519
+ // Perform property assignments
520
+ if (!RETURN[index][field.key][_i][child_field.key][_index])
521
+ RETURN[index][field.key][_i][child_field.key][_index] = {};
522
+ RETURN[index][field.key][_i][child_field.key][_index][key] =
523
+ _element;
524
+ });
525
+ }
526
+ }
527
+ }
528
+ }
529
+ }
530
+ }
531
+ }
532
+ async processSchemaData(tableName, schema, linesNumber, options, prefix) {
533
+ const RETURN = {};
534
+ for (const field of schema) {
535
+ // If the field is of simple type (non-recursive), process it directly
536
+ if (this.isSimpleField(field.type)) {
537
+ await this.processSimpleField(tableName, field, linesNumber, RETURN, options, prefix);
538
+ }
539
+ else if (this.isArrayField(field.type)) {
540
+ // Process array fields (recursive if needed)
541
+ await this.processArrayField(tableName, field, linesNumber, RETURN, options, prefix);
542
+ }
543
+ else if (this.isObjectField(field.type)) {
544
+ // Process object fields (recursive if needed)
545
+ await this.processObjectField(tableName, field, linesNumber, RETURN, options, prefix);
546
+ }
547
+ else if (this.isTableField(field.type)) {
548
+ // Process table reference fields
549
+ await this.processTableField(tableName, field, linesNumber, RETURN, options, prefix);
550
+ }
551
+ }
552
+ return RETURN;
553
+ }
554
+ // Helper function to determine if a field is simple
555
+ isSimpleField(fieldType) {
556
+ const complexTypes = ["array", "object", "table"];
557
+ if (Array.isArray(fieldType))
558
+ return fieldType.every((type) => typeof type === "string" && !complexTypes.includes(type));
559
+ return !complexTypes.includes(fieldType);
560
+ }
561
+ // Process a simple field (non-recursive)
562
+ async processSimpleField(tableName, field, linesNumber, RETURN, _options, prefix) {
563
+ const fieldPath = join(this.databasePath, tableName, `${prefix ?? ""}${field.key}${this.getFileExtension(tableName)}`);
564
+ if (await File.isExists(fieldPath)) {
565
+ const items = await File.get(fieldPath, linesNumber, field.type, field.children, this.salt);
566
+ if (items) {
567
+ for (const [index, item] of Object.entries(items)) {
568
+ if (typeof item === "undefined")
569
+ continue; // Skip undefined items
570
+ if (!RETURN[index])
571
+ RETURN[index] = {}; // Ensure the index exists
572
+ RETURN[index][field.key] = item; // Assign item to the RETURN object
573
+ }
574
+ }
575
+ }
576
+ }
577
+ // Helper function to check if the field type is array
578
+ isArrayField(fieldType) {
579
+ return ((Array.isArray(fieldType) &&
580
+ fieldType.every((type) => typeof type === "string") &&
581
+ fieldType.includes("array")) ||
582
+ fieldType === "array");
583
+ }
584
+ // Process array fields (recursive if needed)
585
+ async processArrayField(tableName, field, linesNumber, RETURN, options, prefix) {
586
+ if (Array.isArray(field.children)) {
587
+ if (this.isSimpleField(field.children)) {
588
+ await this.processSimpleField(tableName, field, linesNumber, RETURN, options, prefix);
589
+ }
590
+ else if (this.isTableField(field.children)) {
591
+ await this.processTableField(tableName, field, linesNumber, RETURN, options, prefix);
592
+ }
593
+ else {
594
+ // Handling array of objects and filtering nested arrays
595
+ const nestedArrayFields = field.children.filter((child) => child.type === "array" &&
596
+ Utils.isArrayOfObjects(child.children));
597
+ if (nestedArrayFields.length > 0) {
598
+ // one of children has array field type and has children array of object = Schema
599
+ const childItems = await this.processSchemaData(tableName, field.children.filter((children) => children.type === "array" &&
600
+ Utils.isArrayOfObjects(children.children)), linesNumber, options, `${(prefix ?? "") + field.key}.`);
601
+ if (childItems)
602
+ for (const [index, item] of Object.entries(childItems))
603
+ this._processSchemaDataHelper(RETURN, item, index, field);
604
+ // Remove nested arrays after processing
605
+ field.children = field.children.filter((child) => child.type === "array" &&
606
+ Utils.isArrayOfObjects(child.children));
607
+ }
608
+ // Process remaining items for the field's children
609
+ const items = await this.processSchemaData(tableName, field.children, linesNumber, options, `${(prefix ?? "") + field.key}.`);
610
+ // Process the items after retrieval
611
+ if (items) {
612
+ for (const [index, item] of Object.entries(items)) {
613
+ if (typeof item === "undefined")
614
+ continue; // Skip undefined items
615
+ if (!RETURN[index])
616
+ RETURN[index] = {};
617
+ if (Utils.isObject(item)) {
618
+ if (!Utils.isArrayOfNulls(Object.values(item))) {
619
+ if (RETURN[index][field.key])
620
+ Object.entries(item).forEach(([key, value], _index) => {
621
+ for (let _index = 0; _index < value.length; _index++)
622
+ if (RETURN[index][field.key][_index])
623
+ Object.assign(RETURN[index][field.key][_index], {
624
+ [key]: value[_index],
625
+ });
626
+ else
627
+ RETURN[index][field.key][_index] = {
628
+ [key]: value[_index],
629
+ };
630
+ });
631
+ else if (Object.values(item).every((_i) => Utils.isArrayOfArrays(_i) || Array.isArray(_i)) &&
632
+ prefix)
633
+ RETURN[index][field.key] = item;
634
+ else {
635
+ RETURN[index][field.key] = [];
636
+ Object.entries(item).forEach(([key, value], _ind) => {
637
+ if (!Array.isArray(value)) {
638
+ RETURN[index][field.key][_ind] = {};
639
+ RETURN[index][field.key][_ind][key] = value;
640
+ }
641
+ else
642
+ for (let _i = 0; _i < value.length; _i++) {
643
+ if (value[_i] === null ||
644
+ (Array.isArray(value[_i]) &&
645
+ Utils.isArrayOfNulls(value[_i])))
646
+ continue;
647
+ if (!RETURN[index][field.key][_i])
648
+ RETURN[index][field.key][_i] = {};
649
+ RETURN[index][field.key][_i][key] = value[_i];
650
+ }
651
+ });
652
+ }
653
+ }
654
+ else
655
+ RETURN[index][field.key] = null;
656
+ }
657
+ else
658
+ RETURN[index][field.key] = item;
659
+ }
660
+ }
661
+ }
662
+ }
663
+ else if (this.isSimpleField(field.children)) {
664
+ // If `children` is FieldType, handle it as an array of simple types (no recursion needed here)
665
+ await this.processSimpleField(tableName, field, linesNumber, RETURN, options, prefix);
666
+ }
667
+ else if (this.isTableField(field.children)) {
668
+ await this.processTableField(tableName, field, linesNumber, RETURN, options, prefix);
669
+ }
670
+ }
671
+ // Helper function to check if the field type is object
672
+ isObjectField(fieldType) {
673
+ return (fieldType === "object" ||
674
+ (Array.isArray(fieldType) &&
675
+ fieldType.every((type) => typeof type === "string") &&
676
+ fieldType.includes("object")));
677
+ }
678
+ // Process object fields (recursive if needed)
679
+ async processObjectField(tableName, field, linesNumber, RETURN, options, prefix) {
680
+ if (Array.isArray(field.children)) {
681
+ // If `children` is a Schema (array of Field objects), recurse
682
+ const items = await this.processSchemaData(tableName, field.children, linesNumber, options, `${prefix ?? ""}${field.key}.`);
683
+ for (const [index, item] of Object.entries(items)) {
684
+ if (typeof item === "undefined")
685
+ continue; // Skip undefined items
686
+ if (!RETURN[index])
687
+ RETURN[index] = {};
688
+ if (Utils.isObject(item)) {
689
+ if (!Object.values(item).every((i) => i === null))
690
+ RETURN[index][field.key] = item;
691
+ }
692
+ }
693
+ }
694
+ }
695
+ // Helper function to check if the field type is table
696
+ isTableField(fieldType) {
697
+ return (fieldType === "table" ||
698
+ (Array.isArray(fieldType) &&
699
+ fieldType.every((type) => typeof type === "string") &&
700
+ fieldType.includes("table")));
701
+ }
702
+ // Process table reference fields
703
+ async processTableField(tableName, field, linesNumber, RETURN, options, prefix) {
704
+ if (field.table &&
705
+ (await File.isExists(join(this.databasePath, field.table)))) {
706
+ const fieldPath = join(this.databasePath, tableName, `${prefix ?? ""}${field.key}${this.getFileExtension(tableName)}`);
707
+ if (await File.isExists(fieldPath)) {
708
+ const itemsIDs = await File.get(fieldPath, linesNumber, field.type, field.children, this.salt);
709
+ const isArrayField = this.isArrayField(field.type);
710
+ if (itemsIDs) {
711
+ const searchableIDs = new Map();
712
+ for (const [lineNumber, lineContent] of Object.entries(itemsIDs)) {
713
+ if (typeof lineContent === "undefined")
714
+ continue; // Skip undefined items
715
+ if (!RETURN[lineNumber])
716
+ RETURN[lineNumber] = {};
717
+ if (lineContent !== null && lineContent !== undefined)
718
+ searchableIDs.set(lineNumber, lineContent);
719
+ }
720
+ if (searchableIDs.size) {
721
+ const items = await this.get(field.table, isArrayField
722
+ ? Array.from(new Set(Array.from(searchableIDs.values()).flat()))
723
+ : Array.from(new Set(searchableIDs.values())), {
724
+ ...options,
725
+ perPage: Number.POSITIVE_INFINITY,
726
+ columns: options.columns
727
+ ?.filter((column) => column.includes(`${field.key}.`))
728
+ .map((column) => column.replace(`${field.key}.`, "")),
729
+ });
730
+ if (items) {
731
+ for (const [lineNumber, lineContent] of searchableIDs.entries()) {
732
+ const foundedItem = isArrayField
733
+ ? items.filter(({ id }) => lineContent.includes(id))
734
+ : items.find(({ id }) => id === lineContent);
735
+ if (foundedItem)
736
+ RETURN[lineNumber][field.key] = foundedItem;
737
+ }
738
+ }
739
+ }
740
+ }
741
+ }
742
+ }
743
+ }
744
+ async applyCriteria(tableName, schema, options, criteria, allTrue) {
745
+ const tablePath = join(this.databasePath, tableName);
746
+ let RETURN = {}, RETURN_LineNumbers = null;
747
+ if (!criteria)
748
+ return [null, null];
749
+ if (criteria.and && Utils.isObject(criteria.and)) {
750
+ const [searchResult, lineNumbers] = await this.applyCriteria(tableName, schema, options, criteria.and, true);
751
+ if (searchResult) {
752
+ RETURN = Utils.deepMerge(RETURN, Object.fromEntries(Object.entries(searchResult).filter(([_k, v], _i) => Object.keys(v).length ===
753
+ Object.keys(criteria.and ?? {}).length)));
754
+ delete criteria.and;
755
+ RETURN_LineNumbers = lineNumbers;
756
+ }
757
+ else
758
+ return [null, null];
759
+ }
760
+ if (criteria.or && Utils.isObject(criteria.or)) {
761
+ const [searchResult, lineNumbers] = await this.applyCriteria(tableName, schema, options, criteria.or, false);
762
+ delete criteria.or;
763
+ if (searchResult) {
764
+ RETURN = Utils.deepMerge(RETURN, searchResult);
765
+ RETURN_LineNumbers = lineNumbers;
766
+ }
767
+ }
768
+ if (Object.keys(criteria).length > 0) {
769
+ if (allTrue === undefined)
770
+ allTrue = true;
771
+ let index = -1;
772
+ for await (const [key, value] of Object.entries(criteria)) {
773
+ const field = Utils.getField(key, schema);
774
+ index++;
775
+ let searchOperator = undefined, searchComparedAtValue = undefined, searchLogicalOperator = undefined;
776
+ if (Utils.isObject(value)) {
777
+ if (value?.or &&
778
+ Array.isArray(value?.or)) {
779
+ const searchCriteria = (value?.or)
780
+ .map((single_or) => typeof single_or === "string"
781
+ ? Utils.FormatObjectCriteriaValue(single_or)
782
+ : ["=", single_or])
783
+ .filter((a) => a);
784
+ if (searchCriteria.length > 0) {
785
+ searchOperator = searchCriteria.map((single_or) => single_or[0]);
786
+ searchComparedAtValue = searchCriteria.map((single_or) => single_or[1]);
787
+ searchLogicalOperator = "or";
788
+ }
789
+ delete value.or;
790
+ }
791
+ if (value?.and &&
792
+ Array.isArray(value?.and)) {
793
+ const searchCriteria = (value?.and)
794
+ .map((single_and) => typeof single_and === "string"
795
+ ? Utils.FormatObjectCriteriaValue(single_and)
796
+ : ["=", single_and])
797
+ .filter((a) => a);
798
+ if (searchCriteria.length > 0) {
799
+ searchOperator = searchCriteria.map((single_and) => single_and[0]);
800
+ searchComparedAtValue = searchCriteria.map((single_and) => single_and[1]);
801
+ searchLogicalOperator = "and";
802
+ }
803
+ delete value.and;
804
+ }
805
+ }
806
+ else if (Array.isArray(value)) {
807
+ const searchCriteria = value
808
+ .map((single) => typeof single === "string"
809
+ ? Utils.FormatObjectCriteriaValue(single)
810
+ : ["=", single])
811
+ .filter((a) => a);
812
+ if (searchCriteria.length > 0) {
813
+ searchOperator = searchCriteria.map((single) => single[0]);
814
+ searchComparedAtValue = searchCriteria.map((single) => single[1]);
815
+ searchLogicalOperator = "and";
816
+ }
817
+ }
818
+ else if (typeof value === "string") {
819
+ const ComparisonOperatorValue = Utils.FormatObjectCriteriaValue(value);
820
+ if (ComparisonOperatorValue) {
821
+ searchOperator = ComparisonOperatorValue[0];
822
+ searchComparedAtValue = ComparisonOperatorValue[1];
823
+ }
824
+ }
825
+ else {
826
+ searchOperator = "=";
827
+ searchComparedAtValue = value;
828
+ }
829
+ const [searchResult, totalLines, linesNumbers] = await File.search(join(tablePath, `${key}${this.getFileExtension(tableName)}`), searchOperator ?? "=", searchComparedAtValue ?? null, searchLogicalOperator, field?.type, field?.children, options.perPage, (options.page - 1) * options.perPage + 1, true, this.salt);
830
+ if (searchResult) {
831
+ RETURN = Utils.deepMerge(RETURN, Object.fromEntries(Object.entries(searchResult).map(([id, value]) => [
832
+ id,
833
+ {
834
+ [key]: value,
835
+ },
836
+ ])));
837
+ this.totalItems[`${tableName}-${key}`] = totalLines;
838
+ RETURN_LineNumbers = linesNumbers;
839
+ }
840
+ if (allTrue && index > 0) {
841
+ if (!Object.keys(RETURN).length)
842
+ RETURN = {};
843
+ RETURN = Object.fromEntries(Object.entries(RETURN).filter(([_index, item]) => Object.keys(item).length > index));
844
+ if (!Object.keys(RETURN).length)
845
+ RETURN = {};
846
+ }
847
+ }
848
+ }
849
+ return [Object.keys(RETURN).length ? RETURN : null, RETURN_LineNumbers];
850
+ }
851
+ _filterSchemaByColumns(schema, columns) {
852
+ return schema
853
+ .map((field) => {
854
+ if (columns.some((column) => column.startsWith("!")))
855
+ return columns.includes(`!${field.key}`) ? null : field;
856
+ if (columns.includes(field.key) || columns.includes("*"))
857
+ return field;
858
+ if ((field.type === "array" || field.type === "object") &&
859
+ Utils.isArrayOfObjects(field.children) &&
860
+ columns.filter((column) => column.startsWith(`${field.key}.`) ||
861
+ column.startsWith(`!${field.key}.`)).length) {
862
+ field.children = this._filterSchemaByColumns(field.children, columns
863
+ .filter((column) => column.startsWith(`${field.key}.`) ||
864
+ column.startsWith(`!${field.key}.`))
865
+ .map((column) => column.replace(`${field.key}.`, "")));
866
+ return field;
867
+ }
868
+ return null;
869
+ })
870
+ .filter((i) => i);
871
+ }
872
+ /**
873
+ * Clear table cache
874
+ *
875
+ * @param {string} tableName
876
+ */
877
+ async clearCache(tableName) {
878
+ const cacheFolderPath = join(this.databasePath, tableName, ".cache");
879
+ await rm(cacheFolderPath, { recursive: true, force: true });
880
+ await mkdir(cacheFolderPath);
881
+ }
882
+ async get(tableName, where, options = {
883
+ page: 1,
884
+ perPage: 15,
885
+ }, onlyOne, onlyLinesNumbers) {
886
+ const tablePath = join(this.databasePath, tableName);
887
+ // Ensure options.columns is an array
888
+ if (options.columns) {
889
+ options.columns = Array.isArray(options.columns)
890
+ ? options.columns
891
+ : [options.columns];
892
+ if (options.columns.length && !options.columns.includes("id"))
893
+ options.columns.push("id");
894
+ }
895
+ // Default values for page and perPage
896
+ options.page = options.page || 1;
897
+ options.perPage = options.perPage || 15;
898
+ let RETURN;
899
+ let schema = (await this.getTable(tableName)).schema;
900
+ if (!schema)
901
+ throw this.Error("NO_SCHEMA", tableName);
902
+ let pagination;
903
+ for await (const paginationFilePath of glob("*.pagination", {
904
+ cwd: tablePath,
905
+ }))
906
+ pagination = parse(paginationFilePath).name.split("-").map(Number);
907
+ if (!pagination[1])
908
+ return null;
909
+ if (options.columns?.length)
910
+ schema = this._filterSchemaByColumns(schema, options.columns);
911
+ if (where &&
912
+ ((Array.isArray(where) && !where.length) ||
913
+ (Utils.isObject(where) && !Object.keys(where).length)))
914
+ where = undefined;
915
+ if (options.sort) {
916
+ let sortArray, awkCommand = "";
917
+ if (Utils.isObject(options.sort) && !Array.isArray(options.sort)) {
918
+ // {name: "ASC", age: "DESC"}
919
+ sortArray = Object.entries(options.sort).map(([key, value]) => [
920
+ key,
921
+ typeof value === "string" ? value.toLowerCase() === "asc" : value > 0,
922
+ ]);
923
+ }
924
+ else
925
+ sortArray = []
926
+ .concat(options.sort)
927
+ .map((column) => [column, true]);
928
+ let cacheKey = "";
929
+ // Criteria
930
+ if (this.tables[tableName].config.cache)
931
+ cacheKey = UtilsServer.hashString(inspect(sortArray, { sorted: true }));
932
+ if (where) {
933
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
934
+ if (!lineNumbers?.length)
935
+ return null;
936
+ const itemsIDs = Object.values((await File.get(join(tablePath, `id${this.getFileExtension(tableName)}`), lineNumbers, "number", undefined, this.salt)) ?? {}).map(Number);
937
+ awkCommand = `awk '${itemsIDs.map((id) => `$1 == ${id}`).join(" || ")}'`;
938
+ }
939
+ else
940
+ awkCommand = `awk '${Array.from({ length: options.perPage }, (_, index) => (options.page - 1) * options.perPage +
941
+ index +
942
+ 1)
943
+ .map((lineNumber) => `NR==${lineNumber}`)
944
+ .join(" || ")}'`;
945
+ const filesPathes = [["id", true], ...sortArray].map((column) => join(tablePath, `${column[0]}${this.getFileExtension(tableName)}`));
946
+ for await (const path of filesPathes.slice(1))
947
+ if (!(await File.isExists(path)))
948
+ return null;
949
+ // Construct the paste command to merge files and filter lines by IDs
950
+ const pasteCommand = `paste ${filesPathes.join(" ")}`;
951
+ // Construct the sort command dynamically based on the number of files for sorting
952
+ const index = 2;
953
+ const sortColumns = sortArray
954
+ .map(([key, ascending], i) => {
955
+ const field = Utils.getField(key, schema);
956
+ if (field)
957
+ return `-k${i + index},${i + index}${Utils.isFieldType(["id", "number", "date"], field.type, field.children)
958
+ ? "n"
959
+ : ""}${!ascending ? "r" : ""}`;
960
+ return "";
961
+ })
962
+ .join(" ");
963
+ const sortCommand = `sort ${sortColumns} -T=${join(tablePath, ".tmp")}`;
964
+ try {
965
+ if (cacheKey)
966
+ await File.lock(join(tablePath, ".tmp"), cacheKey);
967
+ // Combine && Execute the commands synchronously
968
+ let lines = (await UtilsServer.exec(this.tables[tableName].config.cache
969
+ ? (await File.isExists(join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)))
970
+ ? `${awkCommand} ${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)}`
971
+ : `${pasteCommand} | ${sortCommand} -o ${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)} && ${awkCommand} ${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)}`
972
+ : `${pasteCommand} | ${sortCommand} | ${awkCommand}`, {
973
+ encoding: "utf-8",
974
+ })).stdout
975
+ .trimEnd()
976
+ .split("\n");
977
+ if (where)
978
+ lines = lines.slice((options.page - 1) * options.perPage, options.page * options.perPage);
979
+ else if (!this.totalItems[`${tableName}-*`])
980
+ this.totalItems[`${tableName}-*`] = pagination[1];
981
+ if (!lines.length)
982
+ return null;
983
+ // Parse the result and extract the specified lines
984
+ const outputArray = lines.map((line) => {
985
+ const splitedFileColumns = line.split("\t"); // Assuming tab-separated columns
986
+ const outputObject = {};
987
+ // Extract values for each file, including `id${this.getFileExtension(tableName)}`
988
+ filesPathes.forEach((fileName, index) => {
989
+ const field = Utils.getField(parse(fileName).name, schema);
990
+ if (field)
991
+ outputObject[field.key] = File.decode(splitedFileColumns[index], field?.type, field?.children, this.salt);
992
+ });
993
+ return outputObject;
994
+ });
995
+ const restOfColumns = await this.get(tableName, outputArray.map(({ id }) => id), (({ sort, ...rest }) => rest)(options));
996
+ return restOfColumns
997
+ ? outputArray.map((item) => ({
998
+ ...item,
999
+ ...restOfColumns.find(({ id }) => id === item.id),
1000
+ }))
1001
+ : outputArray;
1002
+ }
1003
+ finally {
1004
+ if (cacheKey)
1005
+ await File.unlock(join(tablePath, ".tmp"), cacheKey);
1006
+ }
1007
+ }
1008
+ if (!where) {
1009
+ // Display all data
1010
+ RETURN = Object.values(await this.processSchemaData(tableName, schema, Array.from({ length: options.perPage }, (_, index) => (options.page - 1) * options.perPage +
1011
+ index +
1012
+ 1), options));
1013
+ if (!this.totalItems[`${tableName}-*`])
1014
+ this.totalItems[`${tableName}-*`] = pagination[1];
1015
+ }
1016
+ else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1017
+ Utils.isNumber(where)) {
1018
+ // "where" in this case, is the line(s) number(s) and not id(s)
1019
+ let lineNumbers = where;
1020
+ if (!Array.isArray(lineNumbers))
1021
+ lineNumbers = [lineNumbers];
1022
+ if (!this.totalItems[`${tableName}-*`])
1023
+ this.totalItems[`${tableName}-*`] = lineNumbers.length;
1024
+ // useless
1025
+ if (onlyLinesNumbers)
1026
+ return lineNumbers;
1027
+ RETURN = Object.values((await this.processSchemaData(tableName, schema, lineNumbers, options)) ?? {});
1028
+ if (RETURN?.length && !Array.isArray(where))
1029
+ RETURN = RETURN[0];
1030
+ }
1031
+ else if ((Array.isArray(where) && where.every(Utils.isValidID)) ||
1032
+ Utils.isValidID(where)) {
1033
+ let Ids = where;
1034
+ if (!Array.isArray(Ids))
1035
+ Ids = [Ids];
1036
+ const [lineNumbers, countItems] = await File.search(join(tablePath, `id${this.getFileExtension(tableName)}`), "[]", Ids.map((id) => Utils.isNumber(id) ? Number(id) : UtilsServer.decodeID(id, this.salt)), undefined, "number", undefined, Ids.length, 0, !this.totalItems[`${tableName}-*`], this.salt);
1037
+ if (!lineNumbers)
1038
+ return null;
1039
+ if (!this.totalItems[`${tableName}-*`])
1040
+ this.totalItems[`${tableName}-*`] = countItems;
1041
+ if (onlyLinesNumbers)
1042
+ return Object.keys(lineNumbers).length
1043
+ ? Object.keys(lineNumbers).map(Number)
1044
+ : null;
1045
+ if (options.columns) {
1046
+ options.columns = options.columns.filter((column) => column !== "id");
1047
+ if (!options.columns?.length)
1048
+ options.columns = undefined;
1049
+ }
1050
+ RETURN = Object.values((await this.processSchemaData(tableName, schema, Object.keys(lineNumbers).map(Number), options)) ?? {});
1051
+ if (RETURN?.length && !Array.isArray(where))
1052
+ RETURN = RETURN[0];
1053
+ }
1054
+ else if (Utils.isObject(where)) {
1055
+ let cachedFilePath = "";
1056
+ // Criteria
1057
+ if (this.tables[tableName].config.cache)
1058
+ cachedFilePath = join(tablePath, ".cache", `${UtilsServer.hashString(inspect(where, { sorted: true }))}${this.fileExtension}`);
1059
+ if (this.tables[tableName].config.cache &&
1060
+ (await File.isExists(cachedFilePath))) {
1061
+ const cachedItems = (await readFile(cachedFilePath, "utf8")).split(",");
1062
+ if (!this.totalItems[`${tableName}-*`])
1063
+ this.totalItems[`${tableName}-*`] = cachedItems.length;
1064
+ if (onlyLinesNumbers)
1065
+ return onlyOne ? Number(cachedItems[0]) : cachedItems.map(Number);
1066
+ return this.get(tableName, cachedItems
1067
+ .slice((options.page - 1) * options.perPage, options.page * options.perPage)
1068
+ .map(Number), options, onlyOne);
1069
+ }
1070
+ let linesNumbers = null;
1071
+ [RETURN, linesNumbers] = await this.applyCriteria(tableName, schema, options, where);
1072
+ if (RETURN && linesNumbers?.size) {
1073
+ if (!this.totalItems[`${tableName}-*`])
1074
+ this.totalItems[`${tableName}-*`] = linesNumbers.size;
1075
+ if (onlyLinesNumbers)
1076
+ return onlyOne
1077
+ ? linesNumbers.values().next().value
1078
+ : Array.from(linesNumbers);
1079
+ const alreadyExistsColumns = Object.keys(Object.values(RETURN)[0]), alreadyExistsColumnsIDs = Utils.flattenSchema(schema)
1080
+ .filter(({ key }) => alreadyExistsColumns.includes(key))
1081
+ .map(({ id }) => id);
1082
+ RETURN = Object.values(Utils.deepMerge(RETURN, await this.processSchemaData(tableName, Utils.filterSchema(schema, ({ id, type, children }) => !alreadyExistsColumnsIDs.includes(id) ||
1083
+ Utils.isFieldType("table", type, children)), Object.keys(RETURN).map(Number), options)));
1084
+ if (this.tables[tableName].config.cache)
1085
+ await writeFile(cachedFilePath, Array.from(linesNumbers).join(","));
1086
+ }
1087
+ }
1088
+ if (!RETURN ||
1089
+ (Utils.isObject(RETURN) && !Object.keys(RETURN).length) ||
1090
+ (Array.isArray(RETURN) && !RETURN.length))
1091
+ return null;
1092
+ const greatestTotalItems = this.totalItems[`${tableName}-*`] ??
1093
+ Math.max(...Object.entries(this.totalItems)
1094
+ .filter(([k]) => k.startsWith(`${tableName}-`))
1095
+ .map(([, v]) => v));
1096
+ this.pageInfo[tableName] = {
1097
+ ...(({ columns, ...restOfOptions }) => restOfOptions)(options),
1098
+ perPage: Array.isArray(RETURN) ? RETURN.length : 1,
1099
+ totalPages: Math.ceil(greatestTotalItems / options.perPage),
1100
+ total: greatestTotalItems,
1101
+ };
1102
+ return onlyOne && Array.isArray(RETURN) ? RETURN[0] : RETURN;
1103
+ }
1104
+ async post(tableName, data, options, returnPostedData) {
1105
+ if (!options)
1106
+ options = {
1107
+ page: 1,
1108
+ perPage: 15,
1109
+ };
1110
+ const tablePath = join(this.databasePath, tableName), schema = (await this.getTable(tableName)).schema;
1111
+ if (!schema)
1112
+ throw this.Error("NO_SCHEMA", tableName);
1113
+ if (!returnPostedData)
1114
+ returnPostedData = false;
1115
+ const keys = UtilsServer.hashString(Object.keys(Array.isArray(data) ? data[0] : data).join("."));
1116
+ // Skip ID and (created|updated)At
1117
+ this.validateData(data, schema.slice(1, -2));
1118
+ let lastId = 0;
1119
+ const renameList = [];
1120
+ try {
1121
+ await File.lock(join(tablePath, ".tmp"), keys);
1122
+ let paginationFilePath;
1123
+ for await (const filePath of glob("*.pagination", { cwd: tablePath }))
1124
+ paginationFilePath = filePath;
1125
+ [lastId, this.totalItems[`${tableName}-*`]] = parse(paginationFilePath)
1126
+ .name.split("-")
1127
+ .map(Number);
1128
+ if (Utils.isArrayOfObjects(data))
1129
+ for (let index = 0; index < data.length; index++) {
1130
+ const element = data[index];
1131
+ element.id = ++lastId;
1132
+ element.createdAt = Date.now();
1133
+ element.updatedAt = undefined;
1134
+ }
1135
+ else {
1136
+ data.id = ++lastId;
1137
+ data.createdAt = Date.now();
1138
+ data.updatedAt = undefined;
1139
+ }
1140
+ await this.checkUnique(tableName, schema);
1141
+ data = this.formatData(data, schema);
1142
+ const pathesContents = this.joinPathesContents(tableName, this.tables[tableName].config.prepend
1143
+ ? Array.isArray(data)
1144
+ ? data.toReversed()
1145
+ : data
1146
+ : data);
1147
+ await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(this.tables[tableName].config.prepend
1148
+ ? await File.prepend(path, content)
1149
+ : await File.append(path, content))));
1150
+ await Promise.allSettled(renameList
1151
+ .filter(([_, filePath]) => filePath)
1152
+ .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1153
+ if (this.tables[tableName].config.cache)
1154
+ await this.clearCache(tableName);
1155
+ this.totalItems[`${tableName}-*`] += Array.isArray(data)
1156
+ ? data.length
1157
+ : 1;
1158
+ await rename(join(tablePath, paginationFilePath), join(tablePath, `${lastId}-${this.totalItems[`${tableName}-*`]}.pagination`));
1159
+ if (returnPostedData)
1160
+ return this.get(tableName, this.tables[tableName].config.prepend
1161
+ ? Array.isArray(data)
1162
+ ? data.map((_, index) => index + 1).toReversed()
1163
+ : 1
1164
+ : Array.isArray(data)
1165
+ ? data
1166
+ .map((_, index) => this.totalItems[`${tableName}-*`] - index)
1167
+ .toReversed()
1168
+ : this.totalItems[`${tableName}-*`], options, !Utils.isArrayOfObjects(data));
1169
+ }
1170
+ finally {
1171
+ if (renameList.length)
1172
+ await Promise.allSettled(renameList.map(async ([tempPath, _]) => unlink(tempPath)));
1173
+ await File.unlock(join(tablePath, ".tmp"), keys);
1174
+ }
1175
+ }
1176
+ async put(tableName, data, where, options = {
1177
+ page: 1,
1178
+ perPage: 15,
1179
+ }, returnUpdatedData) {
1180
+ const renameList = [];
1181
+ const tablePath = join(this.databasePath, tableName);
1182
+ const schema = (await this.throwErrorIfTableEmpty(tableName))
1183
+ .schema;
1184
+ if (!where) {
1185
+ if (Utils.isArrayOfObjects(data)) {
1186
+ if (!data.every((item) => Object.hasOwn(item, "id") && Utils.isValidID(item.id)))
1187
+ throw this.Error("INVALID_ID");
1188
+ return this.put(tableName, data, data.map(({ id }) => id), options, returnUpdatedData || undefined);
1189
+ }
1190
+ if (Object.hasOwn(data, "id")) {
1191
+ if (!Utils.isValidID(data.id))
1192
+ throw this.Error("INVALID_ID", data.id);
1193
+ return this.put(tableName, data, data.id, options, returnUpdatedData || undefined);
1194
+ }
1195
+ // Skip ID and (created|updated)At
1196
+ this.validateData(data, schema.slice(1, -2), true);
1197
+ await this.checkUnique(tableName, schema);
1198
+ this.formatData(data, schema, true);
1199
+ const pathesContents = this.joinPathesContents(tableName, {
1200
+ ...(({ id, ...restOfData }) => restOfData)(data),
1201
+ updatedAt: Date.now(),
1202
+ });
1203
+ try {
1204
+ await File.lock(join(tablePath, ".tmp"));
1205
+ for await (const paginationFilePath of glob("*.pagination", {
1206
+ cwd: tablePath,
1207
+ }))
1208
+ this.totalItems[`${tableName}-*`] = parse(paginationFilePath)
1209
+ .name.split("-")
1210
+ .map(Number)[1];
1211
+ await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content, this.totalItems[`${tableName}-*`]))));
1212
+ await Promise.allSettled(renameList
1213
+ .filter(([_, filePath]) => filePath)
1214
+ .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1215
+ if (this.tables[tableName].config.cache)
1216
+ await this.clearCache(join(tablePath, ".cache"));
1217
+ if (returnUpdatedData)
1218
+ return await this.get(tableName, undefined, options);
1219
+ }
1220
+ finally {
1221
+ if (renameList.length)
1222
+ await Promise.allSettled(renameList.map(async ([tempPath, _]) => unlink(tempPath)));
1223
+ await File.unlock(join(tablePath, ".tmp"));
1224
+ }
1225
+ }
1226
+ else if ((Array.isArray(where) && where.every(Utils.isValidID)) ||
1227
+ Utils.isValidID(where)) {
1228
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1229
+ return this.put(tableName, data, lineNumbers, options, returnUpdatedData || undefined);
1230
+ }
1231
+ else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1232
+ Utils.isNumber(where)) {
1233
+ // "where" in this case, is the line(s) number(s) and not id(s)
1234
+ this.validateData(data, schema.slice(1, -2), true);
1235
+ await this.checkUnique(tableName, schema.slice(1, -2));
1236
+ this.formatData(data, schema, true);
1237
+ const pathesContents = Object.fromEntries(Object.entries(this.joinPathesContents(tableName, Utils.isArrayOfObjects(data)
1238
+ ? data.map((item) => ({
1239
+ ...item,
1240
+ updatedAt: Date.now(),
1241
+ }))
1242
+ : { ...data, updatedAt: Date.now() })).map(([path, content]) => [
1243
+ path,
1244
+ [...(Array.isArray(where) ? where : [where])].reduce((obj, lineNum, index) => Object.assign(obj, {
1245
+ [lineNum]: Array.isArray(content) ? content[index] : content,
1246
+ }), {}),
1247
+ ]));
1248
+ const keys = UtilsServer.hashString(Object.keys(pathesContents)
1249
+ .map((path) => path.replaceAll(this.getFileExtension(tableName), ""))
1250
+ .join("."));
1251
+ try {
1252
+ await File.lock(join(tablePath, ".tmp"), keys);
1253
+ await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content))));
1254
+ await Promise.allSettled(renameList
1255
+ .filter(([_, filePath]) => filePath)
1256
+ .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1257
+ if (this.tables[tableName].config.cache)
1258
+ await this.clearCache(tableName);
1259
+ if (returnUpdatedData)
1260
+ return this.get(tableName, where, options, !Array.isArray(where));
1261
+ }
1262
+ finally {
1263
+ if (renameList.length)
1264
+ await Promise.allSettled(renameList.map(async ([tempPath, _]) => unlink(tempPath)));
1265
+ await File.unlock(join(tablePath, ".tmp"), keys);
1266
+ }
1267
+ }
1268
+ else if (Utils.isObject(where)) {
1269
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1270
+ if (lineNumbers)
1271
+ return this.put(tableName, data, lineNumbers, options, returnUpdatedData || undefined);
1272
+ }
1273
+ else
1274
+ throw this.Error("INVALID_PARAMETERS");
1275
+ }
1276
+ /**
1277
+ * Delete item(s) in a table
1278
+ *
1279
+ * @param {string} tableName
1280
+ * @param {(number | string | (number | string)[] | Criteria)} [where]
1281
+ * @return {boolean | null} {(Promise<boolean | null>)}
1282
+ */
1283
+ async delete(tableName, where, _id) {
1284
+ const tablePath = join(this.databasePath, tableName);
1285
+ await this.throwErrorIfTableEmpty(tableName);
1286
+ if (!where) {
1287
+ try {
1288
+ await File.lock(join(tablePath, ".tmp"));
1289
+ let paginationFilePath;
1290
+ let pagination;
1291
+ for await (const filePath of glob("*.pagination", {
1292
+ cwd: tablePath,
1293
+ })) {
1294
+ paginationFilePath = filePath;
1295
+ pagination = parse(filePath).name.split("-").map(Number);
1296
+ }
1297
+ await Promise.all((await readdir(tablePath))
1298
+ ?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)))
1299
+ .map(async (file) => unlink(join(tablePath, file))));
1300
+ if (this.tables[tableName].config.cache)
1301
+ await this.clearCache(tableName);
1302
+ await rename(join(tablePath, paginationFilePath), join(tablePath, `${pagination[0]}-0.pagination`));
1303
+ return true;
1304
+ }
1305
+ finally {
1306
+ await File.unlock(join(tablePath, ".tmp"));
1307
+ }
1308
+ }
1309
+ if ((Array.isArray(where) && where.every(Utils.isValidID)) ||
1310
+ Utils.isValidID(where)) {
1311
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1312
+ return this.delete(tableName, lineNumbers, where);
1313
+ }
1314
+ if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1315
+ Utils.isNumber(where)) {
1316
+ // "where" in this case, is the line(s) number(s) and not id(s)
1317
+ const files = (await readdir(tablePath))?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)));
1318
+ if (files.length) {
1319
+ const renameList = [];
1320
+ try {
1321
+ await File.lock(join(tablePath, ".tmp"));
1322
+ let paginationFilePath;
1323
+ let pagination;
1324
+ for await (const filePath of glob("*.pagination", {
1325
+ cwd: tablePath,
1326
+ })) {
1327
+ paginationFilePath = filePath;
1328
+ pagination = parse(filePath).name.split("-").map(Number);
1329
+ }
1330
+ if (pagination[1] &&
1331
+ pagination[1] - (Array.isArray(where) ? where.length : 1) > 0) {
1332
+ await Promise.all(files.map(async (file) => renameList.push(await File.remove(join(tablePath, file), where))));
1333
+ await Promise.all(renameList
1334
+ .filter(([_, filePath]) => filePath)
1335
+ .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1336
+ }
1337
+ else
1338
+ await Promise.all((await readdir(tablePath))
1339
+ ?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)))
1340
+ .map(async (file) => unlink(join(tablePath, file))));
1341
+ if (this.tables[tableName].config.cache)
1342
+ await this.clearCache(tableName);
1343
+ await rename(join(tablePath, paginationFilePath), join(tablePath, `${pagination[0]}-${pagination[1] - (Array.isArray(where) ? where.length : 1)}.pagination`));
1344
+ return true;
1345
+ }
1346
+ finally {
1347
+ if (renameList.length)
1348
+ await Promise.allSettled(renameList.map(async ([tempPath, _]) => unlink(tempPath)));
1349
+ await File.unlock(join(tablePath, ".tmp"));
1350
+ }
1351
+ }
1352
+ }
1353
+ else if (Utils.isObject(where)) {
1354
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1355
+ if (lineNumbers)
1356
+ return this.delete(tableName, lineNumbers);
1357
+ }
1358
+ else
1359
+ throw this.Error("INVALID_PARAMETERS");
1360
+ return false;
1361
+ }
1362
+ async sum(tableName, columns, where) {
1363
+ const RETURN = {};
1364
+ const tablePath = join(this.databasePath, tableName);
1365
+ await this.throwErrorIfTableEmpty(tableName);
1366
+ if (!Array.isArray(columns))
1367
+ columns = [columns];
1368
+ for await (const column of columns) {
1369
+ const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
1370
+ if (await File.isExists(columnPath)) {
1371
+ if (where) {
1372
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1373
+ RETURN[column] = lineNumbers
1374
+ ? await File.sum(columnPath, lineNumbers)
1375
+ : 0;
1376
+ }
1377
+ else
1378
+ RETURN[column] = await File.sum(columnPath);
1379
+ }
1380
+ }
1381
+ return Array.isArray(columns) ? RETURN : Object.values(RETURN)[0];
1382
+ }
1383
+ async max(tableName, columns, where) {
1384
+ const RETURN = {};
1385
+ const tablePath = join(this.databasePath, tableName);
1386
+ await this.throwErrorIfTableEmpty(tableName);
1387
+ if (!Array.isArray(columns))
1388
+ columns = [columns];
1389
+ for await (const column of columns) {
1390
+ const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
1391
+ if (await File.isExists(columnPath)) {
1392
+ if (where) {
1393
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1394
+ RETURN[column] = lineNumbers
1395
+ ? await File.max(columnPath, lineNumbers)
1396
+ : 0;
1397
+ }
1398
+ else
1399
+ RETURN[column] = await File.max(columnPath);
1400
+ }
1401
+ }
1402
+ return RETURN;
1403
+ }
1404
+ async min(tableName, columns, where) {
1405
+ const RETURN = {};
1406
+ const tablePath = join(this.databasePath, tableName);
1407
+ await this.throwErrorIfTableEmpty(tableName);
1408
+ if (!Array.isArray(columns))
1409
+ columns = [columns];
1410
+ for await (const column of columns) {
1411
+ const columnPath = join(tablePath, `${column}${this.getFileExtension(tableName)}`);
1412
+ if (await File.isExists(columnPath)) {
1413
+ if (where) {
1414
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1415
+ RETURN[column] = lineNumbers
1416
+ ? await File.min(columnPath, lineNumbers)
1417
+ : 0;
1418
+ }
1419
+ else
1420
+ RETURN[column] = await File.min(columnPath);
1421
+ }
1422
+ }
1423
+ return RETURN;
1424
+ }
1425
+ }