inibase 1.0.0-rc.7 → 1.0.0-rc.70

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