inibase 1.0.0-rc.120 → 1.0.0-rc.122

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,42 @@ 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 = {};
25
- this.pageInfo = {};
26
- this.checkIFunique = {};
23
+ this.clear();
27
24
  if (!process.env.INIBASE_SECRET) {
28
25
  if (existsSync(".env") &&
29
26
  readFileSync(".env").includes("INIBASE_SECRET="))
30
- throw this.Error("NO_ENV");
27
+ throw this.createError("NO_ENV");
31
28
  this.salt = scryptSync(randomBytes(16), randomBytes(16), 32);
32
29
  appendFileSync(".env", `\nINIBASE_SECRET=${this.salt.toString("hex")}\n`);
33
30
  }
34
31
  else
35
32
  this.salt = Buffer.from(process.env.INIBASE_SECRET, "hex");
36
33
  }
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];
34
+ static errorMessages = {
35
+ en: {
36
+ TABLE_EMPTY: "Table {variable} is empty",
37
+ TABLE_EXISTS: "Table {variable} already exists",
38
+ TABLE_NOT_EXISTS: "Table {variable} doesn't exist",
39
+ NO_SCHEMA: "Table {variable} does't have a schema",
40
+ FIELD_UNIQUE: "Field {variable} should be unique, got {variable} instead",
41
+ FIELD_REQUIRED: "Field {variable} is required",
42
+ INVALID_ID: "The given ID(s) is/are not valid(s)",
43
+ INVALID_TYPE: "Expect {variable} to be {variable}, got {variable} instead",
44
+ INVALID_PARAMETERS: "The given parameters are not valid",
45
+ INVALID_REGEX_MATCH: "Field {variable} does not match the expected pattern",
46
+ NO_ENV: Number(process.versions.node.split(".").reduce((a, b) => a + b)) >= 26
47
+ ? "please run with '--env-file=.env'"
48
+ : "please use dotenv",
49
+ },
50
+ };
51
+ createError(code, variable, language = "en") {
52
+ const errorMessage = Inibase.errorMessages[language]?.[code];
56
53
  if (!errorMessage)
57
54
  return new Error("ERR");
58
55
  return new Error(variable
@@ -62,17 +59,17 @@ export default class Inibase {
62
59
  : errorMessage.replaceAll("{variable}", ""));
63
60
  }
64
61
  clear() {
65
- this.tables = {};
66
- this.totalItems = {};
62
+ this.tablesMap = new Map();
63
+ this.totalItems = new Map();
67
64
  this.pageInfo = {};
68
- this.checkIFunique = {};
65
+ this.uniqueMap = new Map();
69
66
  }
70
67
  getFileExtension(tableName) {
71
68
  let mainExtension = this.fileExtension;
72
69
  // TODO: ADD ENCRYPTION
73
- // if(this.tables[tableName].config.encryption)
70
+ // if(this.tablesMap.get(tableName).config.encryption)
74
71
  // mainExtension += ".enc"
75
- if (this.tables[tableName].config.compression)
72
+ if (this.tablesMap.get(tableName).config.compression)
76
73
  mainExtension += ".gz";
77
74
  return mainExtension;
78
75
  }
@@ -99,7 +96,7 @@ export default class Inibase {
99
96
  async createTable(tableName, schema, config) {
100
97
  const tablePath = join(this.databasePath, tableName);
101
98
  if (await File.isExists(tablePath))
102
- throw this.Error("TABLE_EXISTS", tableName);
99
+ throw this.createError("TABLE_EXISTS", tableName);
103
100
  await mkdir(join(tablePath, ".tmp"), { recursive: true });
104
101
  await mkdir(join(tablePath, ".cache"));
105
102
  // if config not set => load default global env config
@@ -142,7 +139,8 @@ export default class Inibase {
142
139
  * @param {(Config&{name?: string})} [config]
143
140
  */
144
141
  async updateTable(tableName, schema, config) {
145
- const table = await this.getTable(tableName), tablePath = join(this.databasePath, tableName);
142
+ const table = await this.getTable(tableName);
143
+ const tablePath = join(this.databasePath, tableName);
146
144
  if (schema) {
147
145
  // remove id from schema
148
146
  schema = schema.filter(({ key }) => !["id", "createdAt", "updatedAt"].includes(key));
@@ -232,7 +230,7 @@ export default class Inibase {
232
230
  await this.replaceStringInFile(schemaPath, `"table": "${tableName}"`, `"table": "${config.name}"`);
233
231
  }
234
232
  }
235
- delete this.tables[tableName];
233
+ this.tablesMap.delete(tableName);
236
234
  }
237
235
  /**
238
236
  * Get table schema and config
@@ -240,20 +238,20 @@ export default class Inibase {
240
238
  * @param {string} tableName
241
239
  * @return {*} {Promise<TableObject>}
242
240
  */
243
- async getTable(tableName) {
241
+ async getTable(tableName, encodeIDs = true) {
244
242
  const tablePath = join(this.databasePath, tableName);
245
243
  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),
244
+ throw this.createError("TABLE_NOT_EXISTS", tableName);
245
+ if (!this.tablesMap.has(tableName))
246
+ this.tablesMap.set(tableName, {
247
+ schema: await this.getTableSchema(tableName, encodeIDs),
250
248
  config: {
251
249
  compression: await File.isExists(join(tablePath, ".compression.config")),
252
250
  cache: await File.isExists(join(tablePath, ".cache.config")),
253
251
  prepend: await File.isExists(join(tablePath, ".prepend.config")),
254
252
  },
255
- };
256
- return this.tables[tableName];
253
+ });
254
+ return this.tablesMap.get(tableName);
257
255
  }
258
256
  async getTableSchema(tableName, encodeIDs = true) {
259
257
  const tablePath = join(this.databasePath, tableName);
@@ -288,17 +286,16 @@ export default class Inibase {
288
286
  return UtilsServer.encodeSchemaID(schema, this.salt);
289
287
  }
290
288
  async throwErrorIfTableEmpty(tableName) {
291
- const table = await this.getTable(tableName);
289
+ const table = await this.getTable(tableName, false);
292
290
  if (!table.schema)
293
- throw this.Error("NO_SCHEMA", tableName);
291
+ throw this.createError("NO_SCHEMA", tableName);
294
292
  if (!(await File.isExists(join(this.databasePath, tableName, `id${this.getFileExtension(tableName)}`))))
295
- throw this.Error("TABLE_EMPTY", tableName);
296
- return table;
293
+ throw this.createError("TABLE_EMPTY", tableName);
297
294
  }
298
- validateData(data, schema, skipRequiredField = false) {
295
+ _validateData(data, schema, skipRequiredField = false) {
299
296
  if (Utils.isArrayOfObjects(data))
300
297
  for (const single_data of data)
301
- this.validateData(single_data, schema, skipRequiredField);
298
+ this._validateData(single_data, schema, skipRequiredField);
302
299
  else if (Utils.isObject(data)) {
303
300
  for (const field of schema) {
304
301
  if (!Object.hasOwn(data, field.key) ||
@@ -306,7 +303,7 @@ export default class Inibase {
306
303
  data[field.key] === undefined ||
307
304
  data[field.key] === "") {
308
305
  if (field.required && !skipRequiredField)
309
- throw this.Error("FIELD_REQUIRED", field.key);
306
+ throw this.createError("FIELD_REQUIRED", field.key);
310
307
  return;
311
308
  }
312
309
  if (!Utils.validateFieldType(data[field.key], field.type, (field.type === "array" || field.type === "object") &&
@@ -314,7 +311,7 @@ export default class Inibase {
314
311
  !Utils.isArrayOfObjects(field.children)
315
312
  ? field.children
316
313
  : undefined))
317
- throw this.Error("INVALID_TYPE", [
314
+ throw this.createError("INVALID_TYPE", [
318
315
  field.key,
319
316
  Array.isArray(field.type) ? field.type.join(", ") : field.type,
320
317
  data[field.key],
@@ -322,15 +319,46 @@ export default class Inibase {
322
319
  if ((field.type === "array" || field.type === "object") &&
323
320
  field.children &&
324
321
  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]);
322
+ this._validateData(data[field.key], field.children, skipRequiredField);
323
+ else {
324
+ if (field.regex) {
325
+ const regex = UtilsServer.getCachedRegex(field.regex);
326
+ if (!regex.test(data[field.key]))
327
+ throw this.createError("INVALID_REGEX_MATCH", [field.key]);
328
+ }
329
+ if (field.unique) {
330
+ let uniqueKey;
331
+ if (typeof field.unique === "boolean")
332
+ uniqueKey = randomInt(55);
333
+ else
334
+ uniqueKey = field.unique;
335
+ if (!this.uniqueMap.has(uniqueKey))
336
+ this.uniqueMap.set(uniqueKey, {
337
+ exclude: new Set(),
338
+ columnsValues: new Map(),
339
+ });
340
+ if (!this.uniqueMap
341
+ .get(uniqueKey)
342
+ .columnsValues.has(field.id))
343
+ this.uniqueMap
344
+ .get(uniqueKey)
345
+ .columnsValues.set(field.id, new Set());
346
+ if (data.id)
347
+ this.uniqueMap.get(uniqueKey).exclude.add(-data.id);
348
+ this.uniqueMap
349
+ .get(uniqueKey)
350
+ .columnsValues.get(field.id)
351
+ .add(data[field.key]);
352
+ }
330
353
  }
331
354
  }
332
355
  }
333
356
  }
357
+ async validateData(tableName, data, skipRequiredField = false) {
358
+ // Skip ID and (created|updated)At
359
+ this._validateData(data, this.tablesMap.get(tableName).schema.slice(1, -2), skipRequiredField);
360
+ await this.checkUnique(tableName);
361
+ }
334
362
  cleanObject(obj) {
335
363
  const cleanedObject = Object.entries(obj).reduce((acc, [key, value]) => {
336
364
  if (value !== undefined && value !== null && value !== "")
@@ -353,13 +381,13 @@ export default class Inibase {
353
381
  if (!Array.isArray(value))
354
382
  value = [value];
355
383
  if (Utils.isArrayOfObjects(fieldChildrenType))
356
- return this.formatData(value, fieldChildrenType);
384
+ return this._formatData(value, fieldChildrenType);
357
385
  if (!value.length)
358
386
  return null;
359
387
  return value.map((_value) => this.formatField(_value, fieldChildrenType));
360
388
  case "object":
361
389
  if (Utils.isArrayOfObjects(fieldChildrenType))
362
- return this.formatData(value, fieldChildrenType, _formatOnlyAvailiableKeys);
390
+ return this._formatData(value, fieldChildrenType, _formatOnlyAvailiableKeys);
363
391
  break;
364
392
  case "table":
365
393
  if (Utils.isObject(value)) {
@@ -402,25 +430,32 @@ export default class Inibase {
402
430
  }
403
431
  return null;
404
432
  }
405
- async checkUnique(tableName, schema) {
433
+ async checkUnique(tableName) {
406
434
  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
- ]);
435
+ const flattenSchema = Utils.flattenSchema(this.tablesMap.get(tableName).schema);
436
+ for await (const [_uniqueID, valueObject] of this.uniqueMap) {
437
+ let index = 0;
438
+ for await (const [columnID, values] of valueObject.columnsValues) {
439
+ index++;
440
+ const field = flattenSchema.find(({ id }) => id === columnID);
441
+ const [searchResult, totalLines] = await File.search(join(tablePath, `${field.key}${this.getFileExtension(tableName)}`), "[]", values, undefined, valueObject.exclude, field.type, field.children, 1, undefined, false, this.salt);
442
+ if (searchResult && totalLines > 0) {
443
+ if (index === valueObject.columnsValues.size)
444
+ throw this.createError("FIELD_UNIQUE", [
445
+ field.key,
446
+ Array.isArray(values) ? values.join(", ") : values,
447
+ ]);
448
+ }
449
+ else
450
+ continue;
451
+ }
417
452
  }
418
- this.checkIFunique = {};
453
+ this.uniqueMap = new Map();
419
454
  }
420
- formatData(data, schema, formatOnlyAvailiableKeys) {
455
+ _formatData(data, schema, formatOnlyAvailiableKeys) {
421
456
  const clonedData = JSON.parse(JSON.stringify(data));
422
457
  if (Utils.isArrayOfObjects(clonedData))
423
- return clonedData.map((singleData) => this.formatData(singleData, schema, formatOnlyAvailiableKeys));
458
+ return clonedData.map((singleData) => this._formatData(singleData, schema, formatOnlyAvailiableKeys));
424
459
  if (Utils.isObject(clonedData)) {
425
460
  const RETURN = {};
426
461
  for (const field of schema) {
@@ -436,6 +471,9 @@ export default class Inibase {
436
471
  }
437
472
  return [];
438
473
  }
474
+ formatData(tableName, data, formatOnlyAvailiableKeys) {
475
+ data = this._formatData(data, this.tablesMap.get(tableName).schema, formatOnlyAvailiableKeys);
476
+ }
439
477
  getDefaultValue(field) {
440
478
  if (Array.isArray(field.type))
441
479
  return this.getDefaultValue({
@@ -835,7 +873,7 @@ export default class Inibase {
835
873
  [key]: value,
836
874
  },
837
875
  ])));
838
- this.totalItems[`${tableName}-${key}`] = totalLines;
876
+ this.totalItems.set(`${tableName}-${key}`, totalLines);
839
877
  if (linesNumbers?.size) {
840
878
  if (searchIn) {
841
879
  for (const lineNumber of linesNumbers)
@@ -911,9 +949,10 @@ export default class Inibase {
911
949
  options.page = options.page || 1;
912
950
  options.perPage = options.perPage || 15;
913
951
  let RETURN;
952
+ await this.getTable(tableName);
914
953
  let schema = (await this.getTable(tableName)).schema;
915
954
  if (!schema)
916
- throw this.Error("NO_SCHEMA", tableName);
955
+ throw this.createError("NO_SCHEMA", tableName);
917
956
  let pagination;
918
957
  for await (const paginationFileName of glob("*.pagination", {
919
958
  cwd: tablePath,
@@ -942,7 +981,7 @@ export default class Inibase {
942
981
  .map((column) => [column, true]);
943
982
  let cacheKey = "";
944
983
  // Criteria
945
- if (this.tables[tableName].config.cache)
984
+ if (this.tablesMap.get(tableName).config.cache)
946
985
  cacheKey = UtilsServer.hashString(inspect(sortArray, { sorted: true }));
947
986
  if (where) {
948
987
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
@@ -980,7 +1019,7 @@ export default class Inibase {
980
1019
  if (cacheKey)
981
1020
  await File.lock(join(tablePath, ".tmp"), cacheKey);
982
1021
  // Combine && Execute the commands synchronously
983
- let lines = (await UtilsServer.exec(this.tables[tableName].config.cache
1022
+ let lines = (await UtilsServer.exec(this.tablesMap.get(tableName).config.cache
984
1023
  ? (await File.isExists(join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)))
985
1024
  ? `${awkCommand} '${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)}'`
986
1025
  : `${pasteCommand} | ${sortCommand} -o '${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)}' && ${awkCommand} '${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)}'`
@@ -991,8 +1030,8 @@ export default class Inibase {
991
1030
  .split("\n");
992
1031
  if (where)
993
1032
  lines = lines.slice((options.page - 1) * options.perPage, options.page * options.perPage);
994
- else if (!this.totalItems[`${tableName}-*`])
995
- this.totalItems[`${tableName}-*`] = pagination[1];
1033
+ else if (!this.totalItems.has(`${tableName}-*`))
1034
+ this.totalItems.set(`${tableName}-*`, pagination[1]);
996
1035
  if (!lines.length)
997
1036
  return null;
998
1037
  // Parse the result and extract the specified lines
@@ -1025,8 +1064,8 @@ export default class Inibase {
1025
1064
  RETURN = Object.values(await this.processSchemaData(tableName, schema, Array.from({ length: options.perPage }, (_, index) => (options.page - 1) * options.perPage +
1026
1065
  index +
1027
1066
  1), options));
1028
- if (!this.totalItems[`${tableName}-*`])
1029
- this.totalItems[`${tableName}-*`] = pagination[1];
1067
+ if (!this.totalItems.has(`${tableName}-*`))
1068
+ this.totalItems.set(`${tableName}-*`, pagination[1]);
1030
1069
  }
1031
1070
  else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1032
1071
  Utils.isNumber(where)) {
@@ -1034,8 +1073,8 @@ export default class Inibase {
1034
1073
  let lineNumbers = where;
1035
1074
  if (!Array.isArray(lineNumbers))
1036
1075
  lineNumbers = [lineNumbers];
1037
- if (!this.totalItems[`${tableName}-*`])
1038
- this.totalItems[`${tableName}-*`] = lineNumbers.length;
1076
+ if (!this.totalItems.has(`${tableName}-*`))
1077
+ this.totalItems.set(`${tableName}-*`, lineNumbers.length);
1039
1078
  // useless
1040
1079
  if (onlyLinesNumbers)
1041
1080
  return lineNumbers;
@@ -1048,11 +1087,11 @@ export default class Inibase {
1048
1087
  let Ids = where;
1049
1088
  if (!Array.isArray(Ids))
1050
1089
  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);
1090
+ 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
1091
  if (!lineNumbers)
1053
1092
  return null;
1054
- if (!this.totalItems[`${tableName}-*`])
1055
- this.totalItems[`${tableName}-*`] = countItems;
1093
+ if (!this.totalItems.has(`${tableName}-*`))
1094
+ this.totalItems.set(`${tableName}-*`, countItems);
1056
1095
  if (onlyLinesNumbers)
1057
1096
  return Object.keys(lineNumbers).length
1058
1097
  ? Object.keys(lineNumbers).map(Number)
@@ -1069,13 +1108,13 @@ export default class Inibase {
1069
1108
  else if (Utils.isObject(where)) {
1070
1109
  let cachedFilePath = "";
1071
1110
  // Criteria
1072
- if (this.tables[tableName].config.cache)
1111
+ if (this.tablesMap.get(tableName).config.cache)
1073
1112
  cachedFilePath = join(tablePath, ".cache", `${UtilsServer.hashString(inspect(where, { sorted: true }))}${this.fileExtension}`);
1074
- if (this.tables[tableName].config.cache &&
1113
+ if (this.tablesMap.get(tableName).config.cache &&
1075
1114
  (await File.isExists(cachedFilePath))) {
1076
1115
  const cachedItems = (await readFile(cachedFilePath, "utf8")).split(",");
1077
- if (!this.totalItems[`${tableName}-*`])
1078
- this.totalItems[`${tableName}-*`] = cachedItems.length;
1116
+ if (!this.totalItems.has(`${tableName}-*`))
1117
+ this.totalItems.set(`${tableName}-*`, cachedItems.length);
1079
1118
  if (onlyLinesNumbers)
1080
1119
  return onlyOne ? Number(cachedItems[0]) : cachedItems.map(Number);
1081
1120
  return this.get(tableName, cachedItems
@@ -1085,8 +1124,8 @@ export default class Inibase {
1085
1124
  let linesNumbers = null;
1086
1125
  [RETURN, linesNumbers] = await this.applyCriteria(tableName, schema, options, where);
1087
1126
  if (RETURN && linesNumbers?.size) {
1088
- if (!this.totalItems[`${tableName}-*`])
1089
- this.totalItems[`${tableName}-*`] = linesNumbers.size;
1127
+ if (!this.totalItems.has(`${tableName}-*`))
1128
+ this.totalItems.set(`${tableName}-*`, linesNumbers.size);
1090
1129
  if (onlyLinesNumbers)
1091
1130
  return onlyOne
1092
1131
  ? linesNumbers.values().next().value
@@ -1096,7 +1135,7 @@ export default class Inibase {
1096
1135
  .map(({ id }) => id);
1097
1136
  RETURN = Object.values(Utils.deepMerge(RETURN, await this.processSchemaData(tableName, Utils.filterSchema(schema, ({ id, type, children }) => !alreadyExistsColumnsIDs.includes(id) ||
1098
1137
  Utils.isFieldType("table", type, children)), Object.keys(RETURN).map(Number), options)));
1099
- if (this.tables[tableName].config.cache)
1138
+ if (this.tablesMap.get(tableName).config.cache)
1100
1139
  await writeFile(cachedFilePath, Array.from(linesNumbers).join(","));
1101
1140
  }
1102
1141
  }
@@ -1104,8 +1143,9 @@ export default class Inibase {
1104
1143
  (Utils.isObject(RETURN) && !Object.keys(RETURN).length) ||
1105
1144
  (Array.isArray(RETURN) && !RETURN.length))
1106
1145
  return null;
1107
- const greatestTotalItems = this.totalItems[`${tableName}-*`] ??
1108
- Math.max(...Object.entries(this.totalItems)
1146
+ const greatestTotalItems = this.totalItems.has(`${tableName}-*`)
1147
+ ? this.totalItems.get(`${tableName}-*`)
1148
+ : Math.max(...[...this.totalItems.entries()]
1109
1149
  .filter(([k]) => k.startsWith(`${tableName}-`))
1110
1150
  .map(([, v]) => v));
1111
1151
  this.pageInfo[tableName] = {
@@ -1122,24 +1162,24 @@ export default class Inibase {
1122
1162
  page: 1,
1123
1163
  perPage: 15,
1124
1164
  };
1125
- const tablePath = join(this.databasePath, tableName), schema = (await this.getTable(tableName)).schema;
1126
- if (!schema)
1127
- throw this.Error("NO_SCHEMA", tableName);
1165
+ const tablePath = join(this.databasePath, tableName);
1166
+ await this.getTable(tableName);
1167
+ if (!this.tablesMap.get(tableName).schema)
1168
+ throw this.createError("NO_SCHEMA", tableName);
1128
1169
  if (!returnPostedData)
1129
1170
  returnPostedData = false;
1130
1171
  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;
1172
+ await this.validateData(tableName, data);
1134
1173
  const renameList = [];
1135
1174
  try {
1136
1175
  await File.lock(join(tablePath, ".tmp"), keys);
1137
1176
  let paginationFilePath;
1138
1177
  for await (const fileName of glob("*.pagination", { cwd: tablePath }))
1139
1178
  paginationFilePath = join(tablePath, fileName);
1140
- [lastId, this.totalItems[`${tableName}-*`]] = parse(paginationFilePath)
1179
+ let [lastId, _totalItems] = parse(paginationFilePath)
1141
1180
  .name.split("-")
1142
1181
  .map(Number);
1182
+ this.totalItems.set(`${tableName}-*`, _totalItems);
1143
1183
  if (Utils.isArrayOfObjects(data))
1144
1184
  for (let index = 0; index < data.length; index++) {
1145
1185
  const element = data[index];
@@ -1152,35 +1192,33 @@ export default class Inibase {
1152
1192
  data.createdAt = Date.now();
1153
1193
  data.updatedAt = undefined;
1154
1194
  }
1155
- await this.checkUnique(tableName, schema);
1156
- data = this.formatData(data, schema);
1157
- const pathesContents = this.joinPathesContents(tableName, this.tables[tableName].config.prepend
1195
+ this.formatData(tableName, data);
1196
+ const pathesContents = this.joinPathesContents(tableName, this.tablesMap.get(tableName).config.prepend
1158
1197
  ? Array.isArray(data)
1159
1198
  ? data.toReversed()
1160
1199
  : data
1161
1200
  : data);
1162
- await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(this.tables[tableName].config.prepend
1201
+ await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(this.tablesMap.get(tableName).config.prepend
1163
1202
  ? await File.prepend(path, content)
1164
1203
  : await File.append(path, content))));
1165
1204
  await Promise.allSettled(renameList
1166
1205
  .filter(([_, filePath]) => filePath)
1167
1206
  .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1168
- if (this.tables[tableName].config.cache)
1207
+ if (this.tablesMap.get(tableName).config.cache)
1169
1208
  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`));
1209
+ const currentValue = this.totalItems.get(`${tableName}-*`) || 0;
1210
+ this.totalItems.set(`${tableName}-*`, currentValue + (Array.isArray(data) ? data.length : 1));
1211
+ await rename(paginationFilePath, join(tablePath, `${lastId}-${this.totalItems.get(`${tableName}-*`)}.pagination`));
1174
1212
  if (returnPostedData)
1175
- return this.get(tableName, this.tables[tableName].config.prepend
1213
+ return this.get(tableName, this.tablesMap.get(tableName).config.prepend
1176
1214
  ? Array.isArray(data)
1177
1215
  ? data.map((_, index) => index + 1).toReversed()
1178
1216
  : 1
1179
1217
  : Array.isArray(data)
1180
1218
  ? data
1181
- .map((_, index) => this.totalItems[`${tableName}-*`] - index)
1219
+ .map((_, index) => this.totalItems.get(`${tableName}-*`) - index)
1182
1220
  .toReversed()
1183
- : this.totalItems[`${tableName}-*`], options, !Utils.isArrayOfObjects(data));
1221
+ : this.totalItems.get(`${tableName}-*`), options, !Utils.isArrayOfObjects(data));
1184
1222
  }
1185
1223
  finally {
1186
1224
  if (renameList.length)
@@ -1194,23 +1232,20 @@ export default class Inibase {
1194
1232
  }, returnUpdatedData) {
1195
1233
  const renameList = [];
1196
1234
  const tablePath = join(this.databasePath, tableName);
1197
- const schema = (await this.throwErrorIfTableEmpty(tableName))
1198
- .schema;
1235
+ await this.throwErrorIfTableEmpty(tableName);
1199
1236
  if (!where) {
1200
1237
  if (Utils.isArrayOfObjects(data)) {
1201
1238
  if (!data.every((item) => Object.hasOwn(item, "id") && Utils.isValidID(item.id)))
1202
- throw this.Error("INVALID_ID");
1239
+ throw this.createError("INVALID_ID");
1203
1240
  return this.put(tableName, data, data.map(({ id }) => id), options, returnUpdatedData || undefined);
1204
1241
  }
1205
1242
  if (Object.hasOwn(data, "id")) {
1206
1243
  if (!Utils.isValidID(data.id))
1207
- throw this.Error("INVALID_ID", data.id);
1244
+ throw this.createError("INVALID_ID", data.id);
1208
1245
  return this.put(tableName, data, data.id, options, returnUpdatedData || undefined);
1209
1246
  }
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);
1247
+ await this.validateData(tableName, data, true);
1248
+ this.formatData(tableName, data, true);
1214
1249
  const pathesContents = this.joinPathesContents(tableName, {
1215
1250
  ...(({ id, ...restOfData }) => restOfData)(data),
1216
1251
  updatedAt: Date.now(),
@@ -1220,14 +1255,12 @@ export default class Inibase {
1220
1255
  for await (const paginationFileName of glob("*.pagination", {
1221
1256
  cwd: tablePath,
1222
1257
  }))
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}-*`]))));
1258
+ this.totalItems.set(`${tableName}-*`, parse(paginationFileName).name.split("-").map(Number)[1]);
1259
+ await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content, this.totalItems.get(`${tableName}-*`)))));
1227
1260
  await Promise.allSettled(renameList
1228
1261
  .filter(([_, filePath]) => filePath)
1229
1262
  .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1230
- if (this.tables[tableName].config.cache)
1263
+ if (this.tablesMap.get(tableName).config.cache)
1231
1264
  await this.clearCache(join(tablePath, ".cache"));
1232
1265
  if (returnUpdatedData)
1233
1266
  return await this.get(tableName, undefined, options);
@@ -1246,9 +1279,8 @@ export default class Inibase {
1246
1279
  else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
1247
1280
  Utils.isNumber(where)) {
1248
1281
  // "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);
1282
+ await this.validateData(tableName, data, true);
1283
+ this.formatData(tableName, data, true);
1252
1284
  const pathesContents = Object.fromEntries(Object.entries(this.joinPathesContents(tableName, Utils.isArrayOfObjects(data)
1253
1285
  ? data.map((item) => ({
1254
1286
  ...item,
@@ -1269,7 +1301,7 @@ export default class Inibase {
1269
1301
  await Promise.allSettled(renameList
1270
1302
  .filter(([_, filePath]) => filePath)
1271
1303
  .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1272
- if (this.tables[tableName].config.cache)
1304
+ if (this.tablesMap.get(tableName).config.cache)
1273
1305
  await this.clearCache(tableName);
1274
1306
  if (returnUpdatedData)
1275
1307
  return this.get(tableName, where, options, !Array.isArray(where));
@@ -1286,7 +1318,7 @@ export default class Inibase {
1286
1318
  return this.put(tableName, data, lineNumbers, options, returnUpdatedData || undefined);
1287
1319
  }
1288
1320
  else
1289
- throw this.Error("INVALID_PARAMETERS");
1321
+ throw this.createError("INVALID_PARAMETERS");
1290
1322
  }
1291
1323
  /**
1292
1324
  * Delete item(s) in a table
@@ -1314,7 +1346,7 @@ export default class Inibase {
1314
1346
  await Promise.all((await readdir(tablePath))
1315
1347
  ?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)))
1316
1348
  .map(async (file) => unlink(join(tablePath, file))));
1317
- if (this.tables[tableName].config.cache)
1349
+ if (this.tablesMap.get(tableName).config.cache)
1318
1350
  await this.clearCache(tableName);
1319
1351
  await rename(paginationFilePath, join(tablePath, `${pagination[0]}-0.pagination`));
1320
1352
  return true;
@@ -1357,7 +1389,7 @@ export default class Inibase {
1357
1389
  await Promise.all((await readdir(tablePath))
1358
1390
  ?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)))
1359
1391
  .map(async (file) => unlink(join(tablePath, file))));
1360
- if (this.tables[tableName].config.cache)
1392
+ if (this.tablesMap.get(tableName).config.cache)
1361
1393
  await this.clearCache(tableName);
1362
1394
  await rename(paginationFilePath, join(tablePath, `${pagination[0]}-${pagination[1] - (Array.isArray(where) ? where.length : 1)}.pagination`));
1363
1395
  return true;
@@ -1375,7 +1407,7 @@ export default class Inibase {
1375
1407
  return this.delete(tableName, lineNumbers);
1376
1408
  }
1377
1409
  else
1378
- throw this.Error("INVALID_PARAMETERS");
1410
+ throw this.createError("INVALID_PARAMETERS");
1379
1411
  return false;
1380
1412
  }
1381
1413
  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.122",
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",