inibase 1.0.0-rc.6 → 1.0.0-rc.60

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