inibase 1.0.0-rc.120 → 1.0.0-rc.121

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/README.md CHANGED
@@ -726,13 +726,13 @@ await db.get("user", undefined, { sort: {age: -1, username: "asc"} });
726
726
  - [x] Id
727
727
  - [x] JSON
728
728
  - [ ] TO-DO:
729
- - [ ] Use new Map() instead of Object
729
+ - [x] Use new Map() instead of Object
730
730
  - [ ] Ability to search in JSON fields
731
- - [ ] Re-check used exec functions
731
+ - [x] Re-check used exec functions
732
732
  - [ ] Use smart caching (based on N° of queries)
733
733
  - [ ] Commenting the code
734
734
  - [ ] Add Backup feature (generate a tar.gz)
735
- - [ ] Add Custom field validation property to schema (using RegEx?)
735
+ - [x] Add Custom field validation property to schema (using RegEx?)
736
736
  - [ ] Features:
737
737
  - [ ] Encryption
738
738
  - [x] Data Compression
package/dist/file.js CHANGED
@@ -542,7 +542,7 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
542
542
  // Increment the line count for each line.
543
543
  linesCount++;
544
544
  // Search only in provided linesNumbers
545
- if (searchIn && !searchIn.has(linesCount))
545
+ if ((searchIn && !searchIn.has(linesCount)) || searchIn.has(-linesCount))
546
546
  continue;
547
547
  // Decode the line for comparison.
548
548
  const decodedLine = decode(line, fieldType, fieldChildrenType, secretKey);
package/dist/index.d.ts CHANGED
@@ -12,8 +12,9 @@ export type Field = {
12
12
  type: FieldType | FieldType[];
13
13
  required?: boolean;
14
14
  table?: string;
15
- unique?: boolean;
15
+ unique?: boolean | number | string;
16
16
  children?: FieldType | FieldType[] | Schema;
17
+ regex?: string;
17
18
  };
18
19
  export type Schema = Field[];
19
20
  export interface Options {
@@ -49,18 +50,19 @@ declare global {
49
50
  entries<T extends object>(o: T): Entries<T>;
50
51
  }
51
52
  }
52
- export type ErrorCodes = "FIELD_UNIQUE" | "FIELD_REQUIRED" | "NO_SCHEMA" | "TABLE_EMPTY" | "INVALID_ID" | "INVALID_TYPE" | "INVALID_PARAMETERS" | "NO_ENV" | "TABLE_EXISTS" | "TABLE_NOT_EXISTS";
53
+ export type ErrorCodes = "FIELD_UNIQUE" | "FIELD_REQUIRED" | "NO_SCHEMA" | "TABLE_EMPTY" | "INVALID_ID" | "INVALID_TYPE" | "INVALID_PARAMETERS" | "NO_ENV" | "TABLE_EXISTS" | "TABLE_NOT_EXISTS" | "INVALID_REGEX_MATCH";
53
54
  export type ErrorLang = "en";
54
55
  export default class Inibase {
55
56
  pageInfo: Record<string, pageInfo>;
56
57
  salt: Buffer;
57
58
  private databasePath;
58
- private tables;
59
59
  private fileExtension;
60
- private checkIFunique;
60
+ private tablesMap;
61
+ private uniqueMap;
61
62
  private totalItems;
62
63
  constructor(database: string, mainFolder?: string);
63
- private Error;
64
+ private static errorMessages;
65
+ createError(code: ErrorCodes, variable?: string | number | (string | number)[], language?: ErrorLang): Error;
64
66
  clear(): void;
65
67
  private getFileExtension;
66
68
  private _schemaToIdsPath;
@@ -89,13 +91,15 @@ export default class Inibase {
89
91
  * @param {string} tableName
90
92
  * @return {*} {Promise<TableObject>}
91
93
  */
92
- getTable(tableName: string): Promise<TableObject>;
94
+ getTable(tableName: string, encodeIDs?: boolean): Promise<TableObject>;
93
95
  getTableSchema(tableName: string, encodeIDs?: boolean): Promise<Schema | undefined>;
94
96
  private throwErrorIfTableEmpty;
97
+ private _validateData;
95
98
  private validateData;
96
99
  private cleanObject;
97
100
  private formatField;
98
101
  private checkUnique;
102
+ private _formatData;
99
103
  private formatData;
100
104
  private getDefaultValue;
101
105
  private _combineObjectsToArray;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "dotenv/config";
2
- import { randomBytes, scryptSync } from "node:crypto";
2
+ import { randomBytes, randomInt, scryptSync } from "node:crypto";
3
3
  import { appendFileSync, existsSync, readFileSync } from "node:fs";
4
4
  import { glob, mkdir, readFile, readdir, rename, rm, unlink, writeFile, } from "node:fs/promises";
5
5
  import { join, parse } from "node:path";
@@ -14,45 +14,45 @@ export default class Inibase {
14
14
  pageInfo;
15
15
  salt;
16
16
  databasePath;
17
- tables;
18
17
  fileExtension = ".txt";
19
- checkIFunique;
18
+ tablesMap;
19
+ uniqueMap;
20
20
  totalItems;
21
21
  constructor(database, mainFolder = ".") {
22
22
  this.databasePath = join(mainFolder, database);
23
- this.tables = {};
24
- this.totalItems = {};
23
+ this.tablesMap = new Map();
24
+ this.totalItems = new Map();
25
25
  this.pageInfo = {};
26
- this.checkIFunique = {};
26
+ this.uniqueMap = new Map();
27
27
  if (!process.env.INIBASE_SECRET) {
28
28
  if (existsSync(".env") &&
29
29
  readFileSync(".env").includes("INIBASE_SECRET="))
30
- throw this.Error("NO_ENV");
30
+ throw this.createError("NO_ENV");
31
31
  this.salt = scryptSync(randomBytes(16), randomBytes(16), 32);
32
32
  appendFileSync(".env", `\nINIBASE_SECRET=${this.salt.toString("hex")}\n`);
33
33
  }
34
34
  else
35
35
  this.salt = Buffer.from(process.env.INIBASE_SECRET, "hex");
36
36
  }
37
- Error(code, variable, language = "en") {
38
- const errorMessages = {
39
- en: {
40
- TABLE_EMPTY: "Table {variable} is empty",
41
- TABLE_EXISTS: "Table {variable} already exists",
42
- TABLE_NOT_EXISTS: "Table {variable} doesn't exist",
43
- NO_SCHEMA: "Table {variable} does't have a schema",
44
- FIELD_UNIQUE: "Field {variable} should be unique, got {variable} instead",
45
- FIELD_REQUIRED: "Field {variable} is required",
46
- INVALID_ID: "The given ID(s) is/are not valid(s)",
47
- INVALID_TYPE: "Expect {variable} to be {variable}, got {variable} instead",
48
- INVALID_PARAMETERS: "The given parameters are not valid",
49
- NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
50
- ? "please run with '--env-file=.env'"
51
- : "please use dotenv",
52
- },
53
- // Add more languages and error messages as needed
54
- };
55
- const errorMessage = errorMessages[language][code];
37
+ static errorMessages = {
38
+ en: {
39
+ TABLE_EMPTY: "Table {variable} is empty",
40
+ TABLE_EXISTS: "Table {variable} already exists",
41
+ TABLE_NOT_EXISTS: "Table {variable} doesn't exist",
42
+ NO_SCHEMA: "Table {variable} does't have a schema",
43
+ FIELD_UNIQUE: "Field {variable} should be unique, got {variable} instead",
44
+ FIELD_REQUIRED: "Field {variable} is required",
45
+ INVALID_ID: "The given ID(s) is/are not valid(s)",
46
+ INVALID_TYPE: "Expect {variable} to be {variable}, got {variable} instead",
47
+ INVALID_PARAMETERS: "The given parameters are not valid",
48
+ INVALID_REGEX_MATCH: "Field {variable} does not match the expected pattern",
49
+ NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
50
+ ? "please run with '--env-file=.env'"
51
+ : "please use dotenv",
52
+ },
53
+ };
54
+ createError(code, variable, language = "en") {
55
+ const errorMessage = Inibase.errorMessages[language]?.[code];
56
56
  if (!errorMessage)
57
57
  return new Error("ERR");
58
58
  return new Error(variable
@@ -62,17 +62,17 @@ export default class Inibase {
62
62
  : errorMessage.replaceAll("{variable}", ""));
63
63
  }
64
64
  clear() {
65
- this.tables = {};
66
- this.totalItems = {};
65
+ this.tablesMap = new Map();
66
+ this.totalItems = new Map();
67
67
  this.pageInfo = {};
68
- this.checkIFunique = {};
68
+ this.uniqueMap = new Map();
69
69
  }
70
70
  getFileExtension(tableName) {
71
71
  let mainExtension = this.fileExtension;
72
72
  // TODO: ADD ENCRYPTION
73
- // if(this.tables[tableName].config.encryption)
73
+ // if(this.tablesMap.get(tableName).config.encryption)
74
74
  // mainExtension += ".enc"
75
- if (this.tables[tableName].config.compression)
75
+ if (this.tablesMap.get(tableName).config.compression)
76
76
  mainExtension += ".gz";
77
77
  return mainExtension;
78
78
  }
@@ -99,7 +99,7 @@ export default class Inibase {
99
99
  async createTable(tableName, schema, config) {
100
100
  const tablePath = join(this.databasePath, tableName);
101
101
  if (await File.isExists(tablePath))
102
- throw this.Error("TABLE_EXISTS", tableName);
102
+ throw this.createError("TABLE_EXISTS", tableName);
103
103
  await mkdir(join(tablePath, ".tmp"), { recursive: true });
104
104
  await mkdir(join(tablePath, ".cache"));
105
105
  // if config not set => load default global env config
@@ -142,7 +142,8 @@ export default class Inibase {
142
142
  * @param {(Config&{name?: string})} [config]
143
143
  */
144
144
  async updateTable(tableName, schema, config) {
145
- const table = await this.getTable(tableName), tablePath = join(this.databasePath, tableName);
145
+ const table = await this.getTable(tableName);
146
+ const tablePath = join(this.databasePath, tableName);
146
147
  if (schema) {
147
148
  // remove id from schema
148
149
  schema = schema.filter(({ key }) => !["id", "createdAt", "updatedAt"].includes(key));
@@ -232,7 +233,7 @@ export default class Inibase {
232
233
  await this.replaceStringInFile(schemaPath, `"table": "${tableName}"`, `"table": "${config.name}"`);
233
234
  }
234
235
  }
235
- delete this.tables[tableName];
236
+ this.tablesMap.delete(tableName);
236
237
  }
237
238
  /**
238
239
  * Get table schema and config
@@ -240,20 +241,20 @@ export default class Inibase {
240
241
  * @param {string} tableName
241
242
  * @return {*} {Promise<TableObject>}
242
243
  */
243
- async getTable(tableName) {
244
+ async getTable(tableName, encodeIDs = true) {
244
245
  const tablePath = join(this.databasePath, tableName);
245
246
  if (!(await File.isExists(tablePath)))
246
- throw this.Error("TABLE_NOT_EXISTS", tableName);
247
- if (!this.tables[tableName])
248
- this.tables[tableName] = {
249
- schema: await this.getTableSchema(tableName),
247
+ throw this.createError("TABLE_NOT_EXISTS", tableName);
248
+ if (!this.tablesMap.has(tableName))
249
+ this.tablesMap.set(tableName, {
250
+ schema: await this.getTableSchema(tableName, encodeIDs),
250
251
  config: {
251
252
  compression: await File.isExists(join(tablePath, ".compression.config")),
252
253
  cache: await File.isExists(join(tablePath, ".cache.config")),
253
254
  prepend: await File.isExists(join(tablePath, ".prepend.config")),
254
255
  },
255
- };
256
- return this.tables[tableName];
256
+ });
257
+ return this.tablesMap.get(tableName);
257
258
  }
258
259
  async getTableSchema(tableName, encodeIDs = true) {
259
260
  const tablePath = join(this.databasePath, tableName);
@@ -288,17 +289,16 @@ export default class Inibase {
288
289
  return UtilsServer.encodeSchemaID(schema, this.salt);
289
290
  }
290
291
  async throwErrorIfTableEmpty(tableName) {
291
- const table = await this.getTable(tableName);
292
+ const table = await this.getTable(tableName, false);
292
293
  if (!table.schema)
293
- throw this.Error("NO_SCHEMA", tableName);
294
+ throw this.createError("NO_SCHEMA", tableName);
294
295
  if (!(await File.isExists(join(this.databasePath, tableName, `id${this.getFileExtension(tableName)}`))))
295
- throw this.Error("TABLE_EMPTY", tableName);
296
- return table;
296
+ throw this.createError("TABLE_EMPTY", tableName);
297
297
  }
298
- validateData(data, schema, skipRequiredField = false) {
298
+ _validateData(data, schema, skipRequiredField = false) {
299
299
  if (Utils.isArrayOfObjects(data))
300
300
  for (const single_data of data)
301
- this.validateData(single_data, schema, skipRequiredField);
301
+ this._validateData(single_data, schema, skipRequiredField);
302
302
  else if (Utils.isObject(data)) {
303
303
  for (const field of schema) {
304
304
  if (!Object.hasOwn(data, field.key) ||
@@ -306,7 +306,7 @@ export default class Inibase {
306
306
  data[field.key] === undefined ||
307
307
  data[field.key] === "") {
308
308
  if (field.required && !skipRequiredField)
309
- throw this.Error("FIELD_REQUIRED", field.key);
309
+ throw this.createError("FIELD_REQUIRED", field.key);
310
310
  return;
311
311
  }
312
312
  if (!Utils.validateFieldType(data[field.key], field.type, (field.type === "array" || field.type === "object") &&
@@ -314,7 +314,7 @@ export default class Inibase {
314
314
  !Utils.isArrayOfObjects(field.children)
315
315
  ? field.children
316
316
  : undefined))
317
- throw this.Error("INVALID_TYPE", [
317
+ throw this.createError("INVALID_TYPE", [
318
318
  field.key,
319
319
  Array.isArray(field.type) ? field.type.join(", ") : field.type,
320
320
  data[field.key],
@@ -322,15 +322,46 @@ export default class Inibase {
322
322
  if ((field.type === "array" || field.type === "object") &&
323
323
  field.children &&
324
324
  Utils.isArrayOfObjects(field.children))
325
- this.validateData(data[field.key], field.children, skipRequiredField);
326
- else if (field.unique) {
327
- if (!this.checkIFunique[field.key])
328
- this.checkIFunique[field.key] = [];
329
- this.checkIFunique[`${field.key}`].push(data[field.key]);
325
+ this._validateData(data[field.key], field.children, skipRequiredField);
326
+ else {
327
+ if (field.regex) {
328
+ const regex = UtilsServer.getCachedRegex(field.regex);
329
+ if (!regex.test(data[field.key]))
330
+ throw this.createError("INVALID_REGEX_MATCH", [field.key]);
331
+ }
332
+ if (field.unique) {
333
+ let uniqueKey;
334
+ if (typeof field.unique === "boolean")
335
+ uniqueKey = randomInt(55);
336
+ else
337
+ uniqueKey = field.unique;
338
+ if (!this.uniqueMap.has(uniqueKey))
339
+ this.uniqueMap.set(uniqueKey, {
340
+ exclude: new Set(),
341
+ columnsValues: new Map(),
342
+ });
343
+ if (!this.uniqueMap
344
+ .get(uniqueKey)
345
+ .columnsValues.has(field.id))
346
+ this.uniqueMap
347
+ .get(uniqueKey)
348
+ .columnsValues.set(field.id, new Set());
349
+ if (data.id)
350
+ this.uniqueMap.get(uniqueKey).exclude.add(-data.id);
351
+ this.uniqueMap
352
+ .get(uniqueKey)
353
+ .columnsValues.get(field.id)
354
+ .add(data[field.key]);
355
+ }
330
356
  }
331
357
  }
332
358
  }
333
359
  }
360
+ async validateData(tableName, data, skipRequiredField = false) {
361
+ // Skip ID and (created|updated)At
362
+ this._validateData(data, this.tablesMap.get(tableName).schema.slice(1, -2), skipRequiredField);
363
+ await this.checkUnique(tableName);
364
+ }
334
365
  cleanObject(obj) {
335
366
  const cleanedObject = Object.entries(obj).reduce((acc, [key, value]) => {
336
367
  if (value !== undefined && value !== null && value !== "")
@@ -353,13 +384,13 @@ export default class Inibase {
353
384
  if (!Array.isArray(value))
354
385
  value = [value];
355
386
  if (Utils.isArrayOfObjects(fieldChildrenType))
356
- return this.formatData(value, fieldChildrenType);
387
+ return this._formatData(value, fieldChildrenType);
357
388
  if (!value.length)
358
389
  return null;
359
390
  return value.map((_value) => this.formatField(_value, fieldChildrenType));
360
391
  case "object":
361
392
  if (Utils.isArrayOfObjects(fieldChildrenType))
362
- return this.formatData(value, fieldChildrenType, _formatOnlyAvailiableKeys);
393
+ return this._formatData(value, fieldChildrenType, _formatOnlyAvailiableKeys);
363
394
  break;
364
395
  case "table":
365
396
  if (Utils.isObject(value)) {
@@ -402,25 +433,30 @@ export default class Inibase {
402
433
  }
403
434
  return null;
404
435
  }
405
- async checkUnique(tableName, schema) {
436
+ async checkUnique(tableName) {
406
437
  const tablePath = join(this.databasePath, tableName);
407
- for await (const [key, values] of Object.entries(this.checkIFunique)) {
408
- const field = Utils.getField(key, schema);
409
- if (!field)
410
- continue;
411
- const [searchResult, totalLines] = await File.search(join(tablePath, `${key}${this.getFileExtension(tableName)}`), Array.isArray(values) ? "=" : "[]", values, undefined, undefined, field.type, field.children, 1, undefined, false, this.salt);
412
- if (searchResult && totalLines > 0)
413
- throw this.Error("FIELD_UNIQUE", [
414
- field.key,
415
- Array.isArray(values) ? values.join(", ") : values,
416
- ]);
438
+ const flattenSchema = Utils.flattenSchema(this.tablesMap.get(tableName).schema);
439
+ for await (const [_uniqueID, valueObject] of this.uniqueMap) {
440
+ let valueExisted = false;
441
+ for await (const [columnID, values] of valueObject.columnsValues) {
442
+ const field = flattenSchema.find(({ id }) => id === columnID);
443
+ const [searchResult, totalLines] = await File.search(join(tablePath, `${columnID}${this.getFileExtension(tableName)}`), "[]", values, undefined, valueObject.exclude, field.type, field.children, 1, undefined, false, this.salt);
444
+ if (searchResult && totalLines > 0) {
445
+ if (valueExisted)
446
+ throw this.createError("FIELD_UNIQUE", [
447
+ field.key,
448
+ Array.isArray(values) ? values.join(", ") : values,
449
+ ]);
450
+ valueExisted = true;
451
+ }
452
+ }
417
453
  }
418
- this.checkIFunique = {};
454
+ this.uniqueMap = new Map();
419
455
  }
420
- formatData(data, schema, formatOnlyAvailiableKeys) {
456
+ _formatData(data, schema, formatOnlyAvailiableKeys) {
421
457
  const clonedData = JSON.parse(JSON.stringify(data));
422
458
  if (Utils.isArrayOfObjects(clonedData))
423
- return clonedData.map((singleData) => this.formatData(singleData, schema, formatOnlyAvailiableKeys));
459
+ return clonedData.map((singleData) => this._formatData(singleData, schema, formatOnlyAvailiableKeys));
424
460
  if (Utils.isObject(clonedData)) {
425
461
  const RETURN = {};
426
462
  for (const field of schema) {
@@ -436,6 +472,9 @@ export default class Inibase {
436
472
  }
437
473
  return [];
438
474
  }
475
+ formatData(tableName, data, formatOnlyAvailiableKeys) {
476
+ data = this._formatData(data, this.tablesMap.get(tableName).schema, formatOnlyAvailiableKeys);
477
+ }
439
478
  getDefaultValue(field) {
440
479
  if (Array.isArray(field.type))
441
480
  return this.getDefaultValue({
@@ -835,7 +874,7 @@ export default class Inibase {
835
874
  [key]: value,
836
875
  },
837
876
  ])));
838
- this.totalItems[`${tableName}-${key}`] = totalLines;
877
+ this.totalItems.set(`${tableName}-${key}`, totalLines);
839
878
  if (linesNumbers?.size) {
840
879
  if (searchIn) {
841
880
  for (const lineNumber of linesNumbers)
@@ -911,9 +950,10 @@ export default class Inibase {
911
950
  options.page = options.page || 1;
912
951
  options.perPage = options.perPage || 15;
913
952
  let RETURN;
953
+ await this.getTable(tableName);
914
954
  let schema = (await this.getTable(tableName)).schema;
915
955
  if (!schema)
916
- throw this.Error("NO_SCHEMA", tableName);
956
+ throw this.createError("NO_SCHEMA", tableName);
917
957
  let pagination;
918
958
  for await (const paginationFileName of glob("*.pagination", {
919
959
  cwd: tablePath,
@@ -942,7 +982,7 @@ export default class Inibase {
942
982
  .map((column) => [column, true]);
943
983
  let cacheKey = "";
944
984
  // Criteria
945
- if (this.tables[tableName].config.cache)
985
+ if (this.tablesMap.get(tableName).config.cache)
946
986
  cacheKey = UtilsServer.hashString(inspect(sortArray, { sorted: true }));
947
987
  if (where) {
948
988
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
@@ -980,7 +1020,7 @@ export default class Inibase {
980
1020
  if (cacheKey)
981
1021
  await File.lock(join(tablePath, ".tmp"), cacheKey);
982
1022
  // Combine && Execute the commands synchronously
983
- let lines = (await UtilsServer.exec(this.tables[tableName].config.cache
1023
+ let lines = (await UtilsServer.exec(this.tablesMap.get(tableName).config.cache
984
1024
  ? (await File.isExists(join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)))
985
1025
  ? `${awkCommand} '${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)}'`
986
1026
  : `${pasteCommand} | ${sortCommand} -o '${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)}' && ${awkCommand} '${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)}'`
@@ -991,8 +1031,8 @@ export default class Inibase {
991
1031
  .split("\n");
992
1032
  if (where)
993
1033
  lines = lines.slice((options.page - 1) * options.perPage, options.page * options.perPage);
994
- else if (!this.totalItems[`${tableName}-*`])
995
- this.totalItems[`${tableName}-*`] = pagination[1];
1034
+ else if (!this.totalItems.has(`${tableName}-*`))
1035
+ this.totalItems.set(`${tableName}-*`, pagination[1]);
996
1036
  if (!lines.length)
997
1037
  return null;
998
1038
  // Parse the result and extract the specified lines
@@ -1025,8 +1065,8 @@ export default class Inibase {
1025
1065
  RETURN = Object.values(await this.processSchemaData(tableName, schema, Array.from({ length: options.perPage }, (_, index) => (options.page - 1) * options.perPage +
1026
1066
  index +
1027
1067
  1), options));
1028
- if (!this.totalItems[`${tableName}-*`])
1029
- this.totalItems[`${tableName}-*`] = pagination[1];
1068
+ if (!this.totalItems.has(`${tableName}-*`))
1069
+ this.totalItems.set(`${tableName}-*`, pagination[1]);
1030
1070
  }
1031
1071
  else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1032
1072
  Utils.isNumber(where)) {
@@ -1034,8 +1074,8 @@ export default class Inibase {
1034
1074
  let lineNumbers = where;
1035
1075
  if (!Array.isArray(lineNumbers))
1036
1076
  lineNumbers = [lineNumbers];
1037
- if (!this.totalItems[`${tableName}-*`])
1038
- this.totalItems[`${tableName}-*`] = lineNumbers.length;
1077
+ if (!this.totalItems.has(`${tableName}-*`))
1078
+ this.totalItems.set(`${tableName}-*`, lineNumbers.length);
1039
1079
  // useless
1040
1080
  if (onlyLinesNumbers)
1041
1081
  return lineNumbers;
@@ -1048,11 +1088,11 @@ export default class Inibase {
1048
1088
  let Ids = where;
1049
1089
  if (!Array.isArray(Ids))
1050
1090
  Ids = [Ids];
1051
- const [lineNumbers, countItems] = await File.search(join(tablePath, `id${this.getFileExtension(tableName)}`), "[]", Ids.map((id) => Utils.isNumber(id) ? Number(id) : UtilsServer.decodeID(id, this.salt)), undefined, undefined, "number", undefined, Ids.length, 0, !this.totalItems[`${tableName}-*`], this.salt);
1091
+ const [lineNumbers, countItems] = await File.search(join(tablePath, `id${this.getFileExtension(tableName)}`), "[]", Ids.map((id) => Utils.isNumber(id) ? Number(id) : UtilsServer.decodeID(id, this.salt)), undefined, undefined, "number", undefined, Ids.length, 0, !this.totalItems.has(`${tableName}-*`), this.salt);
1052
1092
  if (!lineNumbers)
1053
1093
  return null;
1054
- if (!this.totalItems[`${tableName}-*`])
1055
- this.totalItems[`${tableName}-*`] = countItems;
1094
+ if (!this.totalItems.has(`${tableName}-*`))
1095
+ this.totalItems.set(`${tableName}-*`, countItems);
1056
1096
  if (onlyLinesNumbers)
1057
1097
  return Object.keys(lineNumbers).length
1058
1098
  ? Object.keys(lineNumbers).map(Number)
@@ -1069,13 +1109,13 @@ export default class Inibase {
1069
1109
  else if (Utils.isObject(where)) {
1070
1110
  let cachedFilePath = "";
1071
1111
  // Criteria
1072
- if (this.tables[tableName].config.cache)
1112
+ if (this.tablesMap.get(tableName).config.cache)
1073
1113
  cachedFilePath = join(tablePath, ".cache", `${UtilsServer.hashString(inspect(where, { sorted: true }))}${this.fileExtension}`);
1074
- if (this.tables[tableName].config.cache &&
1114
+ if (this.tablesMap.get(tableName).config.cache &&
1075
1115
  (await File.isExists(cachedFilePath))) {
1076
1116
  const cachedItems = (await readFile(cachedFilePath, "utf8")).split(",");
1077
- if (!this.totalItems[`${tableName}-*`])
1078
- this.totalItems[`${tableName}-*`] = cachedItems.length;
1117
+ if (!this.totalItems.has(`${tableName}-*`))
1118
+ this.totalItems.set(`${tableName}-*`, cachedItems.length);
1079
1119
  if (onlyLinesNumbers)
1080
1120
  return onlyOne ? Number(cachedItems[0]) : cachedItems.map(Number);
1081
1121
  return this.get(tableName, cachedItems
@@ -1085,8 +1125,8 @@ export default class Inibase {
1085
1125
  let linesNumbers = null;
1086
1126
  [RETURN, linesNumbers] = await this.applyCriteria(tableName, schema, options, where);
1087
1127
  if (RETURN && linesNumbers?.size) {
1088
- if (!this.totalItems[`${tableName}-*`])
1089
- this.totalItems[`${tableName}-*`] = linesNumbers.size;
1128
+ if (!this.totalItems.has(`${tableName}-*`))
1129
+ this.totalItems.set(`${tableName}-*`, linesNumbers.size);
1090
1130
  if (onlyLinesNumbers)
1091
1131
  return onlyOne
1092
1132
  ? linesNumbers.values().next().value
@@ -1096,7 +1136,7 @@ export default class Inibase {
1096
1136
  .map(({ id }) => id);
1097
1137
  RETURN = Object.values(Utils.deepMerge(RETURN, await this.processSchemaData(tableName, Utils.filterSchema(schema, ({ id, type, children }) => !alreadyExistsColumnsIDs.includes(id) ||
1098
1138
  Utils.isFieldType("table", type, children)), Object.keys(RETURN).map(Number), options)));
1099
- if (this.tables[tableName].config.cache)
1139
+ if (this.tablesMap.get(tableName).config.cache)
1100
1140
  await writeFile(cachedFilePath, Array.from(linesNumbers).join(","));
1101
1141
  }
1102
1142
  }
@@ -1104,8 +1144,9 @@ export default class Inibase {
1104
1144
  (Utils.isObject(RETURN) && !Object.keys(RETURN).length) ||
1105
1145
  (Array.isArray(RETURN) && !RETURN.length))
1106
1146
  return null;
1107
- const greatestTotalItems = this.totalItems[`${tableName}-*`] ??
1108
- Math.max(...Object.entries(this.totalItems)
1147
+ const greatestTotalItems = this.totalItems.has(`${tableName}-*`)
1148
+ ? this.totalItems.get(`${tableName}-*`)
1149
+ : Math.max(...[...this.totalItems.entries()]
1109
1150
  .filter(([k]) => k.startsWith(`${tableName}-`))
1110
1151
  .map(([, v]) => v));
1111
1152
  this.pageInfo[tableName] = {
@@ -1122,24 +1163,24 @@ export default class Inibase {
1122
1163
  page: 1,
1123
1164
  perPage: 15,
1124
1165
  };
1125
- const tablePath = join(this.databasePath, tableName), schema = (await this.getTable(tableName)).schema;
1126
- if (!schema)
1127
- throw this.Error("NO_SCHEMA", tableName);
1166
+ const tablePath = join(this.databasePath, tableName);
1167
+ await this.getTable(tableName);
1168
+ if (!this.tablesMap.get(tableName).schema)
1169
+ throw this.createError("NO_SCHEMA", tableName);
1128
1170
  if (!returnPostedData)
1129
1171
  returnPostedData = false;
1130
1172
  const keys = UtilsServer.hashString(Object.keys(Array.isArray(data) ? data[0] : data).join("."));
1131
- // Skip ID and (created|updated)At
1132
- this.validateData(data, schema.slice(1, -2));
1133
- let lastId = 0;
1173
+ await this.validateData(tableName, data);
1134
1174
  const renameList = [];
1135
1175
  try {
1136
1176
  await File.lock(join(tablePath, ".tmp"), keys);
1137
1177
  let paginationFilePath;
1138
1178
  for await (const fileName of glob("*.pagination", { cwd: tablePath }))
1139
1179
  paginationFilePath = join(tablePath, fileName);
1140
- [lastId, this.totalItems[`${tableName}-*`]] = parse(paginationFilePath)
1180
+ let [lastId, _totalItems] = parse(paginationFilePath)
1141
1181
  .name.split("-")
1142
1182
  .map(Number);
1183
+ this.totalItems.set(`${tableName}-*`, _totalItems);
1143
1184
  if (Utils.isArrayOfObjects(data))
1144
1185
  for (let index = 0; index < data.length; index++) {
1145
1186
  const element = data[index];
@@ -1152,35 +1193,33 @@ export default class Inibase {
1152
1193
  data.createdAt = Date.now();
1153
1194
  data.updatedAt = undefined;
1154
1195
  }
1155
- await this.checkUnique(tableName, schema);
1156
- data = this.formatData(data, schema);
1157
- const pathesContents = this.joinPathesContents(tableName, this.tables[tableName].config.prepend
1196
+ this.formatData(tableName, data);
1197
+ const pathesContents = this.joinPathesContents(tableName, this.tablesMap.get(tableName).config.prepend
1158
1198
  ? Array.isArray(data)
1159
1199
  ? data.toReversed()
1160
1200
  : data
1161
1201
  : data);
1162
- await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(this.tables[tableName].config.prepend
1202
+ await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(this.tablesMap.get(tableName).config.prepend
1163
1203
  ? await File.prepend(path, content)
1164
1204
  : await File.append(path, content))));
1165
1205
  await Promise.allSettled(renameList
1166
1206
  .filter(([_, filePath]) => filePath)
1167
1207
  .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1168
- if (this.tables[tableName].config.cache)
1208
+ if (this.tablesMap.get(tableName).config.cache)
1169
1209
  await this.clearCache(tableName);
1170
- this.totalItems[`${tableName}-*`] += Array.isArray(data)
1171
- ? data.length
1172
- : 1;
1173
- await rename(paginationFilePath, join(tablePath, `${lastId}-${this.totalItems[`${tableName}-*`]}.pagination`));
1210
+ const currentValue = this.totalItems.get(`${tableName}-*`) || 0;
1211
+ this.totalItems.set(`${tableName}-*`, currentValue + (Array.isArray(data) ? data.length : 1));
1212
+ await rename(paginationFilePath, join(tablePath, `${lastId}-${this.totalItems.get(`${tableName}-*`)}.pagination`));
1174
1213
  if (returnPostedData)
1175
- return this.get(tableName, this.tables[tableName].config.prepend
1214
+ return this.get(tableName, this.tablesMap.get(tableName).config.prepend
1176
1215
  ? Array.isArray(data)
1177
1216
  ? data.map((_, index) => index + 1).toReversed()
1178
1217
  : 1
1179
1218
  : Array.isArray(data)
1180
1219
  ? data
1181
- .map((_, index) => this.totalItems[`${tableName}-*`] - index)
1220
+ .map((_, index) => this.totalItems.get(`${tableName}-*`) - index)
1182
1221
  .toReversed()
1183
- : this.totalItems[`${tableName}-*`], options, !Utils.isArrayOfObjects(data));
1222
+ : this.totalItems.get(`${tableName}-*`), options, !Utils.isArrayOfObjects(data));
1184
1223
  }
1185
1224
  finally {
1186
1225
  if (renameList.length)
@@ -1194,23 +1233,20 @@ export default class Inibase {
1194
1233
  }, returnUpdatedData) {
1195
1234
  const renameList = [];
1196
1235
  const tablePath = join(this.databasePath, tableName);
1197
- const schema = (await this.throwErrorIfTableEmpty(tableName))
1198
- .schema;
1236
+ await this.throwErrorIfTableEmpty(tableName);
1199
1237
  if (!where) {
1200
1238
  if (Utils.isArrayOfObjects(data)) {
1201
1239
  if (!data.every((item) => Object.hasOwn(item, "id") && Utils.isValidID(item.id)))
1202
- throw this.Error("INVALID_ID");
1240
+ throw this.createError("INVALID_ID");
1203
1241
  return this.put(tableName, data, data.map(({ id }) => id), options, returnUpdatedData || undefined);
1204
1242
  }
1205
1243
  if (Object.hasOwn(data, "id")) {
1206
1244
  if (!Utils.isValidID(data.id))
1207
- throw this.Error("INVALID_ID", data.id);
1245
+ throw this.createError("INVALID_ID", data.id);
1208
1246
  return this.put(tableName, data, data.id, options, returnUpdatedData || undefined);
1209
1247
  }
1210
- // Skip ID and (created|updated)At
1211
- this.validateData(data, schema.slice(1, -2), true);
1212
- await this.checkUnique(tableName, schema);
1213
- data = this.formatData(data, schema, true);
1248
+ await this.validateData(tableName, data, true);
1249
+ this.formatData(tableName, data, true);
1214
1250
  const pathesContents = this.joinPathesContents(tableName, {
1215
1251
  ...(({ id, ...restOfData }) => restOfData)(data),
1216
1252
  updatedAt: Date.now(),
@@ -1220,14 +1256,12 @@ export default class Inibase {
1220
1256
  for await (const paginationFileName of glob("*.pagination", {
1221
1257
  cwd: tablePath,
1222
1258
  }))
1223
- this.totalItems[`${tableName}-*`] = parse(paginationFileName)
1224
- .name.split("-")
1225
- .map(Number)[1];
1226
- await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content, this.totalItems[`${tableName}-*`]))));
1259
+ this.totalItems.set(`${tableName}-*`, parse(paginationFileName).name.split("-").map(Number)[1]);
1260
+ await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content, this.totalItems.get(`${tableName}-*`)))));
1227
1261
  await Promise.allSettled(renameList
1228
1262
  .filter(([_, filePath]) => filePath)
1229
1263
  .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1230
- if (this.tables[tableName].config.cache)
1264
+ if (this.tablesMap.get(tableName).config.cache)
1231
1265
  await this.clearCache(join(tablePath, ".cache"));
1232
1266
  if (returnUpdatedData)
1233
1267
  return await this.get(tableName, undefined, options);
@@ -1246,9 +1280,8 @@ export default class Inibase {
1246
1280
  else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1247
1281
  Utils.isNumber(where)) {
1248
1282
  // "where" in this case, is the line(s) number(s) and not id(s)
1249
- this.validateData(data, schema.slice(1, -2), true);
1250
- await this.checkUnique(tableName, schema.slice(1, -2));
1251
- data = this.formatData(data, schema, true);
1283
+ await this.validateData(tableName, data, true);
1284
+ this.formatData(tableName, data, true);
1252
1285
  const pathesContents = Object.fromEntries(Object.entries(this.joinPathesContents(tableName, Utils.isArrayOfObjects(data)
1253
1286
  ? data.map((item) => ({
1254
1287
  ...item,
@@ -1269,7 +1302,7 @@ export default class Inibase {
1269
1302
  await Promise.allSettled(renameList
1270
1303
  .filter(([_, filePath]) => filePath)
1271
1304
  .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1272
- if (this.tables[tableName].config.cache)
1305
+ if (this.tablesMap.get(tableName).config.cache)
1273
1306
  await this.clearCache(tableName);
1274
1307
  if (returnUpdatedData)
1275
1308
  return this.get(tableName, where, options, !Array.isArray(where));
@@ -1286,7 +1319,7 @@ export default class Inibase {
1286
1319
  return this.put(tableName, data, lineNumbers, options, returnUpdatedData || undefined);
1287
1320
  }
1288
1321
  else
1289
- throw this.Error("INVALID_PARAMETERS");
1322
+ throw this.createError("INVALID_PARAMETERS");
1290
1323
  }
1291
1324
  /**
1292
1325
  * Delete item(s) in a table
@@ -1314,7 +1347,7 @@ export default class Inibase {
1314
1347
  await Promise.all((await readdir(tablePath))
1315
1348
  ?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)))
1316
1349
  .map(async (file) => unlink(join(tablePath, file))));
1317
- if (this.tables[tableName].config.cache)
1350
+ if (this.tablesMap.get(tableName).config.cache)
1318
1351
  await this.clearCache(tableName);
1319
1352
  await rename(paginationFilePath, join(tablePath, `${pagination[0]}-0.pagination`));
1320
1353
  return true;
@@ -1357,7 +1390,7 @@ export default class Inibase {
1357
1390
  await Promise.all((await readdir(tablePath))
1358
1391
  ?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)))
1359
1392
  .map(async (file) => unlink(join(tablePath, file))));
1360
- if (this.tables[tableName].config.cache)
1393
+ if (this.tablesMap.get(tableName).config.cache)
1361
1394
  await this.clearCache(tableName);
1362
1395
  await rename(paginationFilePath, join(tablePath, `${pagination[0]}-${pagination[1] - (Array.isArray(where) ? where.length : 1)}.pagination`));
1363
1396
  return true;
@@ -1375,7 +1408,7 @@ export default class Inibase {
1375
1408
  return this.delete(tableName, lineNumbers);
1376
1409
  }
1377
1410
  else
1378
- throw this.Error("INVALID_PARAMETERS");
1411
+ throw this.createError("INVALID_PARAMETERS");
1379
1412
  return false;
1380
1413
  }
1381
1414
  async sum(tableName, columns, where) {
@@ -1,6 +1,7 @@
1
1
  import { exec as execSync, execFile as execFileSync } from "node:child_process";
2
2
  import { gunzip as gunzipSync, gzip as gzipSync } from "node:zlib";
3
3
  import type { ComparisonOperator, Field, FieldType, Schema } from "./index.js";
4
+ import RE2 from "re2";
4
5
  export declare const exec: typeof execSync.__promisify__;
5
6
  export declare const execFile: typeof execFileSync.__promisify__;
6
7
  export declare const gzip: typeof gzipSync.__promisify__;
@@ -83,3 +84,15 @@ export declare const isArrayEqual: (originalValue: string | number | boolean | n
83
84
  * @returns boolean - Result of the wildcard pattern matching.
84
85
  */
85
86
  export declare const isWildcardMatch: (originalValue: string | number | boolean | null | (string | number | boolean | null)[], comparedValue: string | number | boolean | null | (string | number | boolean | null)[]) => boolean;
87
+ /**
88
+ * Retrieves a cached compiled regex or compiles and caches a new one.
89
+ *
90
+ * This function checks if a given regex pattern is already compiled and cached.
91
+ * If it is, the cached instance is returned. If not, the function attempts to compile
92
+ * the regex using RE2, caches the compiled instance, and then returns it. If the pattern
93
+ * is invalid, it returns a fallback object with a `test` method that always returns `false`.
94
+ *
95
+ * @param {string} pattern - The regex pattern to compile or retrieve from the cache.
96
+ * @returns {RE2} - The compiled regex instance or a fallback object on error.
97
+ */
98
+ export declare const getCachedRegex: (pattern: string) => RE2;
@@ -3,6 +3,7 @@ import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync,
3
3
  import { gunzip as gunzipSync, gzip as gzipSync } from "node:zlib";
4
4
  import { promisify } from "node:util";
5
5
  import { detectFieldType, isArrayOfObjects, isNumber, isPassword, isValidID, } from "./utils.js";
6
+ import RE2 from "re2";
6
7
  export const exec = promisify(execSync);
7
8
  export const execFile = promisify(execFileSync);
8
9
  export const gzip = promisify(gzipSync);
@@ -247,3 +248,28 @@ export const isWildcardMatch = (originalValue, comparedValue) => {
247
248
  const wildcardPattern = `^${(comparedValueStr.includes("%") ? comparedValueStr : `%${comparedValueStr}%`).replace(/%/g, ".*")}$`;
248
249
  return new RegExp(wildcardPattern, "i").test(originalValueStr);
249
250
  };
251
+ const regexCache = new Map();
252
+ /**
253
+ * Retrieves a cached compiled regex or compiles and caches a new one.
254
+ *
255
+ * This function checks if a given regex pattern is already compiled and cached.
256
+ * If it is, the cached instance is returned. If not, the function attempts to compile
257
+ * the regex using RE2, caches the compiled instance, and then returns it. If the pattern
258
+ * is invalid, it returns a fallback object with a `test` method that always returns `false`.
259
+ *
260
+ * @param {string} pattern - The regex pattern to compile or retrieve from the cache.
261
+ * @returns {RE2} - The compiled regex instance or a fallback object on error.
262
+ */
263
+ export const getCachedRegex = (pattern) => {
264
+ if (regexCache.has(pattern)) {
265
+ return regexCache.get(pattern);
266
+ }
267
+ try {
268
+ const compiledRegex = new RE2(pattern);
269
+ regexCache.set(pattern, compiledRegex);
270
+ return compiledRegex;
271
+ }
272
+ catch {
273
+ return { test: (_str) => false };
274
+ }
275
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inibase",
3
- "version": "1.0.0-rc.120",
3
+ "version": "1.0.0-rc.121",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Karim Amahtil",
@@ -70,12 +70,13 @@
70
70
  "devDependencies": {
71
71
  "@types/bun": "^1.1.10",
72
72
  "@types/node": "^22.7.4",
73
- "tinybench": "^2.6.0",
74
- "typescript": "^5.6.2"
73
+ "tinybench": "^3.0.7",
74
+ "typescript": "^5.7.2"
75
75
  },
76
76
  "dependencies": {
77
77
  "dotenv": "^16.4.5",
78
- "inison": "latest"
78
+ "inison": "latest",
79
+ "re2": "^1.21.4"
79
80
  },
80
81
  "scripts": {
81
82
  "prepublish": "npx tsc",