inibase 1.0.0-rc.10 → 1.0.0-rc.101

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