inibase 1.0.0-rc.57 → 1.0.0-rc.58

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
@@ -14,7 +14,7 @@
14
14
  - **Super-Fast** :zap: (built-in caching system)
15
15
  - **ATOMIC** :lock: File lock for writing
16
16
  - **Built-in form-validation** included :sunglasses:
17
- - **Suitable for large data** :page_with_curl: (tested with 200K row)
17
+ - **Suitable for large data** :page_with_curl: (tested with 4M records)
18
18
  - **Support Compression** :eight_spoked_asterisk: (using built-in nodejs zlib)
19
19
  - **Support Table Joins** :link:
20
20
  - **Low memory-usage** :chart_with_downwards_trend: (3-5mb)
@@ -55,6 +55,21 @@ If you like Inibase, please sponsor: [GitHub Sponsors](https://github.com/sponso
55
55
 
56
56
  To simplify the idea, each database has tables, each table has columns, each column will be stored in a seperated file. When **POST**ing new data, it will be appended to the _head_ of each file as new line. When **GET**ing data, the file will be readed line-by-line so it can handle large data (without consuming a lot of resources), when **PUT**ing(updating) in a specific column, only one file will be opened and updated
57
57
 
58
+ ## Config (.env)
59
+
60
+ The `.env` file supports the following parameters (make sure to run commands with flag --env-file=.env)
61
+
62
+ ```ini
63
+ # Auto generated secret key, will be using for encrypting the IDs
64
+ INIBASE_SECRET=
65
+
66
+ INIBASE_COMPRESSION=true
67
+ INIBASE_CACHE=true
68
+
69
+ # Prepend new items to the beginning of file
70
+ INIBASE_REVERSE=true
71
+ ```
72
+
58
73
  ## Benchmark
59
74
 
60
75
  ### Bulk
@@ -77,22 +92,6 @@ To simplify the idea, each database has tables, each table has columns, each col
77
92
 
78
93
  Ps: Testing by default with `user` table, with username, email, password fields _so results include password encryption process_
79
94
 
80
-
81
- ## Config (.env)
82
-
83
- The `.env` file supports the following parameters (make sure to run commands with flag --env-file=.env)
84
-
85
- ```ini
86
- # Auto generated secret key, will be using for encrypting the IDs
87
- INIBASE_SECRET=
88
-
89
- INIBASE_COMPRESSION=true
90
- INIBASE_CACHE=true
91
-
92
- # Prepend new items to the beginning of file
93
- INIBASE_REVERSE=true
94
- ```
95
-
96
95
  ## Roadmap
97
96
 
98
97
  - [x] Actions:
@@ -100,7 +99,7 @@ INIBASE_REVERSE=true
100
99
  - [x] Pagination
101
100
  - [x] Criteria
102
101
  - [x] Columns
103
- - [x] Order By (using UNIX commands)
102
+ - [x] Sort (using UNIX commands)
104
103
  - [x] POST
105
104
  - [x] PUT
106
105
  - [x] DELETE
@@ -512,6 +511,23 @@ await db.min("user", ["age", ...], { isActive: false });
512
511
 
513
512
  </details>
514
513
 
514
+ <details>
515
+ <summary>SORT</summary>
516
+
517
+ ```js
518
+ import Inibase from "inibase";
519
+ const db = new Inibase("/databaseName");
520
+
521
+ // order users by the age column
522
+ await db.sort("user", "age");
523
+
524
+ // order users by the age and username columns
525
+ await db.sort("user", ["age","username"]);
526
+ await db.sort("user", {age: -1, username: "asc"});
527
+ ```
528
+
529
+ </details>
530
+
515
531
  ## License
516
532
 
517
533
  [MIT](./LICENSE)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
package/dist/cli.js ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { parseArgs } from "node:util";
5
+ import Inibase from "./index.js";
6
+ import { basename } from "node:path";
7
+ import { isJSON, isNumber } from "./utils.js";
8
+ import Inison from "inison";
9
+ const { path } = parseArgs({
10
+ options: {
11
+ path: { type: "string", short: "p" },
12
+ },
13
+ }).values;
14
+ if (!path)
15
+ throw new Error("Please specify database folder path --path <databasePath> or -p <databasePath>");
16
+ const db = new Inibase(basename(path));
17
+ const rl = createInterface({
18
+ input: process.stdin,
19
+ output: process.stdout,
20
+ });
21
+ rl.prompt();
22
+ rl.on("line", async (input) => {
23
+ const trimedInput = input.trim();
24
+ if (trimedInput === "clear") {
25
+ console.clear();
26
+ rl.prompt();
27
+ }
28
+ if (trimedInput === "info") {
29
+ console.warn("war");
30
+ console.error("err");
31
+ }
32
+ const splitedInput = trimedInput.match(/[^\s"']+|"([^"]*)"|'([^']*)'/g);
33
+ if (["get", "post", "delete", "put"].includes(splitedInput[0].toLocaleLowerCase())) {
34
+ const table = splitedInput[1];
35
+ if (!table)
36
+ throw new Error("Please specify table name");
37
+ let { where, page, perPage, columns, data, returnData } = parseArgs({
38
+ args: splitedInput.toSpliced(0, 2),
39
+ options: {
40
+ where: { type: "string", short: "w" },
41
+ page: { type: "string", short: "p" },
42
+ perPage: { type: "string", short: "l" },
43
+ columns: { type: "string", short: "c", multiple: true },
44
+ data: { type: "string", short: "d" },
45
+ returnData: { type: "boolean", short: "r" },
46
+ },
47
+ }).values;
48
+ if (where) {
49
+ if (isNumber(where))
50
+ where = Number(where);
51
+ else if (isJSON(where))
52
+ where = Inison.unstringify(where);
53
+ }
54
+ if (data) {
55
+ if (isJSON(data))
56
+ where = Inison.unstringify(data);
57
+ else
58
+ data = undefined;
59
+ }
60
+ switch (splitedInput[0].toLocaleLowerCase()) {
61
+ case "get":
62
+ console.log(await db.get(table, where, {
63
+ page: Number(page) ?? 1,
64
+ perPage: Number(perPage) ?? 15,
65
+ columns,
66
+ }));
67
+ break;
68
+ case "post":
69
+ {
70
+ const postReturn = await db.post(table, data, {
71
+ page: Number(page) ?? 1,
72
+ perPage: Number(perPage) ?? 15,
73
+ columns,
74
+ }, returnData);
75
+ console.log(postReturn !== null && typeof postReturn === "object"
76
+ ? "Item(s) Posted Successfully"
77
+ : postReturn);
78
+ }
79
+ break;
80
+ case "put": {
81
+ const putReturn = await db.put(table, data, where, {
82
+ page: Number(page) ?? 1,
83
+ perPage: Number(perPage) ?? 15,
84
+ columns,
85
+ }, returnData);
86
+ console.log(putReturn !== null && typeof putReturn === "object"
87
+ ? "Item(s) Updated Successfully"
88
+ : putReturn);
89
+ break;
90
+ }
91
+ case "delete":
92
+ console.log(await db.delete(table, where));
93
+ break;
94
+ default:
95
+ break;
96
+ }
97
+ }
98
+ });
package/dist/file.js CHANGED
@@ -1,11 +1,11 @@
1
- import { open, access, writeFile, readFile, constants as fsConstants, unlink, copyFile, appendFile, } from "node:fs/promises";
1
+ import { open, access, writeFile, readFile, constants as fsConstants, unlink, copyFile, appendFile, mkdir, } from "node:fs/promises";
2
2
  import { createInterface } from "node:readline";
3
3
  import { Transform } from "node:stream";
4
4
  import { pipeline } from "node:stream/promises";
5
5
  import { createGzip, createGunzip, gunzipSync, gzipSync } from "node:zlib";
6
- import { join } from "node:path";
6
+ import { dirname, join } from "node:path";
7
7
  import { detectFieldType, isArrayOfObjects, isJSON, isNumber, isObject, } from "./utils.js";
8
- import { encodeID, compare } from "./utils.server.js";
8
+ import { encodeID, compare, exec } from "./utils.server.js";
9
9
  import * as Config from "./config.js";
10
10
  import Inison from "inison";
11
11
  export const lock = async (folderPath, prefix) => {
@@ -30,6 +30,7 @@ export const unlock = async (folderPath, prefix) => {
30
30
  catch { }
31
31
  };
32
32
  export const write = async (filePath, data, disableCompression = false) => {
33
+ await mkdir(dirname(filePath), { recursive: true });
33
34
  await writeFile(filePath, Config.isCompressionEnabled && !disableCompression
34
35
  ? gzipSync(String(data))
35
36
  : String(data));
@@ -203,18 +204,28 @@ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, s
203
204
  lines[linesCount] = decode(lastLine, fieldType, fieldChildrenType, secretKey);
204
205
  }
205
206
  else {
206
- const lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
207
- for await (const line of rl) {
208
- linesCount++;
209
- if (!lineNumbersArray.has(linesCount))
210
- continue;
211
- lines[linesCount] = decode(line, fieldType, fieldChildrenType, secretKey);
212
- lineNumbersArray.delete(linesCount);
213
- if (!lineNumbersArray.size && !readWholeFile)
214
- break;
207
+ lineNumbers = Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers];
208
+ if (readWholeFile) {
209
+ const lineNumbersArray = new Set(lineNumbers);
210
+ for await (const line of rl) {
211
+ linesCount++;
212
+ if (!lineNumbersArray.has(linesCount))
213
+ continue;
214
+ lines[linesCount] = decode(line, fieldType, fieldChildrenType, secretKey);
215
+ lineNumbersArray.delete(linesCount);
216
+ }
217
+ return [lines, linesCount];
218
+ }
219
+ const command = Config.isCompressionEnabled
220
+ ? `zcat ${filePath} | sed -n '${lineNumbers.join("p;")}p'`
221
+ : `sed -n '${lineNumbers.join("p;")}p' ${filePath}`, foundedLines = (await exec(command)).stdout.trim().split(/\r?\n/);
222
+ let index = 0;
223
+ for (const line of foundedLines) {
224
+ lines[lineNumbers[index]] = decode(line, fieldType, fieldChildrenType, secretKey);
225
+ index++;
215
226
  }
216
227
  }
217
- return readWholeFile ? [lines, linesCount] : lines;
228
+ return lines;
218
229
  }
219
230
  finally {
220
231
  // Ensure that file handles are closed, even if an error occurred
@@ -334,32 +345,41 @@ export const append = async (filePath, data) => {
334
345
  * Note: Creates a temporary file during the process and replaces the original file with it after removing lines.
335
346
  */
336
347
  export const remove = async (filePath, linesToDelete) => {
337
- let linesCount = 0;
338
- let deletedCount = 0;
339
- const fileHandle = await open(filePath, "r");
340
- const fileTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/$1");
341
- const fileTempHandle = await open(fileTempPath, "w");
342
- const linesToDeleteArray = new Set(Array.isArray(linesToDelete)
348
+ linesToDelete = Array.isArray(linesToDelete)
343
349
  ? linesToDelete.map(Number)
344
- : [Number(linesToDelete)]);
345
- const rl = readLineInternface(fileHandle);
346
- await _pipeline(rl, fileTempHandle.createWriteStream(), new Transform({
347
- transform(line, encoding, callback) {
348
- linesCount++;
349
- if (linesToDeleteArray.has(linesCount)) {
350
- deletedCount++;
350
+ : [Number(linesToDelete)];
351
+ const fileTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/$1");
352
+ if (linesToDelete.length < 1000) {
353
+ const command = Config.isCompressionEnabled
354
+ ? `zcat ${filePath} | sed "${linesToDelete.join("d;")}d" | gzip > ${fileTempPath}`
355
+ : `sed "${linesToDelete.join("d;")}d" ${filePath} > ${fileTempPath}`;
356
+ await exec(command);
357
+ }
358
+ else {
359
+ let linesCount = 0;
360
+ let deletedCount = 0;
361
+ const fileHandle = await open(filePath, "r");
362
+ const fileTempHandle = await open(fileTempPath, "w");
363
+ const linesToDeleteArray = new Set(linesToDelete);
364
+ const rl = readLineInternface(fileHandle);
365
+ await _pipeline(rl, fileTempHandle.createWriteStream(), new Transform({
366
+ transform(line, _, callback) {
367
+ linesCount++;
368
+ if (linesToDeleteArray.has(linesCount)) {
369
+ deletedCount++;
370
+ return callback();
371
+ }
372
+ return callback(null, `${line}\n`);
373
+ },
374
+ final(callback) {
375
+ if (deletedCount === linesCount)
376
+ this.push("\n");
351
377
  return callback();
352
- }
353
- return callback(null, `${line}\n`);
354
- },
355
- final(callback) {
356
- if (deletedCount === linesCount)
357
- this.push("\n");
358
- return callback();
359
- },
360
- }));
361
- await fileTempHandle.close();
362
- await fileHandle.close();
378
+ },
379
+ }));
380
+ await fileTempHandle.close();
381
+ await fileHandle.close();
382
+ }
363
383
  return [fileTempPath, filePath];
364
384
  };
365
385
  /**
@@ -447,7 +467,7 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
447
467
  * Note: Reads through the file line by line to count the total number of lines.
448
468
  */
449
469
  export const count = async (filePath) => {
450
- // return Number((await exec(`wc -l < ${filePath}`)).stdout.trim());
470
+ // Number((await exec(`wc -l < ${filePath}`)).stdout.trim());
451
471
  let linesCount = 0;
452
472
  if (await isExists(filePath)) {
453
473
  let fileHandle = null;
package/dist/index.d.ts CHANGED
@@ -47,12 +47,13 @@ export default class Inibase {
47
47
  database: string;
48
48
  table: string | null;
49
49
  pageInfo: Record<string, pageInfo>;
50
+ private fileExtension;
50
51
  private checkIFunique;
51
- private isThreadEnabled;
52
52
  private totalItems;
53
53
  salt: Buffer;
54
54
  constructor(database: string, mainFolder?: string, _table?: string | null, _totalItems?: Record<string, number>, _pageInfo?: Record<string, pageInfo>, _isThreadEnabled?: boolean);
55
55
  private throwError;
56
+ private getFileExtension;
56
57
  private _schemaToIdsPath;
57
58
  setTableSchema(tableName: string, schema: Schema): Promise<void>;
58
59
  getTableSchema(tableName: string, encodeIDs?: boolean): Promise<Schema | undefined>;
@@ -81,6 +82,8 @@ export default class Inibase {
81
82
  put(tableName: string, data: Data[], where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true): Promise<Data[] | null>;
82
83
  delete(tableName: string, where?: number | string, _id?: string | string[]): Promise<string | null>;
83
84
  delete(tableName: string, where?: (number | string)[] | Criteria, _id?: string | string[]): Promise<string[] | null>;
85
+ delete(tableName: string, where?: number, _id?: string | string[]): Promise<number | null>;
86
+ delete(tableName: string, where?: number[], _id?: string | string[]): Promise<number[] | null>;
84
87
  sum(tableName: string, columns: string, where?: number | string | (number | string)[] | Criteria): Promise<number>;
85
88
  sum(tableName: string, columns: string[], where?: number | string | (number | string)[] | Criteria): Promise<Record<string, number>>;
86
89
  max(tableName: string, columns: string, where?: number | string | (number | string)[] | Criteria): Promise<number>;
package/dist/index.js CHANGED
@@ -13,8 +13,8 @@ export default class Inibase {
13
13
  database;
14
14
  table;
15
15
  pageInfo;
16
+ fileExtension = ".txt";
16
17
  checkIFunique;
17
- isThreadEnabled = false;
18
18
  totalItems;
19
19
  salt;
20
20
  constructor(database, mainFolder = ".", _table = null, _totalItems = {}, _pageInfo = {}, _isThreadEnabled = false) {
@@ -23,7 +23,6 @@ export default class Inibase {
23
23
  this.table = _table;
24
24
  this.totalItems = _totalItems;
25
25
  this.pageInfo = _pageInfo;
26
- this.isThreadEnabled = _isThreadEnabled;
27
26
  this.checkIFunique = {};
28
27
  if (!process.env.INIBASE_SECRET) {
29
28
  if (existsSync(".env") &&
@@ -34,6 +33,19 @@ export default class Inibase {
34
33
  }
35
34
  else
36
35
  this.salt = Buffer.from(process.env.INIBASE_SECRET, "hex");
36
+ // try {
37
+ // if (Config.isCompressionEnabled)
38
+ // execSync(
39
+ // `gzip -r ${join(this.folder, this.database)}/*/*.txt 2>/dev/null`,
40
+ // );
41
+ // else
42
+ // execSync(
43
+ // `gunzip -r ${join(
44
+ // this.folder,
45
+ // this.database,
46
+ // )}/*/*.txt.gz 2>/dev/null`,
47
+ // );
48
+ // } catch {}
37
49
  }
38
50
  throwError(code, variable, language = "en") {
39
51
  const errorMessages = {
@@ -61,6 +73,15 @@ export default class Inibase {
61
73
  : errorMessage.replaceAll("{variable}", `'${variable.toString()}'`)
62
74
  : errorMessage.replaceAll("{variable}", ""));
63
75
  }
76
+ getFileExtension = () => {
77
+ let mainExtension = this.fileExtension;
78
+ // TODO: ADD ENCRYPTION
79
+ // if(Config.isEncryptionEnabled)
80
+ // mainExtension += ".enc"
81
+ if (Config.isCompressionEnabled)
82
+ mainExtension += ".gz";
83
+ return mainExtension;
84
+ };
64
85
  _schemaToIdsPath = (schema, prefix = "") => {
65
86
  const RETURN = {};
66
87
  for (const field of schema)
@@ -70,7 +91,7 @@ export default class Inibase {
70
91
  Utils.deepMerge(RETURN, this._schemaToIdsPath(field.children, `${(prefix ?? "") + field.key}.`));
71
92
  }
72
93
  else if (field.id)
73
- RETURN[field.id] = `${(prefix ?? "") + field.key}.inib`;
94
+ RETURN[field.id] = `${(prefix ?? "") + field.key}${this.getFileExtension()}`;
74
95
  return RETURN;
75
96
  };
76
97
  async setTableSchema(tableName, schema) {
@@ -140,7 +161,7 @@ export default class Inibase {
140
161
  schema = await this.getTableSchema(tableName);
141
162
  if (!schema)
142
163
  throw this.throwError("NO_SCHEMA", tableName);
143
- if (!(await File.isExists(join(tablePath, "id.inib"))))
164
+ if (!(await File.isExists(join(tablePath, `id${this.getFileExtension()}`))))
144
165
  throw this.throwError("NO_ITEMS", tableName);
145
166
  return schema;
146
167
  }
@@ -268,7 +289,7 @@ export default class Inibase {
268
289
  const field = Utils.getField(key, schema);
269
290
  if (!field)
270
291
  continue;
271
- const [searchResult, totalLines] = await File.search(join(tablePath, `${key}.inib`), Array.isArray(values) ? "=" : "[]", values, undefined, field.type, field.children, 1, undefined, false, this.salt);
292
+ const [searchResult, totalLines] = await File.search(join(tablePath, `${key}${this.getFileExtension()}`), Array.isArray(values) ? "=" : "[]", values, undefined, field.type, field.children, 1, undefined, false, this.salt);
272
293
  if (searchResult && totalLines > 0)
273
294
  throw this.throwError("FIELD_UNIQUE", [
274
295
  field.key,
@@ -357,7 +378,7 @@ export default class Inibase {
357
378
  _addPathToKeys = (obj, path) => {
358
379
  const newObject = {};
359
380
  for (const key in obj)
360
- newObject[join(path, `${key}.inib`)] = obj[key];
381
+ newObject[join(path, `${key}${this.getFileExtension()}`)] = obj[key];
361
382
  return newObject;
362
383
  };
363
384
  joinPathesContents(mainPath, data) {
@@ -407,7 +428,7 @@ export default class Inibase {
407
428
  async getItemsFromSchema(tableName, schema, linesNumber, options, prefix) {
408
429
  const tablePath = join(this.folder, this.database, tableName);
409
430
  let RETURN = {};
410
- await Promise.all(schema.map(async (field) => {
431
+ for await (const field of schema) {
411
432
  if ((field.type === "array" ||
412
433
  (Array.isArray(field.type) && field.type.includes("array"))) &&
413
434
  field.children) {
@@ -415,82 +436,86 @@ export default class Inibase {
415
436
  if (field.children.filter((children) => children.type === "array" &&
416
437
  Utils.isArrayOfObjects(children.children)).length) {
417
438
  // one of children has array field type and has children array of object = Schema
418
- for (const [index, item] of Object.entries((await this.getItemsFromSchema(tableName, field.children.filter((children) => children.type === "array" &&
419
- Utils.isArrayOfObjects(children.children)), linesNumber, options, `${(prefix ?? "") + field.key}.`)) ?? {}))
420
- this._getItemsFromSchemaHelper(RETURN, item, index, field);
439
+ const childItems = await this.getItemsFromSchema(tableName, field.children.filter((children) => children.type === "array" &&
440
+ Utils.isArrayOfObjects(children.children)), linesNumber, options, `${(prefix ?? "") + field.key}.`);
441
+ if (childItems)
442
+ for (const [index, item] of Object.entries(childItems))
443
+ this._getItemsFromSchemaHelper(RETURN, item, index, field);
421
444
  field.children = field.children.filter((children) => children.type !== "array" ||
422
445
  !Utils.isArrayOfObjects(children.children));
423
446
  }
424
- for (const [index, item] of Object.entries((await this.getItemsFromSchema(tableName, field.children, linesNumber, options, `${(prefix ?? "") + field.key}.`)) ?? {})) {
425
- if (!RETURN[index])
426
- RETURN[index] = {};
427
- if (Utils.isObject(item)) {
428
- if (!Utils.isArrayOfNulls(Object.values(item))) {
429
- if (RETURN[index][field.key])
430
- Object.entries(item).forEach(([key, value], _index) => {
431
- for (let _index = 0; _index < value.length; _index++)
432
- if (RETURN[index][field.key][_index])
433
- Object.assign(RETURN[index][field.key][_index], {
434
- [key]: value[_index],
435
- });
436
- else
437
- RETURN[index][field.key][_index] = {
438
- [key]: value[_index],
439
- };
440
- });
441
- else if (Object.values(item).every((_i) => Utils.isArrayOfArrays(_i) || Array.isArray(_i)) &&
442
- prefix)
443
- RETURN[index][field.key] = item;
444
- else {
445
- RETURN[index][field.key] = [];
446
- Object.entries(item).forEach(([key, value], _ind) => {
447
- if (!Array.isArray(value)) {
448
- RETURN[index][field.key][_ind] = {};
449
- RETURN[index][field.key][_ind][key] = value;
450
- }
451
- else
452
- for (let _i = 0; _i < value.length; _i++) {
453
- if (value[_i] === null ||
454
- (Array.isArray(value[_i]) &&
455
- Utils.isArrayOfNulls(value[_i])))
456
- continue;
457
- if (!RETURN[index][field.key][_i])
458
- RETURN[index][field.key][_i] = {};
459
- RETURN[index][field.key][_i][key] = value[_i];
447
+ const fieldItems = await this.getItemsFromSchema(tableName, field.children, linesNumber, options, `${(prefix ?? "") + field.key}.`);
448
+ if (fieldItems)
449
+ for (const [index, item] of Object.entries(fieldItems)) {
450
+ if (!RETURN[index])
451
+ RETURN[index] = {};
452
+ if (Utils.isObject(item)) {
453
+ if (!Utils.isArrayOfNulls(Object.values(item))) {
454
+ if (RETURN[index][field.key])
455
+ Object.entries(item).forEach(([key, value], _index) => {
456
+ for (let _index = 0; _index < value.length; _index++)
457
+ if (RETURN[index][field.key][_index])
458
+ Object.assign(RETURN[index][field.key][_index], {
459
+ [key]: value[_index],
460
+ });
461
+ else
462
+ RETURN[index][field.key][_index] = {
463
+ [key]: value[_index],
464
+ };
465
+ });
466
+ else if (Object.values(item).every((_i) => Utils.isArrayOfArrays(_i) || Array.isArray(_i)) &&
467
+ prefix)
468
+ RETURN[index][field.key] = item;
469
+ else {
470
+ RETURN[index][field.key] = [];
471
+ Object.entries(item).forEach(([key, value], _ind) => {
472
+ if (!Array.isArray(value)) {
473
+ RETURN[index][field.key][_ind] = {};
474
+ RETURN[index][field.key][_ind][key] = value;
460
475
  }
461
- });
476
+ else
477
+ for (let _i = 0; _i < value.length; _i++) {
478
+ if (value[_i] === null ||
479
+ (Array.isArray(value[_i]) &&
480
+ Utils.isArrayOfNulls(value[_i])))
481
+ continue;
482
+ if (!RETURN[index][field.key][_i])
483
+ RETURN[index][field.key][_i] = {};
484
+ RETURN[index][field.key][_i][key] = value[_i];
485
+ }
486
+ });
487
+ }
462
488
  }
489
+ else
490
+ RETURN[index][field.key] = null;
463
491
  }
464
492
  else
465
- RETURN[index][field.key] = null;
493
+ RETURN[index][field.key] = item;
466
494
  }
467
- else
468
- RETURN[index][field.key] = item;
469
- }
470
495
  }
471
496
  else if (field.children === "table" ||
472
497
  (Array.isArray(field.type) && field.type.includes("table")) ||
473
498
  (Array.isArray(field.children) && field.children.includes("table"))) {
474
499
  if (field.table &&
475
500
  (await File.isExists(join(this.folder, this.database, field.table))) &&
476
- (await File.isExists(join(tablePath, `${(prefix ?? "") + field.key}.inib`)))) {
501
+ (await File.isExists(join(tablePath, `${(prefix ?? "") + field.key}${this.getFileExtension()}`)))) {
477
502
  if (options.columns)
478
503
  options.columns = options.columns
479
504
  .filter((column) => column.includes(`${field.key}.`))
480
505
  .map((column) => column.replace(`${field.key}.`, ""));
481
- const items = await File.get(join(tablePath, `${(prefix ?? "") + field.key}.inib`), linesNumber, field.type, field.children, this.salt);
506
+ const items = await File.get(join(tablePath, `${(prefix ?? "") + field.key}${this.getFileExtension()}`), linesNumber, field.type, field.children, this.salt);
482
507
  if (items)
483
- await Promise.allSettled(Object.entries(items).map(async ([index, item]) => {
508
+ for await (const [index, item] of Object.entries(items)) {
484
509
  if (!RETURN[index])
485
510
  RETURN[index] = {};
486
511
  RETURN[index][field.key] = item
487
512
  ? await this.get(field.table, item, options)
488
513
  : this.getDefaultValue(field);
489
- }));
514
+ }
490
515
  }
491
516
  }
492
- else if (await File.isExists(join(tablePath, `${(prefix ?? "") + field.key}.inib`))) {
493
- const items = await File.get(join(tablePath, `${(prefix ?? "") + field.key}.inib`), linesNumber, field.type, field.children, this.salt);
517
+ else if (await File.isExists(join(tablePath, `${(prefix ?? "") + field.key}${this.getFileExtension()}`))) {
518
+ const items = await File.get(join(tablePath, `${(prefix ?? "") + field.key}${this.getFileExtension()}`), linesNumber, field.type, field.children, this.salt);
494
519
  if (items)
495
520
  for (const [index, item] of Object.entries(items)) {
496
521
  if (!RETURN[index])
@@ -500,40 +525,42 @@ export default class Inibase {
500
525
  }
501
526
  }
502
527
  else if (field.type === "object") {
503
- for await (const [index, item] of Object.entries((await this.getItemsFromSchema(tableName, field.children, linesNumber, options, `${(prefix ?? "") + field.key}.`)) ?? {})) {
504
- if (!RETURN[index])
505
- RETURN[index] = {};
506
- if (Utils.isObject(item)) {
507
- if (!Object.values(item).every((i) => i === null))
508
- RETURN[index][field.key] = item;
528
+ const fieldItems = await this.getItemsFromSchema(tableName, field.children, linesNumber, options, `${(prefix ?? "") + field.key}.`);
529
+ if (fieldItems)
530
+ for (const [index, item] of Object.entries(fieldItems)) {
531
+ if (!RETURN[index])
532
+ RETURN[index] = {};
533
+ if (Utils.isObject(item)) {
534
+ if (!Object.values(item).every((i) => i === null))
535
+ RETURN[index][field.key] = item;
536
+ else
537
+ RETURN[index][field.key] = null;
538
+ }
509
539
  else
510
540
  RETURN[index][field.key] = null;
511
541
  }
512
- else
513
- RETURN[index][field.key] = null;
514
- }
515
542
  }
516
543
  else if (field.type === "table") {
517
544
  if (field.table &&
518
545
  (await File.isExists(join(this.folder, this.database, field.table))) &&
519
- (await File.isExists(join(tablePath, `${(prefix ?? "") + field.key}.inib`)))) {
546
+ (await File.isExists(join(tablePath, `${(prefix ?? "") + field.key}${this.getFileExtension()}`)))) {
520
547
  if (options.columns)
521
548
  options.columns = options.columns
522
549
  .filter((column) => column.includes(`${field.key}.`))
523
550
  .map((column) => column.replace(`${field.key}.`, ""));
524
- const items = await File.get(join(tablePath, `${(prefix ?? "") + field.key}.inib`), linesNumber, "number", undefined, this.salt);
551
+ const items = await File.get(join(tablePath, `${(prefix ?? "") + field.key}${this.getFileExtension()}`), linesNumber, "number", undefined, this.salt);
525
552
  if (items)
526
- await Promise.allSettled(Object.entries(items).map(async ([index, item]) => {
553
+ for await (const [index, item] of Object.entries(items)) {
527
554
  if (!RETURN[index])
528
555
  RETURN[index] = {};
529
556
  RETURN[index][field.key] = item
530
557
  ? await this.get(field.table, item, options)
531
558
  : this.getDefaultValue(field);
532
- }));
559
+ }
533
560
  }
534
561
  }
535
- else if (await File.isExists(join(tablePath, `${(prefix ?? "") + field.key}.inib`))) {
536
- const items = await File.get(join(tablePath, `${(prefix ?? "") + field.key}.inib`), linesNumber, field.type, field.children, this.salt);
562
+ else if (await File.isExists(join(tablePath, `${(prefix ?? "") + field.key}${this.getFileExtension()}`))) {
563
+ const items = await File.get(join(tablePath, `${(prefix ?? "") + field.key}${this.getFileExtension()}`), linesNumber, field.type, field.children, this.salt);
537
564
  if (items)
538
565
  for (const [index, item] of Object.entries(items)) {
539
566
  if (!RETURN[index])
@@ -546,7 +573,7 @@ export default class Inibase {
546
573
  { ...data, [field.key]: this.getDefaultValue(field) },
547
574
  ]));
548
575
  }
549
- }));
576
+ }
550
577
  return RETURN;
551
578
  }
552
579
  async applyCriteria(tableName, schema, options, criteria, allTrue) {
@@ -634,7 +661,7 @@ export default class Inibase {
634
661
  searchOperator = "=";
635
662
  searchComparedAtValue = value;
636
663
  }
637
- const [searchResult, totalLines, linesNumbers] = await File.search(join(tablePath, `${key}.inib`), searchOperator ?? "=", searchComparedAtValue ?? null, searchLogicalOperator, field?.type, field?.children, options.perPage, options.page - 1 * options.perPage + 1, true, this.salt);
664
+ const [searchResult, totalLines, linesNumbers] = await File.search(join(tablePath, `${key}${this.getFileExtension()}`), searchOperator ?? "=", searchComparedAtValue ?? null, searchLogicalOperator, field?.type, field?.children, options.perPage, options.page - 1 * options.perPage + 1, true, this.salt);
638
665
  if (searchResult) {
639
666
  RETURN = Utils.deepMerge(RETURN, Object.fromEntries(Object.entries(searchResult).map(([id, value]) => [
640
667
  id,
@@ -679,7 +706,7 @@ export default class Inibase {
679
706
  }
680
707
  async clearCache(tablePath) {
681
708
  await Promise.all((await readdir(join(tablePath, ".cache")))
682
- ?.filter((fileName) => fileName !== "pagination.inib")
709
+ ?.filter((fileName) => fileName !== `pagination${this.fileExtension}`)
683
710
  .map(async (file) => unlink(join(tablePath, ".cache", file))));
684
711
  }
685
712
  async get(tableName, where, options = {
@@ -713,16 +740,14 @@ export default class Inibase {
713
740
  RETURN = Object.values(await this.getItemsFromSchema(tableName, schema, Array.from({ length: options.perPage }, (_, index) => (options.page - 1) * options.perPage +
714
741
  index +
715
742
  1), options));
716
- if (Config.isCacheEnabled &&
717
- (await File.isExists(join(tablePath, ".cache", "pagination.inib"))))
718
- this.totalItems[`${tableName}-*`] = Number((await File.read(join(tablePath, ".cache", "pagination.inib"), true)).split(",")[1]);
743
+ if (await File.isExists(join(tablePath, ".cache", `pagination${this.fileExtension}`)))
744
+ this.totalItems[`${tableName}-*`] = Number((await File.read(join(tablePath, ".cache", `pagination${this.fileExtension}`), true)).split(",")[1]);
719
745
  else {
720
- let [lastId, totalItems] = await File.get(join(tablePath, "id.inib"), -1, "number", undefined, this.salt, true);
746
+ let [lastId, totalItems] = await File.get(join(tablePath, `id${this.getFileExtension()}`), -1, "number", undefined, this.salt, true);
721
747
  if (lastId)
722
748
  lastId = Number(Object.keys(lastId)?.[0] ?? 0);
723
749
  this.totalItems[`${tableName}-*`] = totalItems;
724
- if (Config.isCacheEnabled)
725
- await File.write(join(tablePath, ".cache", "pagination.inib"), `${lastId},${totalItems}`, true);
750
+ await File.write(join(tablePath, ".cache", `pagination${this.fileExtension}`), `${lastId},${totalItems}`, true);
726
751
  }
727
752
  }
728
753
  else if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
@@ -745,7 +770,7 @@ export default class Inibase {
745
770
  let Ids = where;
746
771
  if (!Array.isArray(Ids))
747
772
  Ids = [Ids];
748
- const [lineNumbers, countItems] = await File.search(join(tablePath, "id.inib"), "[]", Ids.map((id) => Utils.isNumber(id) ? Number(id) : UtilsServer.decodeID(id, this.salt)), undefined, "number", undefined, Ids.length, 0, !this.totalItems[`${tableName}-*`], this.salt);
773
+ const [lineNumbers, countItems] = await File.search(join(tablePath, `id${this.getFileExtension()}`), "[]", Ids.map((id) => Utils.isNumber(id) ? Number(id) : UtilsServer.decodeID(id, this.salt)), undefined, "number", undefined, Ids.length, 0, !this.totalItems[`${tableName}-*`], this.salt);
749
774
  if (!lineNumbers)
750
775
  throw this.throwError("NO_RESULTS", tableName);
751
776
  if (onlyLinesNumbers)
@@ -762,7 +787,7 @@ export default class Inibase {
762
787
  let cachedFilePath = "";
763
788
  // Criteria
764
789
  if (Config.isCacheEnabled)
765
- cachedFilePath = join(tablePath, ".cache", `${UtilsServer.hashString(inspect(where, { sorted: true }))}.inib`);
790
+ cachedFilePath = join(tablePath, ".cache", `${UtilsServer.hashString(inspect(where, { sorted: true }))}${this.fileExtension}`);
766
791
  if (Config.isCacheEnabled && (await File.isExists(cachedFilePath))) {
767
792
  const cachedItems = (await File.read(cachedFilePath, true)).split(",");
768
793
  this.totalItems[`${tableName}-*`] = cachedItems.length;
@@ -815,15 +840,14 @@ export default class Inibase {
815
840
  let lastId = 0, totalItems = 0, renameList = [];
816
841
  try {
817
842
  await File.lock(join(tablePath, ".tmp"), keys);
818
- if (await File.isExists(join(tablePath, "id.inib"))) {
819
- if (Config.isCacheEnabled &&
820
- (await File.isExists(join(tablePath, ".cache", "pagination.inib"))))
821
- [lastId, totalItems] = (await File.read(join(tablePath, ".cache", "pagination.inib"), true))
843
+ if (await File.isExists(join(tablePath, `id${this.getFileExtension()}`))) {
844
+ if (await File.isExists(join(tablePath, ".cache", `pagination${this.fileExtension}`)))
845
+ [lastId, totalItems] = (await File.read(join(tablePath, ".cache", `pagination${this.fileExtension}`), true))
822
846
  .split(",")
823
847
  .map(Number);
824
848
  else {
825
849
  let lastIdObj = null;
826
- [lastIdObj, totalItems] = await File.get(join(tablePath, "id.inib"), -1, "number", undefined, this.salt, true);
850
+ [lastIdObj, totalItems] = await File.get(join(tablePath, `id${this.getFileExtension()}`), -1, "number", undefined, this.salt, true);
827
851
  if (lastIdObj)
828
852
  lastId = Number(Object.keys(lastIdObj)?.[0] ?? 0);
829
853
  }
@@ -852,10 +876,9 @@ export default class Inibase {
852
876
  await Promise.all(renameList.map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
853
877
  renameList = [];
854
878
  totalItems += Array.isArray(RETURN) ? RETURN.length : 1;
855
- if (Config.isCacheEnabled) {
879
+ if (Config.isCacheEnabled)
856
880
  await this.clearCache(tablePath);
857
- await File.write(join(tablePath, ".cache", "pagination.inib"), `${lastId},${totalItems}`, true);
858
- }
881
+ await File.write(join(tablePath, ".cache", `pagination${this.fileExtension}`), `${lastId},${totalItems}`, true);
859
882
  if (returnPostedData)
860
883
  return this.get(tableName, Config.isReverseEnabled
861
884
  ? Array.isArray(RETURN)
@@ -895,13 +918,12 @@ export default class Inibase {
895
918
  return this.put(tableName, data, data.id);
896
919
  }
897
920
  let totalItems;
898
- if (Config.isCacheEnabled &&
899
- (await File.isExists(join(tablePath, ".cache", "pagination.inib"))))
900
- totalItems = (await File.read(join(tablePath, ".cache", "pagination.inib"), true))
921
+ if (await File.isExists(join(tablePath, ".cache", `pagination${this.fileExtension}`)))
922
+ totalItems = (await File.read(join(tablePath, ".cache", `pagination${this.fileExtension}`), true))
901
923
  .split(",")
902
924
  .map(Number)[1];
903
925
  else
904
- totalItems = await File.count(join(tablePath, "id.inib"));
926
+ totalItems = await File.count(join(tablePath, `id${this.getFileExtension()}`));
905
927
  const pathesContents = this.joinPathesContents(tablePath, {
906
928
  ...(({ id, ...restOfData }) => restOfData)(data),
907
929
  updatedAt: Date.now(),
@@ -998,26 +1020,23 @@ export default class Inibase {
998
1020
  if ((Array.isArray(where) && where.every(Utils.isNumber)) ||
999
1021
  Utils.isNumber(where)) {
1000
1022
  // "where" in this case, is the line(s) number(s) and not id(s)
1001
- const files = (await readdir(tablePath))?.filter((fileName) => fileName.endsWith(".inib"));
1023
+ const files = (await readdir(tablePath))?.filter((fileName) => fileName.endsWith(this.getFileExtension()));
1002
1024
  if (files.length) {
1003
- if (!_id)
1004
- _id = Object.entries((await File.get(join(tablePath, "id.inib"), where, "number", undefined, this.salt)) ?? {}).map(([_, id]) => UtilsServer.encodeID(id, this.salt));
1005
- if (!_id.length)
1006
- throw this.throwError("NO_RESULTS", tableName);
1007
1025
  try {
1008
1026
  await File.lock(join(tablePath, ".tmp"));
1009
1027
  await Promise.all(files.map(async (file) => renameList.push(await File.remove(join(tablePath, file), where))));
1010
1028
  await Promise.all(renameList.map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1011
- if (Config.isCacheEnabled) {
1029
+ if (Config.isCacheEnabled)
1012
1030
  await this.clearCache(tablePath);
1013
- if (await File.isExists(join(tablePath, ".cache", "pagination.inib"))) {
1014
- const [lastId, totalItems] = (await File.read(join(tablePath, ".cache", "pagination.inib"), true))
1015
- .split(",")
1016
- .map(Number);
1017
- await File.write(join(tablePath, ".cache", "pagination.inib"), `${lastId},${totalItems - (Array.isArray(where) ? where.length : 1)}`, true);
1018
- }
1031
+ if (await File.isExists(join(tablePath, ".cache", `pagination${this.fileExtension}`))) {
1032
+ const [lastId, totalItems] = (await File.read(join(tablePath, ".cache", `pagination${this.fileExtension}`), true))
1033
+ .split(",")
1034
+ .map(Number);
1035
+ await File.write(join(tablePath, ".cache", `pagination${this.fileExtension}`), `${lastId},${totalItems - (Array.isArray(where) ? where.length : 1)}`, true);
1019
1036
  }
1020
- return Array.isArray(_id) && _id.length === 1 ? _id[0] : _id;
1037
+ if (_id)
1038
+ return Array.isArray(_id) && _id.length === 1 ? _id[0] : _id;
1039
+ return where;
1021
1040
  }
1022
1041
  finally {
1023
1042
  if (renameList.length)
@@ -1040,7 +1059,7 @@ export default class Inibase {
1040
1059
  if (!Array.isArray(columns))
1041
1060
  columns = [columns];
1042
1061
  for await (const column of columns) {
1043
- const columnPath = join(tablePath, `${column}.inib`);
1062
+ const columnPath = join(tablePath, `${column}${this.getFileExtension()}`);
1044
1063
  if (await File.isExists(columnPath)) {
1045
1064
  if (where) {
1046
1065
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true, schema);
@@ -1060,7 +1079,7 @@ export default class Inibase {
1060
1079
  if (!Array.isArray(columns))
1061
1080
  columns = [columns];
1062
1081
  for await (const column of columns) {
1063
- const columnPath = join(tablePath, `${column}.inib`);
1082
+ const columnPath = join(tablePath, `${column}${this.getFileExtension()}`);
1064
1083
  if (await File.isExists(columnPath)) {
1065
1084
  if (where) {
1066
1085
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true, schema);
@@ -1080,7 +1099,7 @@ export default class Inibase {
1080
1099
  if (!Array.isArray(columns))
1081
1100
  columns = [columns];
1082
1101
  for await (const column of columns) {
1083
- const columnPath = join(tablePath, `${column}.inib`);
1102
+ const columnPath = join(tablePath, `${column}${this.getFileExtension()}`);
1084
1103
  if (await File.isExists(columnPath)) {
1085
1104
  if (where) {
1086
1105
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true, schema);
@@ -1098,7 +1117,6 @@ export default class Inibase {
1098
1117
  page: 1,
1099
1118
  perPage: 15,
1100
1119
  }) {
1101
- // TO-DO: Cache Results based on "Columns and Sort Direction"
1102
1120
  const tablePath = join(this.folder, this.database, tableName), schema = await this.getSchemaWhenTableNotEmpty(tableName);
1103
1121
  // Default values for page and perPage
1104
1122
  options.page = options.page || 1;
@@ -1122,7 +1140,7 @@ export default class Inibase {
1122
1140
  cacheKey = UtilsServer.hashString(inspect(sortArray, { sorted: true }));
1123
1141
  if (where) {
1124
1142
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true, schema);
1125
- keepItems = Object.values((await File.get(join(tablePath, "id.inib"), lineNumbers, "number", undefined, this.salt)) ?? {}).map(Number);
1143
+ keepItems = Object.values((await File.get(join(tablePath, `id${this.getFileExtension()}`), lineNumbers, "number", undefined, this.salt)) ?? {}).map(Number);
1126
1144
  isLineNumbers = false;
1127
1145
  if (!keepItems.length)
1128
1146
  throw this.throwError("NO_RESULTS", tableName);
@@ -1132,7 +1150,7 @@ export default class Inibase {
1132
1150
  keepItems = Array.from({ length: options.perPage }, (_, index) => (options.page - 1) * options.perPage +
1133
1151
  index +
1134
1152
  1);
1135
- const filesPathes = [["id", true], ...sortArray].map((column) => join(tablePath, `${column[0]}.inib`));
1153
+ const filesPathes = [["id", true], ...sortArray].map((column) => join(tablePath, `${column[0]}${this.getFileExtension()}`));
1136
1154
  // Construct the paste command to merge files and filter lines by IDs
1137
1155
  const pasteCommand = `paste ${filesPathes.join(" ")}`;
1138
1156
  // Construct the sort command dynamically based on the number of files for sorting
@@ -1157,10 +1175,10 @@ export default class Inibase {
1157
1175
  await File.lock(join(tablePath, ".tmp"), cacheKey);
1158
1176
  // Combine the commands
1159
1177
  // Execute the command synchronously
1160
- const { stdout, stderr } = await UtilsServer.exec(Config.isCacheEnabled
1161
- ? (await File.isExists(join(tablePath, ".cache", `${cacheKey}.inib`)))
1162
- ? `${awkCommand} ${join(tablePath, ".cache", `${cacheKey}.inib`)}`
1163
- : `${pasteCommand} | ${sortCommand} -o ${join(tablePath, ".cache", `${cacheKey}.inib`)} && ${awkCommand} ${join(tablePath, ".cache", `${cacheKey}.inib`)}`
1178
+ const { stdout } = await UtilsServer.exec(Config.isCacheEnabled
1179
+ ? (await File.isExists(join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)))
1180
+ ? `${awkCommand} ${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)}`
1181
+ : `${pasteCommand} | ${sortCommand} -o ${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)} && ${awkCommand} ${join(tablePath, ".cache", `${cacheKey}${this.fileExtension}`)}`
1164
1182
  : `${pasteCommand} | ${sortCommand} | ${awkCommand}`, {
1165
1183
  encoding: "utf-8",
1166
1184
  });
@@ -1169,7 +1187,7 @@ export default class Inibase {
1169
1187
  const outputArray = lines.map((line) => {
1170
1188
  const splitedFileColumns = line.split("\t"); // Assuming tab-separated columns
1171
1189
  const outputObject = {};
1172
- // Extract values for each file, including "id.inib"
1190
+ // Extract values for each file, including `id${this.getFileExtension()}`
1173
1191
  filesPathes.forEach((fileName, index) => {
1174
1192
  const Field = Utils.getField(parse(fileName).name, schema);
1175
1193
  if (Field)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inibase",
3
- "version": "1.0.0-rc.57",
3
+ "version": "1.0.0-rc.58",
4
4
  "author": {
5
5
  "name": "Karim Amahtil",
6
6
  "email": "karim.amahtil@gmail.com"
@@ -47,6 +47,9 @@
47
47
  "pocketbase"
48
48
  ],
49
49
  "license": "MIT",
50
+ "bin": {
51
+ "inibase": "./dist/cli.js"
52
+ },
50
53
  "type": "module",
51
54
  "types": "./dist",
52
55
  "typesVersions": {
@@ -73,13 +76,14 @@
73
76
  "tinybench": "^2.6.0"
74
77
  },
75
78
  "dependencies": {
79
+ "commander": "^12.0.0",
80
+ "dotenv": "^16.4.5",
76
81
  "inison": "^1.0.0-rc.2"
77
82
  },
78
83
  "scripts": {
79
84
  "build": "tsc",
80
- "test": "npx tsx watch --expose-gc --env-file=.env ./index.test",
81
- "benchmark": "npx tsx --env-file=.env ./benchmark/index",
82
- "benchmark:single": "npx tsx --expose-gc --env-file=.env ./benchmark/single",
83
- "benchmark:bulk": "npx tsx --expose-gc --env-file=.env ./benchmark/bulk"
85
+ "benchmark": "tsx --env-file=.env ./benchmark/index",
86
+ "benchmark:single": "tsx --expose-gc --env-file=.env ./benchmark/single",
87
+ "benchmark:bulk": "tsx --expose-gc --env-file=.env ./benchmark/bulk"
84
88
  }
85
89
  }