inibase 1.8.0 → 2.0.0

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
@@ -791,6 +791,7 @@ await db.get("user", undefined, { sort: {age: -1, username: "asc"} });
791
791
  | DELETE | 118 ms (0.59 mb) | 113 ms (0.51 mb) | 103 ms (3.14 mb) |
792
792
 
793
793
  > Default testing uses a table with username, email, and password fields, ensuring password encryption is included in the process<br>
794
+ > Results are measured on a default table plus dedicated tables with `prepend`, `compression`, and `decodeID` configs enabled<br>
794
795
  > To run benchmarks, install _typescript_ & _[tsx](https://github.com/privatenumber/tsx)_ globally and run `benchmark` by default bulk, for single use `benchmark --single|-s`
795
796
 
796
797
  ## Roadmap
package/dist/file.d.ts CHANGED
@@ -30,7 +30,8 @@ export declare const encode: (input: string | number | boolean | null | (string
30
30
  */
31
31
  export declare const decode: (input: string | null | number, field: Field & {
32
32
  databasePath?: string;
33
- }) => string | number | boolean | null | (string | number | null | boolean)[];
33
+ }) => string | number | boolean | null | undefined | (string | number | null | boolean)[];
34
+ export declare function _groupIntoRanges(arr: number[], action?: "p" | "d"): string | any[];
34
35
  /**
35
36
  * Asynchronously reads and decodes data from a file at specified line numbers.
36
37
  * Decodes each line based on specified field types and an optional secret key.
@@ -45,11 +46,11 @@ export declare const decode: (input: string | null | number, field: Field & {
45
46
  */
46
47
  export declare function get(filePath: string, lineNumbers?: number | number[], field?: Field & {
47
48
  databasePath?: string;
48
- }, readWholeFile?: false): Promise<Record<number, string | number | boolean | null | (string | number | boolean | (string | number | boolean)[] | null)[]> | null>;
49
+ }, readWholeFile?: false): Promise<Record<number, string | number | boolean | null | undefined | (string | number | boolean | (string | number | boolean)[] | null)[]> | null>;
49
50
  export declare function get(filePath: string, lineNumbers: undefined | number | number[], field: undefined | (Field & {
50
51
  databasePath?: string;
51
52
  }), readWholeFile: true): Promise<[
52
- Record<number, string | number | boolean | null | (string | number | boolean | (string | number | boolean)[] | null)[]> | null,
53
+ Record<number, string | number | boolean | null | undefined | (string | number | boolean | (string | number | boolean)[] | null)[]> | null,
53
54
  number
54
55
  ]>;
55
56
  /**
@@ -62,7 +63,7 @@ export declare function get(filePath: string, lineNumbers: undefined | number |
62
63
  *
63
64
  * Note: If the file doesn't exist and replacements is an object, it creates a new file with the specified replacements.
64
65
  */
65
- export declare const replace: (filePath: string, replacements: string | number | boolean | null | (string | number | boolean | null)[] | Record<number, string | boolean | number | null | (string | boolean | number | null)[]>, totalItems?: number) => Promise<string[]>;
66
+ export declare const replace: (filePath: string, replacements: string | number | boolean | null | (string | number | boolean | null)[] | Record<number, string | boolean | number | null | (string | boolean | number | null)[]>, totalItems?: number) => Promise<(string | null)[]>;
66
67
  /**
67
68
  * Asynchronously appends data to the end of a file.
68
69
  *
@@ -71,7 +72,7 @@ export declare const replace: (filePath: string, replacements: string | number |
71
72
  * @returns Promise<string[]>. Modifies the file by appending data.
72
73
  *
73
74
  */
74
- export declare const append: (filePath: string, data: string | number | (string | number)[]) => Promise<string[]>;
75
+ export declare const append: (filePath: string, data: string | number | (string | number)[]) => Promise<(string | null)[]>;
75
76
  /**
76
77
  * Asynchronously prepends data to the beginning of a file.
77
78
  *
@@ -80,7 +81,7 @@ export declare const append: (filePath: string, data: string | number | (string
80
81
  * @returns Promise<string[]>. Modifies the file by appending data.
81
82
  *
82
83
  */
83
- export declare const prepend: (filePath: string, data: string | number | (string | number)[]) => Promise<string[]>;
84
+ export declare const prepend: (filePath: string, data: string | number | (string | number)[]) => Promise<(string | null)[]>;
84
85
  /**
85
86
  * Asynchronously removes specified lines from a file.
86
87
  *
@@ -90,7 +91,7 @@ export declare const prepend: (filePath: string, data: string | number | (string
90
91
  *
91
92
  * Note: Creates a temporary file during the process and replaces the original file with it after removing lines.
92
93
  */
93
- export declare const remove: (filePath: string, linesToDelete: number | number[]) => Promise<string[]>;
94
+ export declare const remove: (filePath: string, linesToDelete: number | number[]) => Promise<(string | null)[]>;
94
95
  /**
95
96
  * Asynchronously searches a file for lines matching specified criteria, using comparison and logical operators.
96
97
  *
@@ -110,7 +111,7 @@ export declare const remove: (filePath: string, linesToDelete: number | number[]
110
111
  */
111
112
  export declare const search: (filePath: string, operator: ComparisonOperator | ComparisonOperator[], comparedAtValue: string | number | boolean | null | (string | number | boolean | null)[], logicalOperator?: "and" | "or", searchIn?: Set<number>, field?: Field & {
112
113
  databasePath?: string;
113
- }, limit?: number, offset?: number, readWholeFile?: boolean) => Promise<[Record<number, string | number | boolean | null | (string | number | boolean | null)[]> | null, number, Set<number> | null]>;
114
+ }, limit?: number, offset?: number, readWholeFile?: boolean) => Promise<[Record<number, string | number | boolean | null | undefined | (string | number | boolean | null)[]> | null, number, Set<number> | null]>;
114
115
  export declare const sum: (fp: string, ln?: number | number[]) => Promise<number>;
115
116
  export declare const min: (fp: string, ln?: number | number[]) => Promise<number>;
116
117
  export declare const max: (fp: string, ln?: number | number[]) => Promise<number>;
package/dist/file.js CHANGED
@@ -129,8 +129,10 @@ export const encode = (input) => Array.isArray(input)
129
129
  const unSecureString = (input) => {
130
130
  if (isNumber(input))
131
131
  return String(input).at(0) === "0" ? input : Number(input);
132
+ // Fast path: the common case has no `\n` escape sequence, so avoid
133
+ // allocating a replacement string (and a fresh RegExp) per cell.
132
134
  if (typeof input === "string")
133
- return input.replace(/\\n/g, "\n") || null;
135
+ return input.includes("\\n") ? input.replaceAll("\\n", "\n") || null : input;
134
136
  return null;
135
137
  };
136
138
  /**
@@ -156,7 +158,8 @@ const decodeHelper = (value, field) => {
156
158
  return value.map((v) => decode(v, {
157
159
  ...field,
158
160
  type: Array.isArray(field.children)
159
- ? detectFieldType(v, field.children)
161
+ ? (detectFieldType(v, field.children) ??
162
+ field.children[0])
160
163
  : field.children,
161
164
  }));
162
165
  break;
@@ -186,15 +189,33 @@ export const decode = (input, field) => {
186
189
  return undefined;
187
190
  // Detect the fieldType based on the input and the provided array of possible types.
188
191
  // Decode the input using the decodeHelper function.
189
- return decodeHelper(typeof input === "string"
190
- ? isStringified(input)
191
- ? Inison.unstringify(input)
192
- : unSecureString(input)
193
- : input, Array.isArray(field.type)
194
- ? { ...field, type: detectFieldType(String(input), field.type) }
192
+ let value = input;
193
+ if (typeof input === "string") {
194
+ if (isStringified(input)) {
195
+ try {
196
+ value = Inison.unstringify(input);
197
+ }
198
+ catch {
199
+ // The stored string merely *starts with* `{` or `[`
200
+ // (e.g. a template binding like `{{item.username}}`
201
+ // or a literal like `{hello world`) but is not valid
202
+ // Inison. Treat it as a plain string so a single row
203
+ // never breaks reads of the whole file/table.
204
+ value = unSecureString(input);
205
+ }
206
+ }
207
+ else
208
+ value = unSecureString(input);
209
+ }
210
+ return decodeHelper(value, Array.isArray(field.type)
211
+ ? {
212
+ ...field,
213
+ type: detectFieldType(String(input), field.type) ??
214
+ field.type[0],
215
+ }
195
216
  : field);
196
217
  };
197
- function _groupIntoRanges(arr, action = "p") {
218
+ export function _groupIntoRanges(arr, action = "p") {
198
219
  if (arr.length === 0)
199
220
  return [];
200
221
  arr.sort((a, b) => a - b); // Ensure the array is sorted
@@ -224,10 +245,14 @@ export async function get(filePath, lineNumbers, field, readWholeFile = false) {
224
245
  const rl = createReadLineInternface(filePath, fileHandle);
225
246
  const lines = {};
226
247
  let linesCount = 0;
248
+ const config = field ?? {
249
+ key: "BLABLA",
250
+ type: "string",
251
+ };
227
252
  if (!lineNumbers) {
228
253
  for await (const line of rl) {
229
254
  linesCount++;
230
- lines[linesCount] = decode(line, field);
255
+ lines[linesCount] = decode(line, config);
231
256
  }
232
257
  }
233
258
  else if (lineNumbers === -1) {
@@ -237,7 +262,7 @@ export async function get(filePath, lineNumbers, field, readWholeFile = false) {
237
262
  : `sed -n '$p' ${escapedFilePath}`;
238
263
  const foundedLine = (await exec(command)).stdout.trimEnd();
239
264
  if (foundedLine)
240
- lines[linesCount] = decode(foundedLine, field);
265
+ lines[linesCount] = decode(foundedLine, config);
241
266
  }
242
267
  else {
243
268
  lineNumbers = Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers];
@@ -249,7 +274,7 @@ export async function get(filePath, lineNumbers, field, readWholeFile = false) {
249
274
  linesCount++;
250
275
  if (!lineNumbersArray.has(linesCount))
251
276
  continue;
252
- lines[linesCount] = decode(line, field);
277
+ lines[linesCount] = decode(line, config);
253
278
  lineNumbersArray.delete(linesCount);
254
279
  }
255
280
  return [lines, linesCount];
@@ -261,7 +286,7 @@ export async function get(filePath, lineNumbers, field, readWholeFile = false) {
261
286
  const foundedLines = (await exec(command)).stdout.trimEnd().split("\n");
262
287
  let index = 0;
263
288
  for (const line of foundedLines) {
264
- lines[lineNumbers[index]] = decode(line, field);
289
+ lines[lineNumbers[index]] = decode(line, config);
265
290
  index++;
266
291
  }
267
292
  }
@@ -561,7 +586,7 @@ function buildEqualsPatterns(comparedAtValue, field) {
561
586
  for (const form of [...new Set(forms)]) {
562
587
  let valid = false;
563
588
  try {
564
- valid = compare("=", decode(form, field), value, type);
589
+ valid = compare("=", decode(form, field) ?? null, value, type);
565
590
  }
566
591
  catch {
567
592
  valid = false;
@@ -578,7 +603,7 @@ function buildEqualsPatterns(comparedAtValue, field) {
578
603
  const form = String(Number(value));
579
604
  let valid = false;
580
605
  try {
581
- valid = compare("=", decode(form, field), value, type);
606
+ valid = compare("=", decode(form, field) ?? null, value, type);
582
607
  }
583
608
  catch {
584
609
  valid = false;
@@ -607,7 +632,7 @@ function buildEqualsPatterns(comparedAtValue, field) {
607
632
  const form = String(encoded);
608
633
  let valid = false;
609
634
  try {
610
- valid = compare("=", decode(form, field), value, type);
635
+ valid = compare("=", decode(form, field) ?? null, value, type);
611
636
  }
612
637
  catch {
613
638
  valid = false;
@@ -642,7 +667,7 @@ async function searchEqualsNative(filePath, patterns, field, searchIn, comparedA
642
667
  // (decode() eagerly unstringifies them), so a native whole-line grep could
643
668
  // silently miss matches that live inside those containers. Detect such lines
644
669
  // up front; when any are present, fall back to the JS reader.
645
- let probe;
670
+ let probe = "";
646
671
  try {
647
672
  ({ stdout: probe } = await exec(`LC_ALL=C ${source} | LC_ALL=C grep -a -m 1 -E '^[[{]'`, { maxBuffer: 1024 * 1024 * 4 }));
648
673
  }
@@ -696,8 +721,8 @@ async function searchEqualsNative(filePath, patterns, field, searchIn, comparedA
696
721
  // Defensive verification: the raw line must decode to one of the compared
697
722
  // values (guaranteed by construction, kept as a safety net).
698
723
  const verifies = Array.isArray(comparedAtValue)
699
- ? comparedAtValue.some((value) => compare("=", decodedLine, value, field.type))
700
- : compare("=", decodedLine, comparedAtValue, field.type);
724
+ ? comparedAtValue.some((value) => compare("=", decodedLine ?? null, value, field.type))
725
+ : compare("=", decodedLine ?? null, comparedAtValue, field.type);
701
726
  if (!verifies)
702
727
  return null;
703
728
  matchingLines[lineNumber] = decodedLine;
@@ -724,14 +749,13 @@ async function searchEqualsNative(filePath, patterns, field, searchIn, comparedA
724
749
  */
725
750
  export const search = async (filePath, operator, comparedAtValue, logicalOperator, searchIn, field, limit, offset, readWholeFile) => {
726
751
  // Native fast path for exact-equality searches (whole-line `grep -x -F`).
727
- const fieldType = field?.type;
728
752
  if (operator === "=" &&
729
753
  !Array.isArray(operator) &&
730
754
  !logicalOperator &&
731
755
  comparedAtValue !== null &&
732
756
  comparedAtValue !== undefined &&
733
- typeof fieldType === "string" &&
734
- EQUALS_FAST_TYPES.has(fieldType)) {
757
+ typeof field?.type === "string" &&
758
+ EQUALS_FAST_TYPES.has(field.type)) {
735
759
  const patterns = buildEqualsPatterns(comparedAtValue, field);
736
760
  if (patterns) {
737
761
  const fastResult = await searchEqualsNative(filePath, patterns, field, searchIn, comparedAtValue, limit, offset, readWholeFile);
@@ -745,13 +769,17 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
745
769
  let linesCount = 0;
746
770
  const linesNumbers = new Set();
747
771
  let fileHandle = null;
772
+ const config = field ?? {
773
+ key: "BLABLA",
774
+ type: "string",
775
+ };
748
776
  const meetsConditions = (value) => (Array.isArray(operator) &&
749
777
  Array.isArray(comparedAtValue) &&
750
778
  ((logicalOperator === "or" &&
751
- operator.some((single_operator, index) => compare(single_operator, value, comparedAtValue[index], field.type))) ||
752
- operator.every((single_operator, index) => compare(single_operator, value, comparedAtValue[index], field.type)))) ||
779
+ operator.some((single_operator, index) => compare(single_operator, value, comparedAtValue[index], config.type))) ||
780
+ operator.every((single_operator, index) => compare(single_operator, value, comparedAtValue[index], config.type)))) ||
753
781
  (!Array.isArray(operator) &&
754
- compare(operator, value, comparedAtValue, field.type));
782
+ compare(operator, value, comparedAtValue, config.type));
755
783
  try {
756
784
  // Open the file for reading.
757
785
  fileHandle = await open(filePath, "r");
@@ -766,7 +794,7 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
766
794
  (!searchIn.has(linesCount) || searchIn.has(-linesCount)))
767
795
  continue;
768
796
  // Decode the line for comparison.
769
- const decodedLine = decode(line, field);
797
+ const decodedLine = decode(line, config);
770
798
  // Check if the line meets the specified conditions based on comparison and logical operators.
771
799
  const doesMeetCondition = (Array.isArray(decodedLine) &&
772
800
  operator !== "=" &&
package/dist/index.d.ts CHANGED
@@ -73,6 +73,11 @@ export default class Inibase {
73
73
  language: ErrorLang;
74
74
  fileExtension: string;
75
75
  totalItems: Map<string, number>;
76
+ /** Tracks whether a decodeID table's stored ids are the dense sequence
77
+ * 1..rowCount (no rows ever deleted). When true, `get`/`put`/`delete` can
78
+ * resolve numeric ids to line numbers arithmetically instead of scanning
79
+ * the id file. Set false by any partial row deletion. */
80
+ private idDensity;
76
81
  private databasePath;
77
82
  private uniqueMap;
78
83
  private schemaFileExtension;
@@ -134,6 +139,14 @@ export default class Inibase {
134
139
  private processSchemaData;
135
140
  private isSimpleField;
136
141
  private processSimpleField;
142
+ /**
143
+ * Batched read for top-level simple columns: a single `paste | sed` child
144
+ * process returns every requested line of every column at once instead of
145
+ * spawning one `sed`/`gunzip|sed` child process per column. Returning null
146
+ * (e.g. when any column file is missing, or the shell command fails) makes
147
+ * the caller fall back to the existing per-column reads.
148
+ */
149
+ private processSimpleFieldsBatch;
137
150
  private isArrayField;
138
151
  private processArrayField;
139
152
  private isObjectField;
@@ -198,7 +211,16 @@ export default class Inibase {
198
211
  * @param {(number | string | (number | string)[] | Criteria)} [where]
199
212
  * @return {boolean | null} {(Promise<boolean | null>)}
200
213
  */
201
- delete(tableName: string, where?: number | string | (number | string)[] | Criteria, _whereIsLinesNumbers?: boolean): Promise<boolean | null>;
214
+ delete(tableName: string, where?: number | string | (number | string)[] | Criteria, _whereIsLinesNumbers?: boolean, _cascadeGuard?: Set<string>): Promise<boolean | null>;
215
+ /**
216
+ * Cascade delete: remove rows in other tables whose `table`-typed schema
217
+ * fields reference the given rows. Reference columns store the numeric
218
+ * (line-number) id of the referenced row, so deleted ids are matched
219
+ * against them directly. Recursion into child tables happens through
220
+ * `delete` itself (which calls this method again); the `guard` set keeps
221
+ * deep/cyclic reference chains from re-processing the same (table, line).
222
+ */
223
+ private cascadeDelete;
202
224
  /**
203
225
  * Generate sum of column(s) in a table
204
226
  *
package/dist/index.js CHANGED
@@ -40,6 +40,11 @@ export default class Inibase {
40
40
  language;
41
41
  fileExtension = ".txt";
42
42
  totalItems;
43
+ /** Tracks whether a decodeID table's stored ids are the dense sequence
44
+ * 1..rowCount (no rows ever deleted). When true, `get`/`put`/`delete` can
45
+ * resolve numeric ids to line numbers arithmetically instead of scanning
46
+ * the id file. Set false by any partial row deletion. */
47
+ idDensity = new Map();
43
48
  databasePath;
44
49
  uniqueMap;
45
50
  schemaFileExtension = process.env.INIBASE_SCHEMA_EXTENSION ?? "json";
@@ -145,6 +150,8 @@ export default class Inibase {
145
150
  await writeFile(join(tablePath, ".cache.config"), "");
146
151
  if (config.prepend)
147
152
  await writeFile(join(tablePath, ".prepend.config"), "");
153
+ if (config.decodeID)
154
+ await writeFile(join(tablePath, ".decodeID.config"), "");
148
155
  }
149
156
  if (schema) {
150
157
  const lastSchemaID = { value: 0 };
@@ -156,6 +163,7 @@ export default class Inibase {
156
163
  else
157
164
  await writeFile(join(tablePath, "0.schema"), "");
158
165
  await writeFile(join(tablePath, "0-0.pagination"), "");
166
+ this.idDensity.set(tableName, true);
159
167
  }
160
168
  // Function to replace the string in one schema file
161
169
  async replaceStringInFile(filePath, targetString, replaceString) {
@@ -213,6 +221,23 @@ export default class Inibase {
213
221
  await rename(schemaIdFilePath, join(tablePath, `${lastSchemaID.value}.schema`));
214
222
  else
215
223
  await writeFile(join(tablePath, `${lastSchemaID.value}.schema`), "");
224
+ // Fields added by this migration have no backing file yet. If the
225
+ // first post after the migration writes such a file from scratch it
226
+ // starts at line 1 and every existing row becomes misaligned (the
227
+ // new value lands on the wrong record). Materialize missing column
228
+ // files padded with one empty line per existing row so appends keep
229
+ // line-aligned with the other column files (decode("") is
230
+ // undefined/null, matching the "no value yet" semantics).
231
+ let totalLines = 0;
232
+ for await (const paginationFileName of glob("*.pagination", {
233
+ cwd: tablePath,
234
+ }))
235
+ totalLines = parse(paginationFileName).name.split("-").map(Number)[1];
236
+ await Promise.allSettled(schema.map(async ({ key }) => {
237
+ const filePath = join(tablePath, `${key}${this.getFileExtension(tableName)}`);
238
+ if (!(await File.isExists(filePath)))
239
+ await File.write(filePath, "\n".repeat(totalLines));
240
+ }));
216
241
  }
217
242
  if (config) {
218
243
  if (config.compression !== undefined &&
@@ -442,9 +467,11 @@ export default class Inibase {
442
467
  }
443
468
  }
444
469
  async validateTableData(tableName, data, skipRequiredField = false) {
445
- const clonedData = structuredClone(data);
470
+ // `data` is always a private clone owned by the caller (post/put already
471
+ // cloned it once), so validate in place instead of re-cloning — otherwise
472
+ // every bulk write holds several full copies of the payload in memory.
446
473
  // Skip ID and (created|updated)At
447
- this.validateData(clonedData, globalConfig[this.databasePath].tables
474
+ this.validateData(data, globalConfig[this.databasePath].tables
448
475
  ?.get(tableName)
449
476
  ?.schema?.slice(1, -2) ?? [], skipRequiredField);
450
477
  await this.checkUnique(tableName);
@@ -579,7 +606,11 @@ export default class Inibase {
579
606
  this.uniqueMap = new Map();
580
607
  }
581
608
  formatData(data, schema, formatOnlyAvailiableKeys) {
582
- const clonedData = structuredClone(data);
609
+ // formatData only reads its input (all transformations produce new
610
+ // values), so no defensive clone is needed. Callers pass data they no
611
+ // longer need, and skipping the copy halves the payload footprint of
612
+ // every bulk post/put.
613
+ const clonedData = data;
583
614
  if (Utils.isArrayOfObjects(clonedData))
584
615
  return clonedData.map((singleData) => this.formatData(singleData, schema, formatOnlyAvailiableKeys));
585
616
  if (Utils.isObject(clonedData)) {
@@ -717,7 +748,26 @@ export default class Inibase {
717
748
  }
718
749
  async processSchemaData(tableName, schema, linesNumber, options, prefix) {
719
750
  const RETURN = {};
751
+ // Fast path: read every top-level simple column in one `paste | sed`
752
+ // child process instead of spawning one child process per column.
753
+ // Nested (prefixed) schemas keep the per-column path.
754
+ let batchedKeys = null;
755
+ if (!prefix && linesNumber?.length) {
756
+ const simpleFields = schema.filter((field) => this.isSimpleField(field.type));
757
+ const batched = await this.processSimpleFieldsBatch(tableName, simpleFields, linesNumber);
758
+ if (batched) {
759
+ batchedKeys = new Set(simpleFields.map((field) => field.key));
760
+ for (const [line, row] of Object.entries(batched)) {
761
+ if (!RETURN[line])
762
+ RETURN[line] = {};
763
+ Object.assign(RETURN[line], row);
764
+ }
765
+ }
766
+ }
720
767
  for (const field of schema) {
768
+ // Batch-read fields were already merged into RETURN.
769
+ if (batchedKeys?.has(field.key))
770
+ continue;
721
771
  // If the field is of simple type (non-recursive), process it directly
722
772
  if (this.isSimpleField(field.type)) {
723
773
  await this.processSimpleField(tableName, field, RETURN, linesNumber, prefix);
@@ -768,6 +818,80 @@ export default class Inibase {
768
818
  }
769
819
  }
770
820
  }
821
+ /**
822
+ * Batched read for top-level simple columns: a single `paste | sed` child
823
+ * process returns every requested line of every column at once instead of
824
+ * spawning one `sed`/`gunzip|sed` child process per column. Returning null
825
+ * (e.g. when any column file is missing, or the shell command fails) makes
826
+ * the caller fall back to the existing per-column reads.
827
+ */
828
+ async processSimpleFieldsBatch(tableName, fields, linesNumber) {
829
+ if (!fields.length || !linesNumber.length)
830
+ return null;
831
+ const decodeID = globalConfig[this.databasePath].tables?.get(tableName)?.config
832
+ .decodeID === true;
833
+ // Decode configs are computed once per column (the per-column read path
834
+ // builds one config per file too), not once per cell.
835
+ const cols = [];
836
+ for (const field of fields) {
837
+ const path = join(this.databasePath, tableName, `${field.key}${this.getFileExtension(tableName)}`);
838
+ if (!(await File.isExists(path)))
839
+ return null;
840
+ cols.push({
841
+ path,
842
+ key: field.key,
843
+ config: {
844
+ ...field,
845
+ type: field.key === "id" && decodeID
846
+ ? "number"
847
+ : field.type,
848
+ databasePath: this.databasePath,
849
+ },
850
+ });
851
+ }
852
+ const sortedLines = [...linesNumber].sort((a, b) => a - b);
853
+ const range = File._groupIntoRanges(sortedLines);
854
+ const files = cols.map(({ path }) => File.escapeShellPath(path)).join(" ");
855
+ const isGz = this.getFileExtension(tableName).endsWith(".gz");
856
+ // Each compressed column needs its own process substitution so the
857
+ // streams stay aligned column-by-column inside `paste`.
858
+ const pasteInputs = isGz
859
+ ? cols
860
+ .map(({ path }) => `<(gunzip -c ${File.escapeShellPath(path)})`)
861
+ .join(" ")
862
+ : files;
863
+ const command = isGz
864
+ ? `bash -c 'paste -d "\\t" ${pasteInputs} | sed -n "${range}"'`
865
+ : `paste -d'\\t' ${files} | sed -n '${range}'`;
866
+ let output;
867
+ try {
868
+ output = (await UtilsServer.exec(command)).stdout;
869
+ }
870
+ catch {
871
+ return null;
872
+ }
873
+ const outLines = output.trimEnd().split("\n");
874
+ const RETURN = {};
875
+ for (let i = 0; i < outLines.length && i < sortedLines.length; i++) {
876
+ const lineNo = sortedLines[i];
877
+ const cells = outLines[i].split("\t");
878
+ const row = {};
879
+ let added = false;
880
+ for (let c = 0; c < cols.length; c++) {
881
+ const raw = cells[c];
882
+ if (raw === undefined)
883
+ continue;
884
+ const value = File.decode(raw, cols[c].config);
885
+ if (value !== undefined) {
886
+ row[cols[c].key] = value;
887
+ added = true;
888
+ }
889
+ }
890
+ if (added)
891
+ RETURN[lineNo] = row;
892
+ }
893
+ return Object.keys(RETURN).length ? RETURN : null;
894
+ }
771
895
  // Helper function to check if the field type is array
772
896
  isArrayField(fieldType) {
773
897
  return ((Array.isArray(fieldType) &&
@@ -809,7 +933,8 @@ export default class Inibase {
809
933
  RETURN[index] = {};
810
934
  if (Utils.isObject(item)) {
811
935
  const itemEntries = Object.entries(item);
812
- const itemValues = itemEntries.map(([_key, value]) => value);
936
+ // Values without a second per-row tuple array.
937
+ const itemValues = Object.values(item);
813
938
  if (!Utils.isArrayOfNulls(itemValues)) {
814
939
  if (RETURN[index][field.key])
815
940
  for (let _index = 0; _index < itemEntries.length; _index++) {
@@ -1054,11 +1179,19 @@ export default class Inibase {
1054
1179
  return null;
1055
1180
  continue;
1056
1181
  }
1057
- const formatedSearchResult = Object.fromEntries(Object.entries(searchResult).map(([id, value]) => {
1182
+ // Merge matched lines into RETURN. The old code round-tripped through
1183
+ // Object.entries(...).map(...) + Object.fromEntries, allocating two
1184
+ // tuple arrays + a map array per matched row; building the nested
1185
+ // result object directly keeps the exact same semantics with less
1186
+ // garbage. Note: in `allTrue` mode RETURN is *replaced* per key
1187
+ // (searchIn narrowing is what enforces the AND), so the assignment
1188
+ // below intentionally mirrors the original replace/merge behavior.
1189
+ const formatedSearchResult = {};
1190
+ for (const id of Object.keys(searchResult)) {
1058
1191
  const nestedObj = {};
1059
- this._setNestedKey(nestedObj, key, value);
1060
- return [id, nestedObj];
1061
- }));
1192
+ this._setNestedKey(nestedObj, key, searchResult[id]);
1193
+ formatedSearchResult[id] = nestedObj;
1194
+ }
1062
1195
  RETURN = allTrue
1063
1196
  ? formatedSearchResult
1064
1197
  : Utils.deepMerge(RETURN, formatedSearchResult);
@@ -1080,10 +1213,17 @@ export default class Inibase {
1080
1213
  const searchResult = await this.applyCriteria(tableName, options, criteriaOR, false, searchIn);
1081
1214
  if (searchResult) {
1082
1215
  RETURN = Utils.deepMerge(RETURN, searchResult);
1083
- if (!Object.keys(RETURN).length)
1084
- RETURN = {};
1085
- RETURN = Object.fromEntries(Object.entries(RETURN).filter(([_index, item]) => Object.keys(item).filter((key) => Object.keys(criteriaOR).includes(key) ||
1086
- Object.keys(criteriaOR).some((criteriaKey) => criteriaKey.startsWith(`${key}.`))).length));
1216
+ // Filter RETURN in place instead of rebuilding it through
1217
+ // Object.entries/fromEntries on every OR iteration. The key list of
1218
+ // criteriaOR is hoisted out of the per-row check as well.
1219
+ const orKeys = Object.keys(criteriaOR);
1220
+ for (const id of Object.keys(RETURN)) {
1221
+ const item = RETURN[id];
1222
+ const matches = Object.keys(item).some((key) => orKeys.includes(key) ||
1223
+ orKeys.some((criteriaKey) => criteriaKey.startsWith(`${key}.`)));
1224
+ if (!matches)
1225
+ delete RETURN[id];
1226
+ }
1087
1227
  if (!Object.keys(RETURN).length)
1088
1228
  RETURN = {};
1089
1229
  }
@@ -1187,11 +1327,19 @@ export default class Inibase {
1187
1327
  awkCommand = `awk '${itemsIDs.map((id) => `$1 == ${id}`).join(" || ")}'`;
1188
1328
  }
1189
1329
  else
1190
- awkCommand = `awk '${Array.from({ length: options.perPage }, (_, index) => (options.page - 1) * options.perPage +
1191
- index +
1192
- 1)
1193
- .map((lineNumber) => `NR==${lineNumber}`)
1194
- .join(" || ")}'`;
1330
+ // perPage < 0 means "no limit": select every line instead of
1331
+ // generating an empty awk window (with perPage -1 the old code
1332
+ // produced `awk ''`, which prints nothing and the empty stdout
1333
+ // decoded into a single hollow row).
1334
+ awkCommand =
1335
+ options.perPage < 0
1336
+ ? "awk '1'"
1337
+ : `awk '${Array.from({ length: options.perPage }, (_, index) => (options.page - 1) *
1338
+ options.perPage +
1339
+ index +
1340
+ 1)
1341
+ .map((lineNumber) => `NR==${lineNumber}`)
1342
+ .join(" || ")}'`;
1195
1343
  const filesPathes = (sortArray.find(([key]) => key === "id")
1196
1344
  ? sortArray
1197
1345
  : [["id", true], ...sortArray]).map((column) => join(tablePath, `${column[0]}${this.getFileExtension(tableName)}`));
@@ -1200,14 +1348,14 @@ export default class Inibase {
1200
1348
  return null;
1201
1349
  // Construct the paste command to merge files and filter lines by IDs
1202
1350
  const pasteCommand = `paste '${filesPathes.join("' '")}'`;
1203
- // Construct the sort command dynamically based on the number of files for sorting
1204
- const index = 1;
1351
+ const _idPrepended = !sortArray.find(([key]) => key === "id");
1205
1352
  const sortColumns = sortArray
1206
1353
  .map(([key, ascending], i) => {
1207
1354
  const field = Utils.getField(key, schema);
1208
- if (field)
1209
- return `-k${i + index},${i + index}${Utils.isFieldType(field, ["id", "number", "date"]) ? "n" : ""}${!ascending ? "r" : ""}`;
1210
- return "";
1355
+ if (!field)
1356
+ return "";
1357
+ const colIndex = _idPrepended ? i + 2 : i + 1;
1358
+ return `-k${colIndex},${colIndex}${Utils.isFieldType(field, ["id", "number", "date"]) ? "n" : ""}${!ascending ? "r" : ""}`;
1211
1359
  })
1212
1360
  .join(" ");
1213
1361
  const sortCommand = `sort ${sortColumns} -T='${join(tablePath, ".tmp")}'`;
@@ -1299,7 +1447,48 @@ export default class Inibase {
1299
1447
  let Ids = where;
1300
1448
  if (!Array.isArray(Ids))
1301
1449
  Ids = [Ids];
1302
- const [lineNumbers, countItems] = await File.search(join(tablePath, `id${this.getFileExtension(tableName)}`), "[]", Ids.map((id) => Utils.isNumber(id) ? Number(id) : UtilsServer.decodeID(id)), undefined, undefined, { key: "BLABLA", type: "number" }, Ids.length, 0, !this.totalItems.has(`${tableName}-id`));
1450
+ // Fast path for decodeID tables whose ids are the dense sequence
1451
+ // 1..N: when the requested numeric ids form a duplicate-free
1452
+ // consecutive range [min..max] with max within the row count, the
1453
+ // line numbers ARE the ids — no id-file scan required.
1454
+ let lineNumbers = null;
1455
+ let countItems = 0;
1456
+ const isDecodeID = globalConfig[this.databasePath].tables?.get(tableName)?.config
1457
+ .decodeID === true &&
1458
+ !globalConfig[this.databasePath].tables?.get(tableName)?.config
1459
+ .prepend;
1460
+ if (isDecodeID &&
1461
+ this.idDensity.get(tableName) &&
1462
+ Ids.every(Utils.isNumber)) {
1463
+ const seen = new Set();
1464
+ let min = Number.POSITIVE_INFINITY;
1465
+ let max = Number.NEGATIVE_INFINITY;
1466
+ let distinct = true;
1467
+ for (const raw of Ids) {
1468
+ const n = Number(raw);
1469
+ if (seen.has(n)) {
1470
+ distinct = false;
1471
+ break;
1472
+ }
1473
+ seen.add(n);
1474
+ if (n < min)
1475
+ min = n;
1476
+ if (n > max)
1477
+ max = n;
1478
+ }
1479
+ if (distinct &&
1480
+ min >= 1 &&
1481
+ max <= pagination[1] &&
1482
+ max - min + 1 === Ids.length) {
1483
+ lineNumbers = {};
1484
+ for (let line = min; line <= max; line++)
1485
+ lineNumbers[line] = line;
1486
+ countItems = Ids.length;
1487
+ }
1488
+ }
1489
+ if (!lineNumbers) {
1490
+ [lineNumbers, countItems] = await File.search(join(tablePath, `id${this.getFileExtension(tableName)}`), "[]", Ids.map((id) => Utils.isNumber(id) ? Number(id) : UtilsServer.decodeID(id)), undefined, undefined, { key: "BLABLA", type: "number" }, Ids.length, 0, !this.totalItems.has(`${tableName}-id`));
1491
+ }
1303
1492
  if (!lineNumbers)
1304
1493
  return null;
1305
1494
  this.totalItems.set(`${tableName}-id`, countItems);
@@ -1425,7 +1614,7 @@ export default class Inibase {
1425
1614
  ? await File.prepend(path, content)
1426
1615
  : await File.append(path, content))));
1427
1616
  await Promise.allSettled(renameList
1428
- .filter(([_, filePath]) => filePath)
1617
+ .filter((pair) => Boolean(pair[1]))
1429
1618
  .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1430
1619
  if (globalConfig[this.databasePath].tables?.get(tableName)?.config.cache)
1431
1620
  await this.clearCache(tableName);
@@ -1453,7 +1642,7 @@ export default class Inibase {
1453
1642
  finally {
1454
1643
  if (renameList.length)
1455
1644
  await Promise.allSettled(renameList
1456
- .filter(([_, filePath]) => filePath)
1645
+ .filter((pair) => Boolean(pair[1]))
1457
1646
  .map(async ([tempPath, _]) => unlink(tempPath)));
1458
1647
  await File.unlock(join(tablePath, ".tmp"), keys);
1459
1648
  }
@@ -1498,7 +1687,7 @@ export default class Inibase {
1498
1687
  this.totalItems.set(`${tableName}-*`, parse(paginationFileName).name.split("-").map(Number)[1]);
1499
1688
  await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content, this.totalItems.get(`${tableName}-*`)))));
1500
1689
  await Promise.allSettled(renameList
1501
- .filter(([_, filePath]) => filePath)
1690
+ .filter((pair) => Boolean(pair[1]))
1502
1691
  .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1503
1692
  if (globalConfig[this.databasePath].tables?.get(tableName)?.config.cache)
1504
1693
  await this.clearCache(join(tablePath, ".cache"));
@@ -1508,7 +1697,7 @@ export default class Inibase {
1508
1697
  finally {
1509
1698
  if (renameList.length)
1510
1699
  await Promise.allSettled(renameList
1511
- .filter(([_, filePath]) => filePath)
1700
+ .filter((pair) => Boolean(pair[1]))
1512
1701
  .map(async ([tempPath, _]) => unlink(tempPath)));
1513
1702
  await File.unlock(join(tablePath, ".tmp"));
1514
1703
  }
@@ -1540,7 +1729,7 @@ export default class Inibase {
1540
1729
  await File.lock(join(tablePath, ".tmp"), keys);
1541
1730
  await Promise.allSettled(Object.entries(pathesContents).map(async ([path, content]) => renameList.push(await File.replace(path, content))));
1542
1731
  await Promise.allSettled(renameList
1543
- .filter(([_, filePath]) => filePath)
1732
+ .filter((pair) => Boolean(pair[1]))
1544
1733
  .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1545
1734
  if (globalConfig[this.databasePath].tables?.get(tableName)?.config.cache)
1546
1735
  await this.clearCache(tableName);
@@ -1550,7 +1739,7 @@ export default class Inibase {
1550
1739
  finally {
1551
1740
  if (renameList.length)
1552
1741
  await Promise.allSettled(renameList
1553
- .filter(([_, filePath]) => filePath)
1742
+ .filter((pair) => Boolean(pair[1]))
1554
1743
  .map(async ([tempPath, _]) => unlink(tempPath)));
1555
1744
  await File.unlock(join(tablePath, ".tmp"), keys);
1556
1745
  }
@@ -1564,7 +1753,14 @@ export default class Inibase {
1564
1753
  Utils.isValidID(where)) {
1565
1754
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1566
1755
  if (lineNumbers)
1567
- return this.put(tableName, clonedData, lineNumbers, options, returnUpdatedData, true);
1756
+ return this.put(tableName, clonedData,
1757
+ // get() with onlyLinesNumbers always returns an array; a
1758
+ // single-id update must keep the scalar so the recursive
1759
+ // line-numbers branch returns a single row (matching the
1760
+ // shape of get(singleId)) instead of a one-element array.
1761
+ !Array.isArray(where) && Array.isArray(lineNumbers)
1762
+ ? lineNumbers[0]
1763
+ : lineNumbers, options, returnUpdatedData, true);
1568
1764
  }
1569
1765
  else if (Utils.isObject(where)) {
1570
1766
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
@@ -1581,7 +1777,7 @@ export default class Inibase {
1581
1777
  * @param {(number | string | (number | string)[] | Criteria)} [where]
1582
1778
  * @return {boolean | null} {(Promise<boolean | null>)}
1583
1779
  */
1584
- async delete(tableName, where, _whereIsLinesNumbers) {
1780
+ async delete(tableName, where, _whereIsLinesNumbers, _cascadeGuard) {
1585
1781
  this.validateName(tableName);
1586
1782
  const tablePath = join(this.databasePath, tableName);
1587
1783
  await this.throwErrorIfTableEmpty(tableName);
@@ -1604,6 +1800,12 @@ export default class Inibase {
1604
1800
  if (globalConfig[this.databasePath].tables?.get(tableName)?.config.cache)
1605
1801
  await this.clearCache(tableName);
1606
1802
  await rename(paginationFilePath, join(tablePath, `${pagination[0]}-0.pagination`));
1803
+ this.idDensity.set(tableName, true);
1804
+ // Deleting every row must also delete rows that reference them.
1805
+ if (pagination[1]) {
1806
+ const allLines = Array.from({ length: pagination[1] }, (_, i) => i + 1);
1807
+ await this.cascadeDelete(tableName, allLines, new Set());
1808
+ }
1607
1809
  return true;
1608
1810
  }
1609
1811
  finally {
@@ -1633,24 +1835,30 @@ export default class Inibase {
1633
1835
  }
1634
1836
  if (pagination[1] &&
1635
1837
  pagination[1] - (Array.isArray(where) ? where.length : 1) > 0) {
1838
+ this.idDensity.set(tableName, false);
1636
1839
  await Promise.allSettled(files.map(async (file) => renameList.push(await File.remove(join(tablePath, file), where))));
1637
1840
  await Promise.allSettled(renameList
1638
- .filter(([_, filePath]) => filePath)
1841
+ .filter((pair) => Boolean(pair[1]))
1639
1842
  .map(async ([tempPath, filePath]) => rename(tempPath, filePath)));
1640
1843
  }
1641
- else
1844
+ else {
1845
+ this.idDensity.set(tableName, true);
1642
1846
  await Promise.allSettled((await readdir(tablePath))
1643
1847
  ?.filter((fileName) => fileName.endsWith(this.getFileExtension(tableName)))
1644
1848
  .map(async (file) => unlink(join(tablePath, file))));
1849
+ }
1645
1850
  if (globalConfig[this.databasePath].tables?.get(tableName)?.config.cache)
1646
1851
  await this.clearCache(tableName);
1647
1852
  await rename(paginationFilePath, join(tablePath, `${pagination[0]}-${pagination[1] - (Array.isArray(where) ? where.length : 1)}.pagination`));
1853
+ // Cascade: rows in other tables referencing the deleted rows
1854
+ // (via `table`-typed fields) are removed too.
1855
+ await this.cascadeDelete(tableName, Array.isArray(where) ? where : [where], _cascadeGuard ?? new Set());
1648
1856
  return true;
1649
1857
  }
1650
1858
  finally {
1651
1859
  if (renameList.length)
1652
1860
  await Promise.allSettled(renameList
1653
- .filter(([_, filePath]) => filePath)
1861
+ .filter((pair) => Boolean(pair[1]))
1654
1862
  .map(async ([tempPath, _]) => unlink(tempPath)));
1655
1863
  await File.unlock(join(tablePath, ".tmp"));
1656
1864
  }
@@ -1664,17 +1872,86 @@ export default class Inibase {
1664
1872
  (Array.isArray(where) && where.every(Utils.isValidID)) ||
1665
1873
  Utils.isValidID(where)) {
1666
1874
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1667
- return this.delete(tableName, lineNumbers, true);
1875
+ // Deleting a non-existent id must not fall through to the
1876
+ // "delete all rows" branch (this.delete(_, null, _) would truncate
1877
+ // the whole table), so resolve the id to line numbers first and
1878
+ // only delegate when something actually matched.
1879
+ if (lineNumbers)
1880
+ return this.delete(tableName, lineNumbers, true, _cascadeGuard ?? new Set());
1881
+ return false;
1668
1882
  }
1669
1883
  if (Utils.isObject(where)) {
1670
1884
  const lineNumbers = await this.get(tableName, where, undefined, undefined, true);
1671
1885
  if (lineNumbers)
1672
- return this.delete(tableName, lineNumbers, true);
1886
+ return this.delete(tableName, lineNumbers, true, _cascadeGuard ?? new Set());
1673
1887
  }
1674
1888
  else
1675
1889
  throw this.createError("INVALID_PARAMETERS");
1676
1890
  return false;
1677
1891
  }
1892
+ /**
1893
+ * Cascade delete: remove rows in other tables whose `table`-typed schema
1894
+ * fields reference the given rows. Reference columns store the numeric
1895
+ * (line-number) id of the referenced row, so deleted ids are matched
1896
+ * against them directly. Recursion into child tables happens through
1897
+ * `delete` itself (which calls this method again); the `guard` set keeps
1898
+ * deep/cyclic reference chains from re-processing the same (table, line).
1899
+ */
1900
+ async cascadeDelete(tableName, deletedLines, guard) {
1901
+ if (!deletedLines.length)
1902
+ return;
1903
+ for (const line of deletedLines)
1904
+ guard.add(`${tableName}:${line}`);
1905
+ const tables = globalConfig[this.databasePath]?.tables;
1906
+ if (!tables)
1907
+ return;
1908
+ for (const [candidateName, tableData] of tables) {
1909
+ if (candidateName === tableName || !tableData?.schema)
1910
+ continue;
1911
+ // Only direct `table`-typed columns hold one stored id per row;
1912
+ // arrays/objects of table refs serialize differently and are
1913
+ // intentionally out of scope for the cascade.
1914
+ const refFields = Utils.flattenSchema(tableData.schema, true).filter((field) => field.table === tableName && field.type === "table");
1915
+ if (!refFields.length)
1916
+ continue;
1917
+ for (const field of refFields) {
1918
+ const refPath = join(this.databasePath, candidateName, `${field.key}${this.getFileExtension(candidateName)}`);
1919
+ if (!(await File.isExists(refPath)))
1920
+ continue;
1921
+ const matching = new Set();
1922
+ for (const line of deletedLines) {
1923
+ try {
1924
+ const [, , found] = await File.search(refPath, "=", line, undefined, undefined, {
1925
+ key: field.key,
1926
+ type: "number",
1927
+ databasePath: this.databasePath,
1928
+ }, undefined, undefined, false);
1929
+ if (found)
1930
+ for (const l of found)
1931
+ matching.add(l);
1932
+ }
1933
+ catch {
1934
+ // Unreadable/unsupported column -> skip this reference.
1935
+ }
1936
+ }
1937
+ const toDelete = [...matching].filter((line) => {
1938
+ const key = `${candidateName}:${line}`;
1939
+ if (guard.has(key))
1940
+ return false;
1941
+ guard.add(key);
1942
+ return true;
1943
+ });
1944
+ if (!toDelete.length)
1945
+ continue;
1946
+ try {
1947
+ await this.delete(candidateName, toDelete, true, guard);
1948
+ }
1949
+ catch {
1950
+ // Cascade is best-effort: never break the parent delete.
1951
+ }
1952
+ }
1953
+ }
1954
+ }
1678
1955
  async sum(tableName, columns, where) {
1679
1956
  this.validateName(tableName);
1680
1957
  if (!Array.isArray(columns))
@@ -1,9 +1,9 @@
1
1
  import "dotenv/config";
2
- import { execFile as execFileSync, exec as execSync } from "node:child_process";
2
+ import { execFile as execFileSync, exec as rawExec } from "node:child_process";
3
3
  import { gunzip as gunzipSync, gzip as gzipSync } from "node:zlib";
4
4
  import RE2 from "re2";
5
5
  import type { ComparisonOperator, FieldType } from "./index.js";
6
- export declare const exec: typeof execSync.__promisify__;
6
+ export declare const exec: typeof rawExec.__promisify__;
7
7
  export declare const execFile: typeof execFileSync.__promisify__;
8
8
  export declare const gzip: typeof gzipSync.__promisify__;
9
9
  export declare const gunzip: typeof gunzipSync.__promisify__;
@@ -1,5 +1,5 @@
1
1
  import "dotenv/config";
2
- import { execFile as execFileSync, exec as execSync } from "node:child_process";
2
+ import { execFile as execFileSync, exec as rawExec } from "node:child_process";
3
3
  import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync, } from "node:crypto";
4
4
  import { promisify } from "node:util";
5
5
  import { gunzip as gunzipSync, gzip as gzipSync } from "node:zlib";
@@ -7,7 +7,7 @@ import Inison from "inison";
7
7
  import RE2 from "re2";
8
8
  import { globalConfig } from "./index.js";
9
9
  import { detectFieldType, isNumber, isPassword } from "./utils.js";
10
- export const exec = promisify(execSync);
10
+ export const exec = promisify(rawExec);
11
11
  export const execFile = promisify(execFileSync);
12
12
  export const gzip = promisify(gzipSync);
13
13
  export const gunzip = promisify(gunzipSync);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inibase",
3
- "version": "1.8.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Karim Amahtil",
@@ -87,6 +87,7 @@
87
87
  "build": "tsc",
88
88
  "benchmark": "./benchmark/run.js",
89
89
  "test": "tsx ./tests/inibase.test.ts",
90
- "test:utils": "tsx ./tests/utils.test.ts"
90
+ "test:utils": "tsx ./tests/utils.test.ts",
91
+ "test:advanced": "tsx ./tests/inibase.advanced.test.ts"
91
92
  }
92
93
  }