inibase 1.0.0-rc.5 → 1.0.0-rc.50

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