inibase 1.0.0-rc.98 → 1.1.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
@@ -54,6 +54,15 @@ const users = await db.get("user", { favoriteFoods: "![]Pizza,Burger" });
54
54
  <npm|pnpm|yarn|bun> install inibase
55
55
  ```
56
56
 
57
+ > [!WARNING]
58
+ > If you're using **Windows**, the following Unix commands are required: `zcat`, `sed`, `gzip`, and `echo`.
59
+ >
60
+ > To use the missing commands, you need to install additional tools:
61
+ > - **[GnuWin32](http://gnuwin32.sourceforge.net/)**: Provides individual GNU utilities for Windows.
62
+ > - **[Cygwin](https://www.cygwin.com/)**: Offers a full Unix-like environment for Windows.
63
+ >
64
+ > Alternatively, consider using the **Windows Subsystem for Linux (WSL)** to run a Linux environment on Windows. Learn more [here](https://learn.microsoft.com/en-us/windows/wsl/).
65
+
57
66
  ## How it works?
58
67
 
59
68
  `Inibase` organizes data into databases, tables, and columns, each stored in separate files.
@@ -97,40 +106,124 @@ interface {
97
106
  <summary>Schema</summary>
98
107
  <blockquote>
99
108
 
109
+ <details>
110
+ <summary>Types</summary>
111
+ <blockquote>
112
+
100
113
  ```ts
101
- interface {
114
+ interface Field {
102
115
  id: number; // stored as a Number but displayed as a hashed ID
103
116
  key: string;
104
117
  required?: boolean;
105
- unique?: boolean;
106
- type: "string" | "number" | "boolean" | "date" | "email" | "url" | "password" | "html" | "ip" | "json" | "id";
118
+ unique?: boolean | string; // boolean for simple uniqueness, string for grouped uniqueness
119
+ regex?: RegExp; // Regular expression for custom validation
120
+ type:
121
+ | "string"
122
+ | "number"
123
+ | "boolean"
124
+ | "date"
125
+ | "email"
126
+ | "url"
127
+ | "password"
128
+ | "html"
129
+ | "ip"
130
+ | "json"
131
+ | "id";
107
132
  }
108
- interface Table {
133
+
134
+ interface TableField {
109
135
  id: number;
110
136
  key: string;
111
137
  required?: boolean;
138
+ unique?: boolean | string; // Supports uniqueness constraints
112
139
  type: "table";
113
140
  table: string;
114
141
  }
115
- interface Array {
142
+
143
+ interface ArrayField {
116
144
  id: number;
117
145
  key: string;
118
146
  required?: boolean;
147
+ unique?: boolean | string; // Supports uniqueness constraints
119
148
  type: "array";
120
- children: string|string[];
149
+ children: string | string[]; // Can be a single type or an array of types
121
150
  }
122
- interface ObjectOrArrayOfObjects {
151
+
152
+ interface ObjectOrArrayOfObjectsField {
123
153
  id: number;
124
154
  key: string;
125
155
  required?: boolean;
156
+ unique?: boolean | string; // Supports uniqueness constraints
157
+ regex?: RegExp; // For validation of object-level keys
126
158
  type: "object" | "array";
127
- children: Schema;
159
+ children: Schema; // Nested schema for objects or arrays
128
160
  }
129
161
  ```
130
162
 
131
163
  </blockquote>
132
164
  </details>
133
165
 
166
+ <details>
167
+ <summary>Unique</summary>
168
+ <blockquote>
169
+
170
+ The `unique` property ensures that the values of a specific column or a group of columns are unique within a table. This property can be either a boolean or a string.
171
+ - **Boolean**: Setting `unique: true` ensures that the values in the column are unique across all rows.
172
+ - **String**: By setting a string value, you can group columns to enforce a combined uniqueness constraint. This is useful when you need to ensure that a combination of values across multiple fields is unique.
173
+
174
+ <details>
175
+ <summary>Examples</summary>
176
+ <blockquote>
177
+
178
+ <details>
179
+ <summary>Unique Column</summary>
180
+ <blockquote>
181
+
182
+ ```js
183
+ {
184
+ key: "email",
185
+ type: "string",
186
+ required: true,
187
+ unique: true, // Ensures all email values are unique
188
+ }
189
+ ```
190
+
191
+ </blockquote>
192
+ </details>
193
+
194
+ <details>
195
+ <summary>Group of Unique Columns</summary>
196
+ <blockquote>
197
+
198
+ ```js
199
+ [
200
+ {
201
+ key: "firstName",
202
+ type: "string",
203
+ required: true,
204
+ unique: "nameGroup", // Part of "nameGroup" uniqueness
205
+ },
206
+ {
207
+ key: "lastName",
208
+ type: "string",
209
+ required: true,
210
+ unique: "nameGroup", // Part of "nameGroup" uniqueness
211
+ },
212
+ ]
213
+ ```
214
+
215
+ </blockquote>
216
+ </details>
217
+
218
+ </blockquote>
219
+ </details>
220
+
221
+ </blockquote>
222
+ </details>
223
+
224
+ </blockquote>
225
+ </details>
226
+
134
227
  <details>
135
228
  <summary>Create Table</summary>
136
229
  <blockquote>
@@ -348,7 +441,7 @@ const product = await db.post("product", productTableData);
348
441
  </blockquote>
349
442
  </details>
350
443
 
351
- <details>
444
+ <details open>
352
445
  <summary>Methods</summary>
353
446
  <blockquote>
354
447
 
@@ -667,21 +760,22 @@ await db.get("user", undefined, { sort: {age: -1, username: "asc"} });
667
760
 
668
761
  ### Bulk
669
762
 
670
- | | 10 | 100 | 1000 |
671
- |--------|-----------------|-----------------|-----------------|
672
- | POST | 11 ms (0.65 mb) | 19 ms (1.00 mb) | 85 ms (4.58 mb) |
673
- | GET | 14 ms (2.77 mb) | 12 ms (3.16 mb) | 34 ms (1.38 mb) |
674
- | PUT | 6 ms (1.11 mb) | 5 ms (1.37 mb) | 10 ms (1.12 mb) |
675
- | DELETE | 17 ms (1.68 mb) | 14 ms (5.45 mb) | 25 ms (5.94 mb) |
763
+ | | 10 | 100 | 1000 |
764
+ |--------|-------------------|-------------------|-------------------|
765
+ | POST | 11 ms (0.66 mb) | 5 ms (1.02 mb) | 24 ms (1.44 mb) |
766
+ | GET | 29 ms (2.86 mb) | 24 ms (2.81 mb) | 36 ms (0.89 mb) |
767
+ | PUT | 21 ms (2.68 mb) | 16 ms (2.90 mb) | 12 ms (0.63 mb) |
768
+ | DELETE | 14 ms (0.82 mb) | 13 ms (0.84 mb) | 2 ms (0.17 mb) |
769
+
676
770
 
677
771
  ### Single
678
772
 
679
- | | 10 | 100 | 1000 |
680
- |--------|-------------------|--------------------|--------------------|
681
- | POST | 43 ms (4.70 mb) | 387 ms (6.36 mb) | 5341 ms (24.73 mb) |
682
- | GET | 99 ms (12.51 mb) | 846 ms (30.68 mb) | 7103 ms (30.86 mb) |
683
- | PUT | 33 ms (10.29 mb) | 312 ms (11.06 mb) | 3539 ms (14.87 mb) |
684
- | DELETE | 134 ms (13.50 mb) | 1224 ms (16.57 mb) | 7339 ms (11.46 mb) |
773
+ | | 10 | 100 | 1000 |
774
+ |--------|---------------------|--------------------|--------------------|
775
+ | POST | 45 ms (1.07 mb) | 12 ms (0.52 mb) | 11 ms (0.37 mb) |
776
+ | GET | 200 ms (2.15 mb) | 192 ms (2.72 mb) | 190 ms (2.31 mb) |
777
+ | PUT | 49 ms (3.22 mb) | 17 ms (2.98 mb) | 17 ms (3.06 mb) |
778
+ | DELETE | 118 ms (0.59 mb) | 113 ms (0.51 mb) | 103 ms (3.14 mb) |
685
779
 
686
780
  > Default testing uses a table with username, email, and password fields, ensuring password encryption is included in the process<br>
687
781
  > To run benchmarks, install _typescript_ & _[tsx](https://github.com/privatenumber/tsx)_ globally and run `benchmark` by default bulk, for single use `benchmark --single|-s`
@@ -716,12 +810,13 @@ await db.get("user", undefined, { sort: {age: -1, username: "asc"} });
716
810
  - [x] Id
717
811
  - [x] JSON
718
812
  - [ ] TO-DO:
813
+ - [x] Use new Map() instead of Object
719
814
  - [ ] Ability to search in JSON fields
720
- - [ ] Re-check used exec functions
815
+ - [x] Re-check used exec functions
721
816
  - [ ] Use smart caching (based on N° of queries)
722
817
  - [ ] Commenting the code
723
818
  - [ ] Add Backup feature (generate a tar.gz)
724
- - [ ] Add Custom field validation property to schema (using RegEx?)
819
+ - [x] Add Custom field validation property to schema (using RegEx?)
725
820
  - [ ] Features:
726
821
  - [ ] Encryption
727
822
  - [x] Data Compression
package/dist/file.d.ts CHANGED
@@ -3,6 +3,7 @@ export declare const lock: (folderPath: string, prefix?: string) => Promise<void
3
3
  export declare const unlock: (folderPath: string, prefix?: string) => Promise<void>;
4
4
  export declare const write: (filePath: string, data: any) => Promise<void>;
5
5
  export declare const read: (filePath: string) => Promise<string>;
6
+ export declare function escapeShellPath(filePath: string): string;
6
7
  /**
7
8
  * Checks if a file or directory exists at the specified path.
8
9
  *
@@ -58,7 +59,7 @@ export declare function get(filePath: string, lineNumbers: undefined | number |
58
59
  *
59
60
  * Note: If the file doesn't exist and replacements is an object, it creates a new file with the specified replacements.
60
61
  */
61
- 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)[]>) => Promise<string[]>;
62
+ 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[]>;
62
63
  /**
63
64
  * Asynchronously appends data to the end of a file.
64
65
  *
@@ -106,16 +107,7 @@ export declare const remove: (filePath: string, linesToDelete: number | number[]
106
107
  *
107
108
  * Note: Decodes each line for comparison and can handle complex queries with multiple conditions.
108
109
  */
109
- export declare const search: (filePath: string, operator: ComparisonOperator | ComparisonOperator[], comparedAtValue: string | number | boolean | null | (string | number | boolean | null)[], logicalOperator?: "and" | "or", fieldType?: FieldType | FieldType[], fieldChildrenType?: FieldType | FieldType[] | Schema, limit?: number, offset?: number, readWholeFile?: boolean, secretKey?: string | Buffer) => Promise<[Record<number, string | number | boolean | null | (string | number | boolean | null)[]> | null, number, Set<number> | null]>;
110
- /**
111
- * Asynchronously counts the number of lines in a file.
112
- *
113
- * @param filePath - Path of the file to count lines in.
114
- * @returns Promise<number>. The number of lines in the file.
115
- *
116
- * Note: Reads through the file line by line to count the total number of lines.
117
- */
118
- export declare const count: (filePath: string) => Promise<number>;
110
+ export declare const search: (filePath: string, operator: ComparisonOperator | ComparisonOperator[], comparedAtValue: string | number | boolean | null | (string | number | boolean | null)[], logicalOperator?: "and" | "or", searchIn?: Set<number>, fieldType?: FieldType | FieldType[], fieldChildrenType?: FieldType | FieldType[] | Schema, limit?: number, offset?: number, readWholeFile?: boolean, secretKey?: string | Buffer) => Promise<[Record<number, string | number | boolean | null | (string | number | boolean | null)[]> | null, number, Set<number> | null]>;
119
111
  /**
120
112
  * Asynchronously calculates the sum of numerical values from specified lines in a file.
121
113
  *
package/dist/file.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { access, appendFile, copyFile, constants as fsConstants, open, readFile, unlink, writeFile, } from "node:fs/promises";
2
- import { join } from "node:path";
2
+ import { join, resolve } from "node:path";
3
3
  import { createInterface } from "node:readline";
4
4
  import { Transform } from "node:stream";
5
5
  import { pipeline } from "node:stream/promises";
@@ -34,6 +34,12 @@ export const write = async (filePath, data) => {
34
34
  export const read = async (filePath) => filePath.endsWith(".gz")
35
35
  ? (await gunzip(await readFile(filePath, "utf8"))).toString()
36
36
  : await readFile(filePath, "utf8");
37
+ export function escapeShellPath(filePath) {
38
+ // Resolve the path to avoid relative path issues
39
+ const resolvedPath = resolve(filePath);
40
+ // Escape double quotes and special shell characters
41
+ return `"${resolvedPath.replace(/(["\\$`])/g, "\\$1")}"`;
42
+ }
37
43
  const _pipeline = async (filePath, rl, writeStream, transform) => {
38
44
  if (filePath.endsWith(".gz"))
39
45
  await pipeline(rl, transform, createGzip(), writeStream);
@@ -89,7 +95,7 @@ const secureString = (input) => {
89
95
  decodedInput = decodeURIComponent(input.replace(/%(?![0-9][0-9a-fA-F]+)/g, ""));
90
96
  }
91
97
  // Replace characters using a single regular expression.
92
- return decodedInput.replace(/\\n/g, "\n").replace(/\n/g, "\\n");
98
+ return decodedInput.replace(/\r\n|\r|\n/g, "\\n");
93
99
  };
94
100
  /**
95
101
  * Encodes the input using 'secureString' and 'Inison.stringify' functions.
@@ -168,7 +174,7 @@ export const decode = (input, fieldType, fieldChildrenType, secretKey) => {
168
174
  if (!fieldType)
169
175
  return null;
170
176
  if (input === null || input === "")
171
- return null;
177
+ return undefined;
172
178
  // Detect the fieldType based on the input and the provided array of possible types.
173
179
  if (Array.isArray(fieldType))
174
180
  fieldType = detectFieldType(String(input), fieldType);
@@ -179,6 +185,29 @@ export const decode = (input, fieldType, fieldChildrenType, secretKey) => {
179
185
  : unSecureString(input)
180
186
  : input, fieldType, fieldChildrenType, secretKey);
181
187
  };
188
+ function _groupIntoRanges(arr, action = "p") {
189
+ if (arr.length === 0)
190
+ return [];
191
+ arr.sort((a, b) => a - b); // Ensure the array is sorted
192
+ const ranges = [];
193
+ let start = arr[0];
194
+ let end = arr[0];
195
+ for (let i = 1; i < arr.length; i++) {
196
+ if (arr[i] === end + 1) {
197
+ // Continue the range
198
+ end = arr[i];
199
+ }
200
+ else {
201
+ // End the current range and start a new one
202
+ ranges.push(start === end ? `${start}` : `${start},${end}`);
203
+ start = arr[i];
204
+ end = arr[i];
205
+ }
206
+ }
207
+ // Push the last range
208
+ ranges.push(start === end ? `${start}` : `${start},${end}`);
209
+ return ranges.map((range) => `${range}${action}`).join(";");
210
+ }
182
211
  export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, secretKey, readWholeFile = false) {
183
212
  let fileHandle = null;
184
213
  try {
@@ -192,9 +221,10 @@ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, s
192
221
  }
193
222
  }
194
223
  else if (lineNumbers == -1) {
224
+ const escapedFilePath = escapeShellPath(filePath);
195
225
  const command = filePath.endsWith(".gz")
196
- ? `zcat ${filePath} | sed -n '$p'`
197
- : `sed -n '$p' ${filePath}`, foundedLine = (await exec(command)).stdout.trimEnd();
226
+ ? `zcat ${escapedFilePath} | sed -n '$p'`
227
+ : `sed -n '$p' ${escapedFilePath}`, foundedLine = (await exec(command)).stdout.trimEnd();
198
228
  if (foundedLine)
199
229
  lines[linesCount] = decode(foundedLine, fieldType, fieldChildrenType, secretKey);
200
230
  }
@@ -213,9 +243,10 @@ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, s
213
243
  }
214
244
  return [lines, linesCount];
215
245
  }
246
+ const escapedFilePath = escapeShellPath(filePath);
216
247
  const command = filePath.endsWith(".gz")
217
- ? `zcat ${filePath} | sed -n '${lineNumbers.join("p;")}p'`
218
- : `sed -n '${lineNumbers.join("p;")}p' ${filePath}`, foundedLines = (await exec(command)).stdout.trimEnd().split("\n");
248
+ ? `zcat ${escapedFilePath} | sed -n '${_groupIntoRanges(lineNumbers)}'`
249
+ : `sed -n '${_groupIntoRanges(lineNumbers)}' ${escapedFilePath}`, foundedLines = (await exec(command)).stdout.trimEnd().split("\n");
219
250
  let index = 0;
220
251
  for (const line of foundedLines) {
221
252
  lines[lineNumbers[index]] = decode(line, fieldType, fieldChildrenType, secretKey);
@@ -239,48 +270,103 @@ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, s
239
270
  *
240
271
  * Note: If the file doesn't exist and replacements is an object, it creates a new file with the specified replacements.
241
272
  */
242
- export const replace = async (filePath, replacements) => {
273
+ export const replace = async (filePath, replacements, totalItems) => {
243
274
  const fileTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/$1");
275
+ const isReplacementsObject = isObject(replacements);
276
+ const isReplacementsLineNumbered = isReplacementsObject && !Number.isNaN(Number(Object.keys(replacements)[0]));
244
277
  if (await isExists(filePath)) {
245
- let fileHandle = null;
246
- let fileTempHandle = null;
278
+ if (isReplacementsLineNumbered) {
279
+ let fileHandle = null;
280
+ let fileTempHandle = null;
281
+ try {
282
+ let linesCount = 0;
283
+ fileHandle = await open(filePath, "r");
284
+ fileTempHandle = await open(fileTempPath, "w");
285
+ const writeStream = fileTempHandle.createWriteStream();
286
+ const rl = createReadLineInternface(filePath, fileHandle);
287
+ await _pipeline(filePath, rl, writeStream, new Transform({
288
+ transform(line, _, callback) {
289
+ linesCount++;
290
+ const replacement = isReplacementsObject
291
+ ? Object.hasOwn(replacements, linesCount)
292
+ ? replacements[linesCount]
293
+ : line
294
+ : replacements;
295
+ return callback(null, `${replacement}\n`);
296
+ },
297
+ flush(callback) {
298
+ const remainingReplacementsKeys = Object.keys(replacements)
299
+ .map(Number)
300
+ .toSorted((a, b) => a - b)
301
+ .filter((lineNumber) => lineNumber > linesCount);
302
+ if (remainingReplacementsKeys.length)
303
+ this.push("\n".repeat(remainingReplacementsKeys[0] - linesCount - 1) +
304
+ remainingReplacementsKeys
305
+ .map((lineNumber, index) => index === 0 ||
306
+ lineNumber -
307
+ (remainingReplacementsKeys[index - 1] - 1) ===
308
+ 0
309
+ ? replacements[lineNumber]
310
+ : "\n".repeat(lineNumber -
311
+ remainingReplacementsKeys[index - 1] -
312
+ 1) + replacements[lineNumber])
313
+ .join("\n"));
314
+ callback();
315
+ },
316
+ }));
317
+ return [fileTempPath, filePath];
318
+ }
319
+ catch {
320
+ return [fileTempPath, null];
321
+ }
322
+ finally {
323
+ // Ensure that file handles are closed, even if an error occurred
324
+ await fileHandle?.close();
325
+ await fileTempHandle?.close();
326
+ }
327
+ }
328
+ else {
329
+ const escapedFilePath = escapeShellPath(filePath);
330
+ const escapedFileTempPath = escapeShellPath(fileTempPath);
331
+ const sedCommand = `sed -e s/.*/${replacements}/ -e /^$/s/^/${replacements}/ ${escapedFilePath}`;
332
+ const command = filePath.endsWith(".gz")
333
+ ? `zcat ${escapedFilePath} | ${sedCommand} | gzip > ${escapedFileTempPath}`
334
+ : `${sedCommand} > ${escapedFileTempPath}`;
335
+ try {
336
+ await exec(command);
337
+ return [fileTempPath, filePath];
338
+ }
339
+ catch {
340
+ return [fileTempPath, null];
341
+ }
342
+ }
343
+ }
344
+ else if (isReplacementsObject) {
247
345
  try {
248
- let linesCount = 0;
249
- fileHandle = await open(filePath, "r");
250
- fileTempHandle = await open(fileTempPath, "w");
251
- const rl = createReadLineInternface(filePath, fileHandle);
252
- await _pipeline(filePath, rl, fileTempHandle.createWriteStream(), new Transform({
253
- transform(line, _, callback) {
254
- linesCount++;
255
- const replacement = isObject(replacements)
256
- ? Object.hasOwn(replacements, linesCount)
257
- ? replacements[linesCount]
258
- : line
259
- : replacements;
260
- return callback(null, `${replacement}\n`);
261
- },
262
- }));
346
+ if (isReplacementsLineNumbered) {
347
+ const replacementsKeys = Object.keys(replacements)
348
+ .map(Number)
349
+ .toSorted((a, b) => a - b);
350
+ await write(fileTempPath, `${"\n".repeat(replacementsKeys[0] - 1) +
351
+ replacementsKeys
352
+ .map((lineNumber, index) => index === 0 ||
353
+ lineNumber - replacementsKeys[index - 1] - 1 === 0
354
+ ? replacements[lineNumber]
355
+ : "\n".repeat(lineNumber - replacementsKeys[index - 1] - 1) +
356
+ replacements[lineNumber])
357
+ .join("\n")}\n`);
358
+ }
359
+ else {
360
+ if (!totalItems)
361
+ throw new Error("INVALID_PARAMETERS");
362
+ await write(fileTempPath, `${`${replacements}\n`.repeat(totalItems)}\n`);
363
+ }
263
364
  return [fileTempPath, filePath];
264
365
  }
265
- finally {
266
- // Ensure that file handles are closed, even if an error occurred
267
- await fileHandle?.close();
268
- await fileTempHandle?.close();
366
+ catch {
367
+ return [fileTempPath, null];
269
368
  }
270
369
  }
271
- else if (isObject(replacements)) {
272
- const replacementsKeys = Object.keys(replacements)
273
- .map(Number)
274
- .toSorted((a, b) => a - b);
275
- await write(fileTempPath, `${"\n".repeat(replacementsKeys[0] - 1) +
276
- replacementsKeys
277
- .map((lineNumber, index) => index === 0 || lineNumber - replacementsKeys[index - 1] - 1 === 0
278
- ? replacements[lineNumber]
279
- : "\n".repeat(lineNumber - replacementsKeys[index - 1] - 1) +
280
- replacements[lineNumber])
281
- .join("\n")}\n`);
282
- return [fileTempPath, filePath];
283
- }
284
370
  return [];
285
371
  };
286
372
  /**
@@ -293,20 +379,26 @@ export const replace = async (filePath, replacements) => {
293
379
  */
294
380
  export const append = async (filePath, data) => {
295
381
  const fileTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/$1");
296
- if (await isExists(filePath)) {
297
- await copyFile(filePath, fileTempPath);
298
- if (!filePath.endsWith(".gz")) {
299
- await appendFile(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
300
- }
301
- else {
302
- await exec(`echo $'${(Array.isArray(data) ? data.join("\n") : data)
303
- .toString()
304
- .replace(/'/g, "\\'")}' | gzip - >> ${fileTempPath}`);
382
+ try {
383
+ if (await isExists(filePath)) {
384
+ await copyFile(filePath, fileTempPath);
385
+ if (!filePath.endsWith(".gz")) {
386
+ await appendFile(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
387
+ }
388
+ else {
389
+ const escapedFileTempPath = escapeShellPath(fileTempPath);
390
+ await exec(`echo '${(Array.isArray(data) ? data.join("\n") : data)
391
+ .toString()
392
+ .replace(/'/g, "\\'")}' | gzip - >> ${escapedFileTempPath}`);
393
+ }
305
394
  }
395
+ else
396
+ await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
397
+ return [fileTempPath, filePath];
398
+ }
399
+ catch {
400
+ return [fileTempPath, null];
306
401
  }
307
- else
308
- await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
309
- return [fileTempPath, filePath];
310
402
  };
311
403
  /**
312
404
  * Asynchronously prepends data to the beginning of a file.
@@ -337,6 +429,9 @@ export const prepend = async (filePath, data) => {
337
429
  },
338
430
  }));
339
431
  }
432
+ catch {
433
+ return [fileTempPath, null];
434
+ }
340
435
  finally {
341
436
  // Ensure that file handles are closed, even if an error occurred
342
437
  await fileHandle?.close();
@@ -347,15 +442,27 @@ export const prepend = async (filePath, data) => {
347
442
  const fileChildTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/tmp_$1");
348
443
  try {
349
444
  await write(fileChildTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
350
- await exec(`cat ${fileChildTempPath} ${filePath} > ${fileTempPath}`);
445
+ const escapedFilePath = escapeShellPath(filePath);
446
+ const escapedFileTempPath = escapeShellPath(fileTempPath);
447
+ const escapedFileChildTempPath = escapeShellPath(fileChildTempPath);
448
+ await exec(`cat ${escapedFileChildTempPath} ${escapedFilePath} > ${escapedFileTempPath}`);
449
+ }
450
+ catch {
451
+ return [fileTempPath, null];
351
452
  }
352
453
  finally {
353
454
  await unlink(fileChildTempPath);
354
455
  }
355
456
  }
356
457
  }
357
- else
358
- await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
458
+ else {
459
+ try {
460
+ await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
461
+ }
462
+ catch {
463
+ return [fileTempPath, null];
464
+ }
465
+ }
359
466
  return [fileTempPath, filePath];
360
467
  };
361
468
  /**
@@ -374,11 +481,18 @@ export const remove = async (filePath, linesToDelete) => {
374
481
  if (linesToDelete.some(Number.isNaN))
375
482
  throw new Error("UNVALID_LINE_NUMBERS");
376
483
  const fileTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/$1");
377
- const command = filePath.endsWith(".gz")
378
- ? `zcat ${filePath} | sed "${linesToDelete.join("d;")}d" | gzip > ${fileTempPath}`
379
- : `sed "${linesToDelete.join("d;")}d" ${filePath} > ${fileTempPath}`;
380
- await exec(command);
381
- return [fileTempPath, filePath];
484
+ try {
485
+ const escapedFilePath = escapeShellPath(filePath);
486
+ const escapedFileTempPath = escapeShellPath(fileTempPath);
487
+ const command = filePath.endsWith(".gz")
488
+ ? `zcat ${escapedFilePath} | sed '${_groupIntoRanges(linesToDelete, "d")}' | gzip > ${escapedFileTempPath}`
489
+ : `sed '${_groupIntoRanges(linesToDelete, "d")}' ${escapedFilePath} > ${escapedFileTempPath}`;
490
+ await exec(command);
491
+ return [fileTempPath, filePath];
492
+ }
493
+ catch {
494
+ return [fileTempPath, null];
495
+ }
382
496
  };
383
497
  /**
384
498
  * Asynchronously searches a file for lines matching specified criteria, using comparison and logical operators.
@@ -399,13 +513,20 @@ export const remove = async (filePath, linesToDelete) => {
399
513
  *
400
514
  * Note: Decodes each line for comparison and can handle complex queries with multiple conditions.
401
515
  */
402
- export const search = async (filePath, operator, comparedAtValue, logicalOperator, fieldType, fieldChildrenType, limit, offset, readWholeFile, secretKey) => {
516
+ export const search = async (filePath, operator, comparedAtValue, logicalOperator, searchIn, fieldType, fieldChildrenType, limit, offset, readWholeFile, secretKey) => {
403
517
  // Initialize a Map to store the matching lines with their line numbers.
404
518
  const matchingLines = {};
405
519
  // Initialize counters for line number, found items, and processed items.
406
520
  let linesCount = 0;
407
521
  const linesNumbers = new Set();
408
522
  let fileHandle = null;
523
+ const meetsConditions = (value) => (Array.isArray(operator) &&
524
+ Array.isArray(comparedAtValue) &&
525
+ ((logicalOperator === "or" &&
526
+ operator.some((single_operator, index) => compare(single_operator, value, comparedAtValue[index], fieldType))) ||
527
+ operator.every((single_operator, index) => compare(single_operator, value, comparedAtValue[index], fieldType)))) ||
528
+ (!Array.isArray(operator) &&
529
+ compare(operator, value, comparedAtValue, fieldType));
409
530
  try {
410
531
  // Open the file for reading.
411
532
  fileHandle = await open(filePath, "r");
@@ -415,18 +536,19 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
415
536
  for await (const line of rl) {
416
537
  // Increment the line count for each line.
417
538
  linesCount++;
539
+ // Search only in provided linesNumbers
540
+ if (searchIn?.size &&
541
+ (!searchIn.has(linesCount) || searchIn.has(-linesCount)))
542
+ continue;
418
543
  // Decode the line for comparison.
419
544
  const decodedLine = decode(line, fieldType, fieldChildrenType, secretKey);
420
545
  // Check if the line meets the specified conditions based on comparison and logical operators.
421
- const meetsConditions = (Array.isArray(operator) &&
422
- Array.isArray(comparedAtValue) &&
423
- ((logicalOperator === "or" &&
424
- operator.some((single_operator, index) => compare(single_operator, decodedLine, comparedAtValue[index], fieldType))) ||
425
- operator.every((single_operator, index) => compare(single_operator, decodedLine, comparedAtValue[index], fieldType)))) ||
426
- (!Array.isArray(operator) &&
427
- compare(operator, decodedLine, comparedAtValue, fieldType));
546
+ const doesMeetCondition = (Array.isArray(decodedLine) &&
547
+ !Array.isArray(decodedLine[1]) &&
548
+ decodedLine.some(meetsConditions)) ||
549
+ meetsConditions(decodedLine);
428
550
  // If the line meets the conditions, process it.
429
- if (meetsConditions) {
551
+ if (doesMeetCondition) {
430
552
  // Increment the found items counter.
431
553
  linesNumbers.add(linesCount);
432
554
  // Check if the line should be skipped based on the offset.
@@ -447,36 +569,14 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
447
569
  ? [matchingLines, linesNumbers.size, linesNumbers]
448
570
  : [null, 0, null];
449
571
  }
572
+ catch {
573
+ return [null, 0, null];
574
+ }
450
575
  finally {
451
576
  // Close the file handle in the finally block to ensure it is closed even if an error occurs.
452
577
  await fileHandle?.close();
453
578
  }
454
579
  };
455
- /**
456
- * Asynchronously counts the number of lines in a file.
457
- *
458
- * @param filePath - Path of the file to count lines in.
459
- * @returns Promise<number>. The number of lines in the file.
460
- *
461
- * Note: Reads through the file line by line to count the total number of lines.
462
- */
463
- export const count = async (filePath) => {
464
- // Number((await exec(`wc -l < ${filePath}`)).stdout.trimEnd());
465
- let linesCount = 0;
466
- if (await isExists(filePath)) {
467
- let fileHandle = null;
468
- try {
469
- fileHandle = await open(filePath, "r");
470
- const rl = createReadLineInternface(filePath, fileHandle);
471
- for await (const _ of rl)
472
- linesCount++;
473
- }
474
- finally {
475
- await fileHandle?.close();
476
- }
477
- }
478
- return linesCount;
479
- };
480
580
  /**
481
581
  * Asynchronously calculates the sum of numerical values from specified lines in a file.
482
582
  *