inibase 1.0.0-rc.4 → 1.0.0-rc.42

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