inibase 1.0.0-rc.5 → 1.0.0-rc.51

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