inibase 1.0.0-rc.12 → 1.0.0-rc.15

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,940 @@
1
+ import { unlink, rename, readFile, writeFile, mkdir, readdir, } from "node:fs/promises";
2
+ import { join, parse } from "node:path";
3
+ import { scryptSync } from "node:crypto";
4
+ import File from "./file";
5
+ import Utils from "./utils";
6
+ export default class Inibase {
7
+ folder;
8
+ database;
9
+ table;
10
+ pageInfo;
11
+ cache;
12
+ totalItems;
13
+ salt;
14
+ constructor(database, mainFolder = ".") {
15
+ this.database = database;
16
+ this.folder = mainFolder;
17
+ this.table = null;
18
+ this.cache = new Map();
19
+ this.totalItems = {};
20
+ this.pageInfo = { page: 1, per_page: 15 };
21
+ this.salt = scryptSync(process.env.INIBASE_SECRET ?? "inibase", (process.env.INIBASE_SECRET ?? "inibase") + "_salt", 32);
22
+ }
23
+ throwError(code, variable, language = "en") {
24
+ const errorMessages = {
25
+ en: {
26
+ FIELD_REQUIRED: "REQUIRED: {variable}",
27
+ NO_SCHEMA: "NO_SCHEMA: {variable}",
28
+ NO_ITEMS: "NO_ITEMS: {variable}",
29
+ NO_DATA: "NO_DATA: {variable}",
30
+ INVALID_ID: "INVALID_ID: {variable}",
31
+ INVALID_TYPE: "INVALID_TYPE: {variable}",
32
+ INVALID_OPERATOR: "INVALID_OPERATOR: {variable}",
33
+ INVALID_PARAMETERS: "PARAMETERS: {variable}",
34
+ },
35
+ // Add more languages and error messages as needed
36
+ };
37
+ let errorMessage = errorMessages[language][code] || code;
38
+ if (variable) {
39
+ if (typeof variable === "string" ||
40
+ typeof variable === "number" ||
41
+ Array.isArray(variable))
42
+ errorMessage = errorMessage.replaceAll(`{variable}`, Array.isArray(variable) ? variable.join(", ") : variable);
43
+ else
44
+ Object.keys(variable).forEach((variableKey) => (errorMessage = errorMessage.replaceAll(`{${variableKey}}`, variable[variableKey].toString())));
45
+ }
46
+ return new Error(errorMessage);
47
+ }
48
+ async setTableSchema(tableName, schema) {
49
+ const decodeIdFromSchema = (schema) => schema.map((field) => {
50
+ if (field.children && Utils.isArrayOfObjects(field.children))
51
+ field.children = decodeIdFromSchema(field.children);
52
+ if (!Utils.isNumber(field.id))
53
+ field.id = Utils.decodeID(field.id, this.salt);
54
+ return field;
55
+ });
56
+ // remove id from schema
57
+ schema = schema.filter(({ key }) => !["id", "createdAt", "updatedAt"].includes(key));
58
+ schema = Utils.addIdToSchema(schema, Utils.findLastIdNumber(schema, this.salt), this.salt);
59
+ const TablePath = join(this.folder, this.database, tableName), TableSchemaPath = join(TablePath, "schema.json");
60
+ if (!(await File.isExists(TablePath)))
61
+ await mkdir(TablePath, { recursive: true });
62
+ if (await File.isExists(TableSchemaPath)) {
63
+ // update columns files names based on field id
64
+ const schemaToIdsPath = (schema, prefix = "") => {
65
+ let RETURN = {};
66
+ for (const field of schema)
67
+ if (field.children && Utils.isArrayOfObjects(field.children)) {
68
+ Utils.deepMerge(RETURN, schemaToIdsPath(field.children, (prefix ?? "") + field.key + "."));
69
+ }
70
+ else if (Utils.isValidID(field.id))
71
+ RETURN[Utils.decodeID(field.id, this.salt)] =
72
+ (prefix ?? "") + field.key + ".inib";
73
+ return RETURN;
74
+ }, replaceOldPathes = Utils.findChangedProperties(schemaToIdsPath(await this.getTableSchema(tableName)), schemaToIdsPath(schema));
75
+ if (replaceOldPathes)
76
+ for (const [oldPath, newPath] of Object.entries(replaceOldPathes))
77
+ if (await File.isExists(join(TablePath, oldPath)))
78
+ await rename(join(TablePath, oldPath), join(TablePath, newPath));
79
+ }
80
+ await writeFile(join(TablePath, "schema.json"), JSON.stringify(decodeIdFromSchema(schema)));
81
+ }
82
+ async getTableSchema(tableName) {
83
+ const TableSchemaPath = join(this.folder, this.database, tableName, "schema.json");
84
+ if (!(await File.isExists(TableSchemaPath)))
85
+ return undefined;
86
+ if (!this.cache.has(TableSchemaPath))
87
+ this.cache.set(TableSchemaPath, await readFile(TableSchemaPath, "utf8"));
88
+ if (!this.cache.get(TableSchemaPath))
89
+ return undefined;
90
+ const schema = JSON.parse(this.cache.get(TableSchemaPath)), lastIdNumber = Utils.findLastIdNumber(schema, this.salt);
91
+ return [
92
+ {
93
+ id: Utils.encodeID(0, this.salt),
94
+ key: "id",
95
+ type: "id",
96
+ required: true,
97
+ },
98
+ ...Utils.addIdToSchema(schema, lastIdNumber, this.salt),
99
+ {
100
+ id: Utils.encodeID(lastIdNumber + 1, this.salt),
101
+ key: "createdAt",
102
+ type: "date",
103
+ required: true,
104
+ },
105
+ {
106
+ id: Utils.encodeID(lastIdNumber + 2, this.salt),
107
+ key: "updatedAt",
108
+ type: "date",
109
+ required: false,
110
+ },
111
+ ];
112
+ }
113
+ getField(keyPath, schema, property) {
114
+ const keyPathSplited = keyPath.split(".");
115
+ for (const [index, key] of keyPathSplited.entries()) {
116
+ if (key === "*")
117
+ continue;
118
+ const foundItem = schema.find((item) => item.key === key);
119
+ if (!foundItem)
120
+ return null;
121
+ if (index === keyPathSplited.length - 1)
122
+ schema = foundItem;
123
+ if ((foundItem.type === "array" || foundItem.type === "object") &&
124
+ foundItem.children &&
125
+ Utils.isArrayOfObjects(foundItem.children))
126
+ schema = foundItem.children;
127
+ }
128
+ if (property) {
129
+ switch (property) {
130
+ case "type":
131
+ return schema.type;
132
+ case "children":
133
+ return schema.children;
134
+ default:
135
+ return schema[property];
136
+ }
137
+ }
138
+ else
139
+ return schema;
140
+ }
141
+ validateData(data, schema, skipRequiredField = false) {
142
+ if (Utils.isArrayOfObjects(data))
143
+ for (const single_data of data)
144
+ this.validateData(single_data, schema, skipRequiredField);
145
+ else if (Utils.isObject(data)) {
146
+ for (const { key, type, required, children } of schema) {
147
+ if (!data.hasOwnProperty(key) && required && !skipRequiredField)
148
+ throw this.throwError("FIELD_REQUIRED", key);
149
+ if (data.hasOwnProperty(key) &&
150
+ !Utils.validateFieldType(data[key], type, children && !Utils.isArrayOfObjects(children) ? children : undefined))
151
+ throw this.throwError("INVALID_TYPE", key);
152
+ if ((type === "array" || type === "object") &&
153
+ children &&
154
+ Utils.isArrayOfObjects(children))
155
+ this.validateData(data[key], children, skipRequiredField);
156
+ }
157
+ }
158
+ }
159
+ formatData(data, schema, formatOnlyAvailiableKeys) {
160
+ this.validateData(data, schema, formatOnlyAvailiableKeys);
161
+ const formatField = (value, field) => {
162
+ if (Array.isArray(field.type))
163
+ field.type = Utils.detectFieldType(value, field.type);
164
+ switch (field.type) {
165
+ case "array":
166
+ if (!Array.isArray(value))
167
+ value = [value];
168
+ if (typeof field.children === "string") {
169
+ if (field.type === "array" && field.children === "table") {
170
+ if (Array.isArray(value)) {
171
+ if (Utils.isArrayOfObjects(value)) {
172
+ if (value.every((item) => item.hasOwnProperty("id") &&
173
+ (Utils.isValidID(item.id) || Utils.isNumber(item.id))))
174
+ value.map((item) => Utils.isNumber(item.id)
175
+ ? Number(item.id)
176
+ : Utils.decodeID(item.id, this.salt));
177
+ }
178
+ else if (value.every(Utils.isValidID) ||
179
+ value.every(Utils.isNumber))
180
+ return value.map((item) => Utils.isNumber(item)
181
+ ? Number(item)
182
+ : Utils.decodeID(item, this.salt));
183
+ }
184
+ else if (Utils.isValidID(value))
185
+ return [Utils.decodeID(value, this.salt)];
186
+ else if (Utils.isNumber(value))
187
+ return [Number(value)];
188
+ }
189
+ else if (data.hasOwnProperty(field.key))
190
+ return value;
191
+ }
192
+ else if (Utils.isArrayOfObjects(field.children))
193
+ return this.formatData(value, field.children, formatOnlyAvailiableKeys);
194
+ else if (Array.isArray(field.children))
195
+ return Array.isArray(value) ? value : [value];
196
+ break;
197
+ case "object":
198
+ if (Utils.isArrayOfObjects(field.children))
199
+ return this.formatData(value, field.children, formatOnlyAvailiableKeys);
200
+ break;
201
+ case "table":
202
+ if (Array.isArray(value))
203
+ value = value[0];
204
+ if (Utils.isObject(value)) {
205
+ if (value.hasOwnProperty("id") &&
206
+ (Utils.isValidID(value.id) ||
207
+ Utils.isNumber(value.id)))
208
+ return Utils.isNumber(value.id)
209
+ ? Number(value.id)
210
+ : Utils.decodeID(value.id, this.salt);
211
+ }
212
+ else if (Utils.isValidID(value) || Utils.isNumber(value))
213
+ return Utils.isNumber(value)
214
+ ? Number(value)
215
+ : Utils.decodeID(value, this.salt);
216
+ break;
217
+ case "password":
218
+ if (Array.isArray(value))
219
+ value = value[0];
220
+ return typeof value === "string" && value.length === 161
221
+ ? value
222
+ : Utils.hashPassword(String(value));
223
+ case "number":
224
+ if (Array.isArray(value))
225
+ value = value[0];
226
+ return Utils.isNumber(value) ? Number(value) : null;
227
+ case "id":
228
+ if (Array.isArray(value))
229
+ value = value[0];
230
+ return Utils.isNumber(value)
231
+ ? value
232
+ : Utils.decodeID(value, this.salt);
233
+ default:
234
+ return value;
235
+ }
236
+ return null;
237
+ };
238
+ if (Utils.isArrayOfObjects(data))
239
+ return data.map((single_data) => this.formatData(single_data, schema, formatOnlyAvailiableKeys));
240
+ else if (Utils.isObject(data)) {
241
+ let RETURN = {};
242
+ for (const field of schema) {
243
+ if (!data.hasOwnProperty(field.key)) {
244
+ if (formatOnlyAvailiableKeys || !field.required)
245
+ continue;
246
+ RETURN[field.key] = this.getDefaultValue(field);
247
+ continue;
248
+ }
249
+ RETURN[field.key] = formatField(data[field.key], field);
250
+ }
251
+ return RETURN;
252
+ }
253
+ else
254
+ return [];
255
+ }
256
+ getDefaultValue(field) {
257
+ if (Array.isArray(field.type))
258
+ return this.getDefaultValue({
259
+ ...field,
260
+ type: field.type.sort((a, b) => Number(b === "array") - Number(a === "array") ||
261
+ Number(a === "string") - Number(b === "string") ||
262
+ Number(a === "number") - Number(b === "number"))[0],
263
+ });
264
+ switch (field.type) {
265
+ case "array":
266
+ return Utils.isArrayOfObjects(field.children)
267
+ ? [
268
+ this.getDefaultValue({
269
+ ...field,
270
+ type: "object",
271
+ children: field.children,
272
+ }),
273
+ ]
274
+ : [];
275
+ case "object":
276
+ return Utils.combineObjects(field.children.map((f) => ({
277
+ [f.key]: this.getDefaultValue(f),
278
+ })));
279
+ case "boolean":
280
+ return false;
281
+ default:
282
+ return null;
283
+ }
284
+ }
285
+ joinPathesContentsReplacement(mainPath, data, startingtAt = 1) {
286
+ if (Utils.isArrayOfObjects(data)) {
287
+ return Utils.combineObjects(data.map((single_data) => this.joinPathesContentsReplacement(mainPath, single_data, startingtAt++)));
288
+ }
289
+ else {
290
+ return Object.fromEntries(Object.entries(Utils.objectToDotNotation(data)).map(([key, value]) => [
291
+ join(mainPath, key + ".inib"),
292
+ { [startingtAt]: value },
293
+ ]));
294
+ }
295
+ }
296
+ joinPathesContents(mainPath, data, startWith = 1) {
297
+ const CombineData = (_data, prefix) => {
298
+ let RETURN = {};
299
+ const combineObjectsToArray = (input) => input.reduce((r, c) => (Object.keys(c).map((k) => (r[k] = [...(r[k] || []), c[k]])), r), {});
300
+ if (Utils.isArrayOfObjects(_data))
301
+ RETURN = combineObjectsToArray(_data.map((single_data) => CombineData(single_data)));
302
+ else {
303
+ for (const [key, value] of Object.entries(_data)) {
304
+ if (Utils.isObject(value))
305
+ Object.assign(RETURN, CombineData(value, `${key}.`));
306
+ else if (Utils.isArrayOfObjects(value)) {
307
+ Object.assign(RETURN, CombineData(combineObjectsToArray(value), (prefix ?? "") + key + "."));
308
+ }
309
+ else if (Utils.isArrayOfArrays(value) &&
310
+ value.every(Utils.isArrayOfObjects))
311
+ Object.assign(RETURN, CombineData(combineObjectsToArray(value.map(combineObjectsToArray)), (prefix ?? "") + key + "."));
312
+ else
313
+ RETURN[(prefix ?? "") + key] = File.encode(value);
314
+ }
315
+ }
316
+ return RETURN;
317
+ };
318
+ const addPathToKeys = (obj, path) => {
319
+ const newObject = {};
320
+ for (const key in obj)
321
+ newObject[join(path, key + ".inib")] = obj[key];
322
+ return newObject;
323
+ };
324
+ return addPathToKeys(CombineData(data), mainPath);
325
+ }
326
+ async get(tableName, where, options = {
327
+ page: 1,
328
+ per_page: 15,
329
+ }, onlyOne, onlyLinesNumbers) {
330
+ if (!options.columns)
331
+ options.columns = [];
332
+ else if (!Array.isArray(options.columns))
333
+ options.columns = [options.columns];
334
+ if (options.columns.length && !options.columns.includes("id"))
335
+ options.columns.push("id");
336
+ if (!options.page)
337
+ options.page = 1;
338
+ if (!options.per_page)
339
+ options.per_page = 15;
340
+ let RETURN;
341
+ let schema = await this.getTableSchema(tableName);
342
+ if (!schema)
343
+ throw this.throwError("NO_SCHEMA", tableName);
344
+ const idFilePath = join(this.folder, this.database, tableName, "id.inib");
345
+ if (!(await File.isExists(idFilePath)))
346
+ return null;
347
+ const filterSchemaByColumns = (schema, columns) => schema
348
+ .map((field) => {
349
+ if (columns.some((column) => column.startsWith("!")))
350
+ return columns.includes("!" + field.key) ? null : field;
351
+ if (columns.includes(field.key) || columns.includes("*"))
352
+ return field;
353
+ if ((field.type === "array" || field.type === "object") &&
354
+ Utils.isArrayOfObjects(field.children) &&
355
+ columns.filter((column) => column.startsWith(field.key + ".") ||
356
+ column.startsWith("!" + field.key + ".")).length) {
357
+ field.children = filterSchemaByColumns(field.children, columns
358
+ .filter((column) => column.startsWith(field.key + ".") ||
359
+ column.startsWith("!" + field.key + "."))
360
+ .map((column) => column.replace(field.key + ".", "")));
361
+ return field;
362
+ }
363
+ return null;
364
+ })
365
+ .filter((i) => i);
366
+ if (options.columns.length)
367
+ schema = filterSchemaByColumns(schema, options.columns);
368
+ const getItemsFromSchema = async (path, schema, linesNumber, prefix) => {
369
+ let RETURN = {};
370
+ for (const field of schema) {
371
+ if ((field.type === "array" ||
372
+ (Array.isArray(field.type) &&
373
+ field.type.includes("array"))) &&
374
+ field
375
+ .children) {
376
+ if (Utils.isArrayOfObjects(field
377
+ .children)) {
378
+ if (field
379
+ .children.filter((children) => children.type === "array" &&
380
+ Utils.isArrayOfObjects(children.children)).length) {
381
+ // one of children has array field type and has children array of object = Schema
382
+ Object.entries((await getItemsFromSchema(path, field.children.filter((children) => children.type === "array" &&
383
+ Utils.isArrayOfObjects(children.children)), linesNumber, (prefix ?? "") + field.key + ".")) ?? {}).forEach(([index, item]) => {
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" &&
390
+ Utils.isArrayOfObjects(children.children))) {
391
+ if (Utils.isObject(item[child_field.key])) {
392
+ Object.entries(item[child_field.key]).forEach(([key, value]) => {
393
+ for (let _i = 0; _i < value.length; _i++) {
394
+ if (!RETURN[index][field.key][_i])
395
+ RETURN[index][field.key][_i] = {};
396
+ if (!RETURN[index][field.key][_i][child_field.key])
397
+ RETURN[index][field.key][_i][child_field.key] =
398
+ [];
399
+ value[_i].forEach((_element, _index) => {
400
+ if (!RETURN[index][field.key][_i][child_field.key][_index])
401
+ RETURN[index][field.key][_i][child_field.key][_index] = {};
402
+ RETURN[index][field.key][_i][child_field.key][_index][key] = _element;
403
+ });
404
+ }
405
+ });
406
+ }
407
+ }
408
+ }
409
+ });
410
+ field.children = field
411
+ .children.filter((children) => children.type !== "array" ||
412
+ !Utils.isArrayOfObjects(children.children));
413
+ }
414
+ Object.entries((await getItemsFromSchema(path, field
415
+ .children, linesNumber, (prefix ?? "") + field.key + ".")) ?? {}).forEach(([index, item]) => {
416
+ if (!RETURN[index])
417
+ RETURN[index] = {};
418
+ if (Utils.isObject(item)) {
419
+ if (!Object.values(item).every((i) => i === null)) {
420
+ if (RETURN[index][field.key])
421
+ Object.entries(item).forEach(([key, value], _index) => {
422
+ RETURN[index][field.key] = RETURN[index][field.key].map((_obj, _i) => ({
423
+ ..._obj,
424
+ [key]: value[_i],
425
+ }));
426
+ });
427
+ else if (Object.values(item).every(Utils.isArrayOfArrays))
428
+ RETURN[index][field.key] = item;
429
+ else {
430
+ RETURN[index][field.key] = [];
431
+ Object.entries(item).forEach(([key, value]) => {
432
+ for (let _i = 0; _i < value.length; _i++) {
433
+ if (!RETURN[index][field.key][_i])
434
+ RETURN[index][field.key][_i] = {};
435
+ RETURN[index][field.key][_i][key] = value[_i];
436
+ }
437
+ });
438
+ }
439
+ }
440
+ else
441
+ RETURN[index][field.key] = null;
442
+ }
443
+ else
444
+ RETURN[index][field.key] = item;
445
+ });
446
+ }
447
+ else if (field
448
+ .children === "table" ||
449
+ (Array.isArray(field
450
+ .children) &&
451
+ field
452
+ .children.includes("table"))) {
453
+ if (options.columns)
454
+ options.columns = options.columns
455
+ .filter((column) => column.includes(`${field.key}.`))
456
+ .map((column) => column.replace(`${field.key}.`, ""));
457
+ const [items, total_lines] = await File.get(join(path, (prefix ?? "") + field.key + ".inib"), linesNumber, field.type, field
458
+ .children, this.salt);
459
+ this.totalItems[tableName + "-" + field.key] = total_lines;
460
+ for (const [index, item] of Object.entries(items)) {
461
+ if (!RETURN[index])
462
+ RETURN[index] = {};
463
+ RETURN[index][field.key] = item
464
+ ? await this.get(field.key, item, options)
465
+ : this.getDefaultValue(field);
466
+ }
467
+ }
468
+ else if (await File.isExists(join(path, (prefix ?? "") + field.key + ".inib"))) {
469
+ const [items, total_lines] = await File.get(join(path, (prefix ?? "") + field.key + ".inib"), linesNumber, field.type, field?.children, this.salt);
470
+ this.totalItems[tableName + "-" + field.key] = total_lines;
471
+ for (const [index, item] of Object.entries(items)) {
472
+ if (!RETURN[index])
473
+ RETURN[index] = {};
474
+ RETURN[index][field.key] = item ?? this.getDefaultValue(field);
475
+ }
476
+ }
477
+ }
478
+ else if (field.type === "object") {
479
+ for (const [index, item] of Object.entries((await getItemsFromSchema(path, field.children, linesNumber, (prefix ?? "") + field.key + ".")) ?? {})) {
480
+ if (!RETURN[index])
481
+ RETURN[index] = {};
482
+ if (Utils.isObject(item)) {
483
+ if (!Object.values(item).every((i) => i === null))
484
+ RETURN[index][field.key] = item;
485
+ else
486
+ RETURN[index][field.key] = null;
487
+ }
488
+ else
489
+ RETURN[index][field.key] = null;
490
+ }
491
+ }
492
+ else if (field.type === "table") {
493
+ if ((await File.isExists(join(this.folder, this.database, field.key))) &&
494
+ (await File.isExists(join(path, (prefix ?? "") + field.key + ".inib")))) {
495
+ if (options.columns)
496
+ options.columns = options.columns
497
+ .filter((column) => column.includes(`${field.key}.`) &&
498
+ !column.includes(`${field.key}.`))
499
+ .map((column) => column.replace(`${field.key}.`, ""));
500
+ const [items, total_lines] = await File.get(join(path, (prefix ?? "") + field.key + ".inib"), linesNumber, "number", undefined, this.salt);
501
+ this.totalItems[tableName + "-" + field.key] = total_lines;
502
+ for (const [index, item] of Object.entries(items)) {
503
+ if (!RETURN[index])
504
+ RETURN[index] = {};
505
+ RETURN[index][field.key] = item
506
+ ? await this.get(field.key, item, options)
507
+ : this.getDefaultValue(field);
508
+ }
509
+ }
510
+ }
511
+ else if (await File.isExists(join(path, (prefix ?? "") + field.key + ".inib"))) {
512
+ const [items, total_lines] = await File.get(join(path, (prefix ?? "") + field.key + ".inib"), linesNumber, field.type, field?.children, this.salt);
513
+ this.totalItems[tableName + "-" + field.key] = total_lines;
514
+ for (const [index, item] of Object.entries(items)) {
515
+ if (!RETURN[index])
516
+ RETURN[index] = {};
517
+ RETURN[index][field.key] = item ?? this.getDefaultValue(field);
518
+ }
519
+ }
520
+ }
521
+ return RETURN;
522
+ };
523
+ if (!where) {
524
+ // Display all data
525
+ RETURN = Object.values(await getItemsFromSchema(join(this.folder, this.database, tableName), schema, Array.from({ length: options.per_page }, (_, index) => (options.page - 1) * options.per_page +
526
+ index +
527
+ 1)));
528
+ }
529
+ else if ((Array.isArray(where) &&
530
+ (where.every(Utils.isValidID) || where.every(Utils.isNumber))) ||
531
+ Utils.isValidID(where) ||
532
+ Utils.isNumber(where)) {
533
+ let Ids = where;
534
+ if (!Array.isArray(Ids))
535
+ Ids = [Ids];
536
+ const [lineNumbers, countItems] = await File.search(idFilePath, "[]", Ids.map((id) => Utils.isNumber(id) ? Number(id) : Utils.decodeID(id, this.salt)), undefined, "number", undefined, Ids.length, 0, false, this.salt);
537
+ if (!lineNumbers || !Object.keys(lineNumbers).length)
538
+ throw this.throwError("INVALID_ID", where);
539
+ if (onlyLinesNumbers)
540
+ return Object.keys(lineNumbers).map(Number);
541
+ RETURN = Object.values((await getItemsFromSchema(join(this.folder, this.database, tableName), schema, Object.keys(lineNumbers).map(Number))) ?? {});
542
+ if (RETURN.length && !Array.isArray(where))
543
+ RETURN = RETURN[0];
544
+ }
545
+ else if (Utils.isObject(where)) {
546
+ // Criteria
547
+ const FormatObjectCriteriaValue = (value, isParentArray = false) => {
548
+ switch (value[0]) {
549
+ case ">":
550
+ case "<":
551
+ case "[":
552
+ return ["=", "]", "*"].includes(value[1])
553
+ ? [
554
+ value.slice(0, 2),
555
+ value.slice(2),
556
+ ]
557
+ : [
558
+ value.slice(0, 1),
559
+ value.slice(1),
560
+ ];
561
+ case "!":
562
+ return ["=", "*"].includes(value[1])
563
+ ? [
564
+ value.slice(0, 2),
565
+ value.slice(2),
566
+ ]
567
+ : value[1] === "["
568
+ ? [
569
+ value.slice(0, 3),
570
+ value.slice(3),
571
+ ]
572
+ : [
573
+ (value.slice(0, 1) + "="),
574
+ value.slice(1),
575
+ ];
576
+ case "=":
577
+ return isParentArray
578
+ ? [
579
+ value.slice(0, 1),
580
+ value.slice(1),
581
+ ]
582
+ : [
583
+ value.slice(0, 1),
584
+ (value.slice(1) + ","),
585
+ ];
586
+ case "*":
587
+ return [
588
+ value.slice(0, 1),
589
+ value.slice(1),
590
+ ];
591
+ default:
592
+ return ["=", value];
593
+ }
594
+ };
595
+ const applyCriteria = async (criteria, allTrue) => {
596
+ let RETURN = {};
597
+ if (!criteria)
598
+ return null;
599
+ if (criteria.and && Utils.isObject(criteria.and)) {
600
+ const searchResult = await applyCriteria(criteria.and, true);
601
+ if (searchResult) {
602
+ RETURN = Utils.deepMerge(RETURN, Object.fromEntries(Object.entries(searchResult).filter(([_k, v], _i) => Object.keys(v).length ===
603
+ Object.keys(criteria.and ?? {}).length)));
604
+ delete criteria.and;
605
+ }
606
+ else
607
+ return null;
608
+ }
609
+ if (criteria.or && Utils.isObject(criteria.or)) {
610
+ const searchResult = await applyCriteria(criteria.or, false);
611
+ delete criteria.or;
612
+ if (searchResult)
613
+ RETURN = Utils.deepMerge(RETURN, searchResult);
614
+ }
615
+ if (Object.keys(criteria).length > 0) {
616
+ if (allTrue === undefined)
617
+ allTrue = true;
618
+ let index = -1;
619
+ for (const [key, value] of Object.entries(criteria)) {
620
+ const field = this.getField(key, schema);
621
+ index++;
622
+ let searchOperator = undefined, searchComparedAtValue = undefined, searchLogicalOperator = undefined;
623
+ if (Utils.isObject(value)) {
624
+ if (value?.or &&
625
+ Array.isArray(value.or)) {
626
+ const searchCriteria = value.or
627
+ .map((single_or) => typeof single_or === "string"
628
+ ? FormatObjectCriteriaValue(single_or)
629
+ : ["=", single_or])
630
+ .filter((a) => a);
631
+ if (searchCriteria.length > 0) {
632
+ searchOperator = searchCriteria.map((single_or) => single_or[0]);
633
+ searchComparedAtValue = searchCriteria.map((single_or) => single_or[1]);
634
+ searchLogicalOperator = "or";
635
+ }
636
+ delete value.or;
637
+ }
638
+ if (value?.and &&
639
+ Array.isArray(value.and)) {
640
+ const searchCriteria = value.and
641
+ .map((single_and) => typeof single_and === "string"
642
+ ? FormatObjectCriteriaValue(single_and)
643
+ : ["=", single_and])
644
+ .filter((a) => a);
645
+ if (searchCriteria.length > 0) {
646
+ searchOperator = searchCriteria.map((single_and) => single_and[0]);
647
+ searchComparedAtValue = searchCriteria.map((single_and) => single_and[1]);
648
+ searchLogicalOperator = "and";
649
+ }
650
+ delete value.and;
651
+ }
652
+ }
653
+ else if (Array.isArray(value)) {
654
+ const searchCriteria = value
655
+ .map((single) => typeof single === "string"
656
+ ? FormatObjectCriteriaValue(single)
657
+ : ["=", single])
658
+ .filter((a) => a);
659
+ if (searchCriteria.length > 0) {
660
+ searchOperator = searchCriteria.map((single) => single[0]);
661
+ searchComparedAtValue = searchCriteria.map((single) => single[1]);
662
+ searchLogicalOperator = "and";
663
+ }
664
+ }
665
+ else if (typeof value === "string") {
666
+ const ComparisonOperatorValue = FormatObjectCriteriaValue(value);
667
+ if (ComparisonOperatorValue) {
668
+ searchOperator = ComparisonOperatorValue[0];
669
+ searchComparedAtValue = ComparisonOperatorValue[1];
670
+ }
671
+ }
672
+ else {
673
+ searchOperator = "=";
674
+ searchComparedAtValue = value;
675
+ }
676
+ const [searchResult, total_lines] = await File.search(join(this.folder, this.database, tableName, key + ".inib"), searchOperator, searchComparedAtValue, searchLogicalOperator, field?.type, field?.children, options.per_page, options.page - 1 * options.per_page + 1, true, this.salt);
677
+ if (searchResult) {
678
+ RETURN = Utils.deepMerge(RETURN, searchResult);
679
+ this.totalItems[tableName + "-" + key] = total_lines;
680
+ }
681
+ if (allTrue && index > 0) {
682
+ if (!Object.keys(RETURN).length)
683
+ RETURN = {};
684
+ RETURN = Object.fromEntries(Object.entries(RETURN).filter(([_index, item]) => Object.keys(item).length > index));
685
+ if (!Object.keys(RETURN).length)
686
+ RETURN = {};
687
+ }
688
+ }
689
+ }
690
+ return Object.keys(RETURN).length ? RETURN : null;
691
+ };
692
+ RETURN = await applyCriteria(where);
693
+ if (RETURN) {
694
+ if (onlyLinesNumbers)
695
+ return Object.keys(RETURN).map(Number);
696
+ const alreadyExistsColumns = Object.keys(Object.values(RETURN)[0]).map((key) => parse(key).name);
697
+ RETURN = Object.values(Utils.deepMerge(RETURN, await getItemsFromSchema(join(this.folder, this.database, tableName), schema.filter((field) => !alreadyExistsColumns.includes(field.key)), Object.keys(RETURN).map(Number))));
698
+ }
699
+ }
700
+ if (!RETURN ||
701
+ (Utils.isObject(RETURN) && !Object.keys(RETURN).length) ||
702
+ (Array.isArray(RETURN) && !RETURN.length))
703
+ return null;
704
+ const greatestTotalItems = Math.max(...Object.entries(this.totalItems)
705
+ .filter(([k]) => k.startsWith(tableName + "-"))
706
+ .map(([, v]) => v));
707
+ this.pageInfo = {
708
+ ...(({ columns, ...restOfOptions }) => restOfOptions)(options),
709
+ total_pages: Math.ceil(greatestTotalItems / options.per_page),
710
+ total: greatestTotalItems,
711
+ };
712
+ return onlyOne ? RETURN[0] : RETURN;
713
+ }
714
+ async post(tableName, data, options = {
715
+ page: 1,
716
+ per_page: 15,
717
+ }, returnPostedData = true) {
718
+ const schema = await this.getTableSchema(tableName);
719
+ let RETURN;
720
+ if (!schema)
721
+ throw this.throwError("NO_SCHEMA", tableName);
722
+ const idFilePath = join(this.folder, this.database, tableName, "id.inib");
723
+ let [last_line_number, last_id] = (await File.isExists(idFilePath))
724
+ ? Object.entries((await File.get(idFilePath, -1, "number", undefined, this.salt))[0])[0] ?? [0, 0]
725
+ : [0, 0];
726
+ if (Utils.isArrayOfObjects(data))
727
+ RETURN = data.map(({ id, updatedAt, createdAt, ...rest }) => ({
728
+ id: ++last_id,
729
+ ...rest,
730
+ createdAt: new Date(),
731
+ }));
732
+ else
733
+ RETURN = (({ id, updatedAt, createdAt, ...rest }) => ({
734
+ id: ++last_id,
735
+ ...rest,
736
+ createdAt: new Date(),
737
+ }))(data);
738
+ if (!RETURN)
739
+ throw this.throwError("NO_DATA");
740
+ RETURN = this.formatData(RETURN, schema);
741
+ const pathesContents = this.joinPathesContentsReplacement(join(this.folder, this.database, tableName), RETURN, ++last_line_number);
742
+ for await (const [path, content] of Object.entries(pathesContents))
743
+ await File.replace(path, content, this.salt);
744
+ if (returnPostedData)
745
+ return this.get(tableName, Utils.isArrayOfObjects(RETURN)
746
+ ? RETURN.map((data) => data.id)
747
+ : RETURN.id, options, !Utils.isArrayOfObjects(data) // return only one item if data is not array of objects
748
+ );
749
+ }
750
+ async put(tableName, data, where, options = {
751
+ page: 1,
752
+ per_page: 15,
753
+ }, returnPostedData) {
754
+ const schema = await this.getTableSchema(tableName);
755
+ if (!schema)
756
+ throw this.throwError("NO_SCHEMA", tableName);
757
+ const idFilePath = join(this.folder, this.database, tableName, "id.inib");
758
+ if (!(await File.isExists(idFilePath)))
759
+ throw this.throwError("NO_ITEMS", tableName);
760
+ data = this.formatData(data, schema, true);
761
+ if (!where) {
762
+ if (Utils.isArrayOfObjects(data)) {
763
+ if (!data.every((item) => item.hasOwnProperty("id") && Utils.isValidID(item.id)))
764
+ throw this.throwError("INVALID_ID");
765
+ return this.put(tableName, data, data.map((item) => item.id));
766
+ }
767
+ else if (data.hasOwnProperty("id")) {
768
+ if (!Utils.isValidID(data.id))
769
+ throw this.throwError("INVALID_ID", data.id);
770
+ return this.put(tableName, data, Utils.decodeID(data.id, this.salt));
771
+ }
772
+ else {
773
+ const pathesContents = this.joinPathesContents(join(this.folder, this.database, tableName), Utils.isArrayOfObjects(data)
774
+ ? data.map((item) => ({
775
+ ...(({ id, ...restOfData }) => restOfData)(item),
776
+ updatedAt: new Date(),
777
+ }))
778
+ : {
779
+ ...(({ id, ...restOfData }) => restOfData)(data),
780
+ updatedAt: new Date(),
781
+ });
782
+ for await (const [path, content] of Object.entries(pathesContents))
783
+ await File.replace(path, content, this.salt);
784
+ if (returnPostedData)
785
+ return this.get(tableName, where, options);
786
+ }
787
+ }
788
+ else if ((Array.isArray(where) &&
789
+ (where.every(Utils.isValidID) || where.every(Utils.isNumber))) ||
790
+ Utils.isValidID(where) ||
791
+ Utils.isNumber(where)) {
792
+ if ((Array.isArray(where) && where.every(Utils.isValidID)) ||
793
+ Utils.isValidID(where)) {
794
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
795
+ return this.put(tableName, data, lineNumbers);
796
+ }
797
+ else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
798
+ Utils.isNumber(where)) {
799
+ // "where" in this case, is the line(s) number(s) and not id(s)
800
+ const pathesContents = Object.fromEntries(Object.entries(this.joinPathesContents(join(this.folder, this.database, tableName), Utils.isArrayOfObjects(data)
801
+ ? data.map((item) => ({
802
+ ...item,
803
+ updatedAt: new Date(),
804
+ }))
805
+ : { ...data, updatedAt: new Date() })).map(([key, value]) => [
806
+ key,
807
+ [...(Array.isArray(where) ? where : [where])].reduce((obj, key, index) => ({
808
+ ...obj,
809
+ [key]: Array.isArray(value) ? value[index] : value,
810
+ }), {}),
811
+ ]));
812
+ for await (const [path, content] of Object.entries(pathesContents))
813
+ await File.replace(path, content, this.salt);
814
+ if (returnPostedData)
815
+ return this.get(tableName, where, options, !Array.isArray(where));
816
+ }
817
+ }
818
+ else if (typeof where === "object") {
819
+ const lineNumbers = this.get(tableName, where, undefined, undefined, true);
820
+ if (!lineNumbers || !Array.isArray(lineNumbers) || !lineNumbers.length)
821
+ throw this.throwError("NO_ITEMS", tableName);
822
+ return this.put(tableName, data, lineNumbers);
823
+ }
824
+ else
825
+ throw this.throwError("INVALID_PARAMETERS", tableName);
826
+ }
827
+ async delete(tableName, where, _id) {
828
+ const schema = await this.getTableSchema(tableName);
829
+ if (!schema)
830
+ throw this.throwError("NO_SCHEMA", tableName);
831
+ const idFilePath = join(this.folder, this.database, tableName, "id.inib");
832
+ if (!(await File.isExists(idFilePath)))
833
+ throw this.throwError("NO_ITEMS", tableName);
834
+ if (!where) {
835
+ const files = await readdir(join(this.folder, this.database, tableName));
836
+ if (files.length) {
837
+ for (const file in files.filter((fileName) => fileName !== "schema.json"))
838
+ await unlink(join(this.folder, this.database, tableName, file));
839
+ }
840
+ return "*";
841
+ }
842
+ else if ((Array.isArray(where) &&
843
+ (where.every(Utils.isValidID) || where.every(Utils.isNumber))) ||
844
+ Utils.isValidID(where) ||
845
+ Utils.isNumber(where)) {
846
+ if ((Array.isArray(where) && where.every(Utils.isValidID)) ||
847
+ Utils.isValidID(where)) {
848
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
849
+ return this.delete(tableName, lineNumbers, where);
850
+ }
851
+ else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
852
+ Utils.isNumber(where)) {
853
+ // "where" in this case, is the line(s) number(s) and not id(s)
854
+ const files = await readdir(join(this.folder, this.database, tableName));
855
+ if (files.length) {
856
+ if (!_id)
857
+ _id = Object.values((await File.get(join(this.folder, this.database, tableName, "id.inib"), where, "number", undefined, this.salt))[0]).map((id) => Utils.encodeID(Number(id), this.salt));
858
+ for (const file of files.filter((fileName) => fileName.endsWith(".inib")))
859
+ await File.remove(join(this.folder, this.database, tableName, file), where);
860
+ return Array.isArray(_id) && _id.length === 1 ? _id[0] : _id;
861
+ }
862
+ }
863
+ }
864
+ else if (typeof where === "object" && !Array.isArray(where)) {
865
+ const lineNumbers = this.get(tableName, where, undefined, undefined, true);
866
+ if (!lineNumbers || !Array.isArray(lineNumbers) || !lineNumbers.length)
867
+ throw this.throwError("NO_ITEMS", tableName);
868
+ return this.delete(tableName, lineNumbers);
869
+ }
870
+ else
871
+ throw this.throwError("INVALID_PARAMETERS", tableName);
872
+ return null;
873
+ }
874
+ async sum(tableName, columns, where) {
875
+ let RETURN;
876
+ const schema = await this.getTableSchema(tableName);
877
+ if (!schema)
878
+ throw this.throwError("NO_SCHEMA", tableName);
879
+ if (!(await File.isExists(join(this.folder, this.database, tableName, "id.inib"))))
880
+ throw this.throwError("NO_ITEMS", tableName);
881
+ if (!Array.isArray(columns))
882
+ columns = [columns];
883
+ for await (const column of columns) {
884
+ const columnPath = join(this.folder, this.database, tableName, column + ".inib");
885
+ if (await File.isExists(columnPath)) {
886
+ if (where) {
887
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
888
+ RETURN[column] = await File.sum(columnPath, lineNumbers);
889
+ }
890
+ else
891
+ RETURN[column] = await File.sum(columnPath);
892
+ }
893
+ }
894
+ return Array.isArray(columns) ? RETURN : Object.values(RETURN)[0];
895
+ }
896
+ async max(tableName, columns, where) {
897
+ let RETURN;
898
+ const schema = await this.getTableSchema(tableName);
899
+ if (!schema)
900
+ throw this.throwError("NO_SCHEMA", tableName);
901
+ if (!(await File.isExists(join(this.folder, this.database, tableName, "id.inib"))))
902
+ throw this.throwError("NO_ITEMS", tableName);
903
+ if (!Array.isArray(columns))
904
+ columns = [columns];
905
+ for await (const column of columns) {
906
+ const columnPath = join(this.folder, this.database, tableName, column + ".inib");
907
+ if (await File.isExists(columnPath)) {
908
+ if (where) {
909
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
910
+ RETURN[column] = await File.max(columnPath, lineNumbers);
911
+ }
912
+ else
913
+ RETURN[column] = await File.max(columnPath);
914
+ }
915
+ }
916
+ return RETURN;
917
+ }
918
+ async min(tableName, columns, where) {
919
+ let RETURN;
920
+ const schema = await this.getTableSchema(tableName);
921
+ if (!schema)
922
+ throw this.throwError("NO_SCHEMA", tableName);
923
+ if (!(await File.isExists(join(this.folder, this.database, tableName, "id.inib"))))
924
+ throw this.throwError("NO_ITEMS", tableName);
925
+ if (!Array.isArray(columns))
926
+ columns = [columns];
927
+ for await (const column of columns) {
928
+ const columnPath = join(this.folder, this.database, tableName, column + ".inib");
929
+ if (await File.isExists(columnPath)) {
930
+ if (where) {
931
+ const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
932
+ RETURN[column] = await File.min(columnPath, lineNumbers);
933
+ }
934
+ else
935
+ RETURN[column] = await File.min(columnPath);
936
+ }
937
+ }
938
+ return RETURN;
939
+ }
940
+ }