inibase 1.0.0-rc.99 → 1.1.1

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);
@@ -100,7 +106,9 @@ const secureString = (input) => {
100
106
  * @returns The secured and/or joined string.
101
107
  */
102
108
  export const encode = (input) => Array.isArray(input)
103
- ? input.every((_input) => typeof _input === "string" && isStringified(_input))
109
+ ? input.every((_input) => _input === null ||
110
+ _input === undefined ||
111
+ (typeof _input === "string" && isStringified(_input)))
104
112
  ? `[${input.join(",")}]`
105
113
  : Inison.stringify(input)
106
114
  : secureString(input);
@@ -168,7 +176,7 @@ export const decode = (input, fieldType, fieldChildrenType, secretKey) => {
168
176
  if (!fieldType)
169
177
  return null;
170
178
  if (input === null || input === "")
171
- return null;
179
+ return undefined;
172
180
  // Detect the fieldType based on the input and the provided array of possible types.
173
181
  if (Array.isArray(fieldType))
174
182
  fieldType = detectFieldType(String(input), fieldType);
@@ -179,6 +187,29 @@ export const decode = (input, fieldType, fieldChildrenType, secretKey) => {
179
187
  : unSecureString(input)
180
188
  : input, fieldType, fieldChildrenType, secretKey);
181
189
  };
190
+ function _groupIntoRanges(arr, action = "p") {
191
+ if (arr.length === 0)
192
+ return [];
193
+ arr.sort((a, b) => a - b); // Ensure the array is sorted
194
+ const ranges = [];
195
+ let start = arr[0];
196
+ let end = arr[0];
197
+ for (let i = 1; i < arr.length; i++) {
198
+ if (arr[i] === end + 1) {
199
+ // Continue the range
200
+ end = arr[i];
201
+ }
202
+ else {
203
+ // End the current range and start a new one
204
+ ranges.push(start === end ? `${start}` : `${start},${end}`);
205
+ start = arr[i];
206
+ end = arr[i];
207
+ }
208
+ }
209
+ // Push the last range
210
+ ranges.push(start === end ? `${start}` : `${start},${end}`);
211
+ return ranges.map((range) => `${range}${action}`).join(";");
212
+ }
182
213
  export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, secretKey, readWholeFile = false) {
183
214
  let fileHandle = null;
184
215
  try {
@@ -192,9 +223,10 @@ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, s
192
223
  }
193
224
  }
194
225
  else if (lineNumbers == -1) {
226
+ const escapedFilePath = escapeShellPath(filePath);
195
227
  const command = filePath.endsWith(".gz")
196
- ? `zcat ${filePath} | sed -n '$p'`
197
- : `sed -n '$p' ${filePath}`, foundedLine = (await exec(command)).stdout.trimEnd();
228
+ ? `zcat ${escapedFilePath} | sed -n '$p'`
229
+ : `sed -n '$p' ${escapedFilePath}`, foundedLine = (await exec(command)).stdout.trimEnd();
198
230
  if (foundedLine)
199
231
  lines[linesCount] = decode(foundedLine, fieldType, fieldChildrenType, secretKey);
200
232
  }
@@ -213,9 +245,10 @@ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, s
213
245
  }
214
246
  return [lines, linesCount];
215
247
  }
248
+ const escapedFilePath = escapeShellPath(filePath);
216
249
  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");
250
+ ? `zcat ${escapedFilePath} | sed -n '${_groupIntoRanges(lineNumbers)}'`
251
+ : `sed -n '${_groupIntoRanges(lineNumbers)}' ${escapedFilePath}`, foundedLines = (await exec(command)).stdout.trimEnd().split("\n");
219
252
  let index = 0;
220
253
  for (const line of foundedLines) {
221
254
  lines[lineNumbers[index]] = decode(line, fieldType, fieldChildrenType, secretKey);
@@ -239,48 +272,103 @@ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, s
239
272
  *
240
273
  * Note: If the file doesn't exist and replacements is an object, it creates a new file with the specified replacements.
241
274
  */
242
- export const replace = async (filePath, replacements) => {
275
+ export const replace = async (filePath, replacements, totalItems) => {
243
276
  const fileTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/$1");
277
+ const isReplacementsObject = isObject(replacements);
278
+ const isReplacementsLineNumbered = isReplacementsObject && !Number.isNaN(Number(Object.keys(replacements)[0]));
244
279
  if (await isExists(filePath)) {
245
- let fileHandle = null;
246
- let fileTempHandle = null;
280
+ if (isReplacementsLineNumbered) {
281
+ let fileHandle = null;
282
+ let fileTempHandle = null;
283
+ try {
284
+ let linesCount = 0;
285
+ fileHandle = await open(filePath, "r");
286
+ fileTempHandle = await open(fileTempPath, "w");
287
+ const writeStream = fileTempHandle.createWriteStream();
288
+ const rl = createReadLineInternface(filePath, fileHandle);
289
+ await _pipeline(filePath, rl, writeStream, new Transform({
290
+ transform(line, _, callback) {
291
+ linesCount++;
292
+ const replacement = isReplacementsObject
293
+ ? Object.hasOwn(replacements, linesCount)
294
+ ? replacements[linesCount]
295
+ : line
296
+ : replacements;
297
+ return callback(null, `${replacement}\n`);
298
+ },
299
+ flush(callback) {
300
+ const remainingReplacementsKeys = Object.keys(replacements)
301
+ .map(Number)
302
+ .toSorted((a, b) => a - b)
303
+ .filter((lineNumber) => lineNumber > linesCount);
304
+ if (remainingReplacementsKeys.length)
305
+ this.push("\n".repeat(remainingReplacementsKeys[0] - linesCount - 1) +
306
+ remainingReplacementsKeys
307
+ .map((lineNumber, index) => index === 0 ||
308
+ lineNumber -
309
+ (remainingReplacementsKeys[index - 1] - 1) ===
310
+ 0
311
+ ? replacements[lineNumber]
312
+ : "\n".repeat(lineNumber -
313
+ remainingReplacementsKeys[index - 1] -
314
+ 1) + replacements[lineNumber])
315
+ .join("\n"));
316
+ callback();
317
+ },
318
+ }));
319
+ return [fileTempPath, filePath];
320
+ }
321
+ catch {
322
+ return [fileTempPath, null];
323
+ }
324
+ finally {
325
+ // Ensure that file handles are closed, even if an error occurred
326
+ await fileHandle?.close();
327
+ await fileTempHandle?.close();
328
+ }
329
+ }
330
+ else {
331
+ const escapedFilePath = escapeShellPath(filePath);
332
+ const escapedFileTempPath = escapeShellPath(fileTempPath);
333
+ const sedCommand = `sed -e s/.*/${replacements}/ -e /^$/s/^/${replacements}/ ${escapedFilePath}`;
334
+ const command = filePath.endsWith(".gz")
335
+ ? `zcat ${escapedFilePath} | ${sedCommand} | gzip > ${escapedFileTempPath}`
336
+ : `${sedCommand} > ${escapedFileTempPath}`;
337
+ try {
338
+ await exec(command);
339
+ return [fileTempPath, filePath];
340
+ }
341
+ catch {
342
+ return [fileTempPath, null];
343
+ }
344
+ }
345
+ }
346
+ else if (isReplacementsObject) {
247
347
  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
- }));
348
+ if (isReplacementsLineNumbered) {
349
+ const replacementsKeys = Object.keys(replacements)
350
+ .map(Number)
351
+ .toSorted((a, b) => a - b);
352
+ await write(fileTempPath, `${"\n".repeat(replacementsKeys[0] - 1) +
353
+ replacementsKeys
354
+ .map((lineNumber, index) => index === 0 ||
355
+ lineNumber - replacementsKeys[index - 1] - 1 === 0
356
+ ? replacements[lineNumber]
357
+ : "\n".repeat(lineNumber - replacementsKeys[index - 1] - 1) +
358
+ replacements[lineNumber])
359
+ .join("\n")}\n`);
360
+ }
361
+ else {
362
+ if (!totalItems)
363
+ throw new Error("INVALID_PARAMETERS");
364
+ await write(fileTempPath, `${`${replacements}\n`.repeat(totalItems)}\n`);
365
+ }
263
366
  return [fileTempPath, filePath];
264
367
  }
265
- finally {
266
- // Ensure that file handles are closed, even if an error occurred
267
- await fileHandle?.close();
268
- await fileTempHandle?.close();
368
+ catch {
369
+ return [fileTempPath, null];
269
370
  }
270
371
  }
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
372
  return [];
285
373
  };
286
374
  /**
@@ -293,20 +381,26 @@ export const replace = async (filePath, replacements) => {
293
381
  */
294
382
  export const append = async (filePath, data) => {
295
383
  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}`);
384
+ try {
385
+ if (await isExists(filePath)) {
386
+ await copyFile(filePath, fileTempPath);
387
+ if (!filePath.endsWith(".gz")) {
388
+ await appendFile(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
389
+ }
390
+ else {
391
+ const escapedFileTempPath = escapeShellPath(fileTempPath);
392
+ await exec(`echo '${(Array.isArray(data) ? data.join("\n") : data)
393
+ .toString()
394
+ .replace(/'/g, "\\'")}' | gzip - >> ${escapedFileTempPath}`);
395
+ }
305
396
  }
397
+ else
398
+ await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
399
+ return [fileTempPath, filePath];
400
+ }
401
+ catch {
402
+ return [fileTempPath, null];
306
403
  }
307
- else
308
- await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
309
- return [fileTempPath, filePath];
310
404
  };
311
405
  /**
312
406
  * Asynchronously prepends data to the beginning of a file.
@@ -337,6 +431,9 @@ export const prepend = async (filePath, data) => {
337
431
  },
338
432
  }));
339
433
  }
434
+ catch {
435
+ return [fileTempPath, null];
436
+ }
340
437
  finally {
341
438
  // Ensure that file handles are closed, even if an error occurred
342
439
  await fileHandle?.close();
@@ -347,15 +444,27 @@ export const prepend = async (filePath, data) => {
347
444
  const fileChildTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/tmp_$1");
348
445
  try {
349
446
  await write(fileChildTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
350
- await exec(`cat ${fileChildTempPath} ${filePath} > ${fileTempPath}`);
447
+ const escapedFilePath = escapeShellPath(filePath);
448
+ const escapedFileTempPath = escapeShellPath(fileTempPath);
449
+ const escapedFileChildTempPath = escapeShellPath(fileChildTempPath);
450
+ await exec(`cat ${escapedFileChildTempPath} ${escapedFilePath} > ${escapedFileTempPath}`);
451
+ }
452
+ catch {
453
+ return [fileTempPath, null];
351
454
  }
352
455
  finally {
353
456
  await unlink(fileChildTempPath);
354
457
  }
355
458
  }
356
459
  }
357
- else
358
- await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
460
+ else {
461
+ try {
462
+ await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
463
+ }
464
+ catch {
465
+ return [fileTempPath, null];
466
+ }
467
+ }
359
468
  return [fileTempPath, filePath];
360
469
  };
361
470
  /**
@@ -374,11 +483,18 @@ export const remove = async (filePath, linesToDelete) => {
374
483
  if (linesToDelete.some(Number.isNaN))
375
484
  throw new Error("UNVALID_LINE_NUMBERS");
376
485
  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];
486
+ try {
487
+ const escapedFilePath = escapeShellPath(filePath);
488
+ const escapedFileTempPath = escapeShellPath(fileTempPath);
489
+ const command = filePath.endsWith(".gz")
490
+ ? `zcat ${escapedFilePath} | sed '${_groupIntoRanges(linesToDelete, "d")}' | gzip > ${escapedFileTempPath}`
491
+ : `sed '${_groupIntoRanges(linesToDelete, "d")}' ${escapedFilePath} > ${escapedFileTempPath}`;
492
+ await exec(command);
493
+ return [fileTempPath, filePath];
494
+ }
495
+ catch {
496
+ return [fileTempPath, null];
497
+ }
382
498
  };
383
499
  /**
384
500
  * Asynchronously searches a file for lines matching specified criteria, using comparison and logical operators.
@@ -399,13 +515,20 @@ export const remove = async (filePath, linesToDelete) => {
399
515
  *
400
516
  * Note: Decodes each line for comparison and can handle complex queries with multiple conditions.
401
517
  */
402
- export const search = async (filePath, operator, comparedAtValue, logicalOperator, fieldType, fieldChildrenType, limit, offset, readWholeFile, secretKey) => {
518
+ export const search = async (filePath, operator, comparedAtValue, logicalOperator, searchIn, fieldType, fieldChildrenType, limit, offset, readWholeFile, secretKey) => {
403
519
  // Initialize a Map to store the matching lines with their line numbers.
404
520
  const matchingLines = {};
405
521
  // Initialize counters for line number, found items, and processed items.
406
522
  let linesCount = 0;
407
523
  const linesNumbers = new Set();
408
524
  let fileHandle = null;
525
+ const meetsConditions = (value) => (Array.isArray(operator) &&
526
+ Array.isArray(comparedAtValue) &&
527
+ ((logicalOperator === "or" &&
528
+ operator.some((single_operator, index) => compare(single_operator, value, comparedAtValue[index], fieldType))) ||
529
+ operator.every((single_operator, index) => compare(single_operator, value, comparedAtValue[index], fieldType)))) ||
530
+ (!Array.isArray(operator) &&
531
+ compare(operator, value, comparedAtValue, fieldType));
409
532
  try {
410
533
  // Open the file for reading.
411
534
  fileHandle = await open(filePath, "r");
@@ -415,18 +538,19 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
415
538
  for await (const line of rl) {
416
539
  // Increment the line count for each line.
417
540
  linesCount++;
541
+ // Search only in provided linesNumbers
542
+ if (searchIn?.size &&
543
+ (!searchIn.has(linesCount) || searchIn.has(-linesCount)))
544
+ continue;
418
545
  // Decode the line for comparison.
419
546
  const decodedLine = decode(line, fieldType, fieldChildrenType, secretKey);
420
547
  // 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));
548
+ const doesMeetCondition = (Array.isArray(decodedLine) &&
549
+ !Array.isArray(decodedLine[1]) &&
550
+ decodedLine.some(meetsConditions)) ||
551
+ meetsConditions(decodedLine);
428
552
  // If the line meets the conditions, process it.
429
- if (meetsConditions) {
553
+ if (doesMeetCondition) {
430
554
  // Increment the found items counter.
431
555
  linesNumbers.add(linesCount);
432
556
  // Check if the line should be skipped based on the offset.
@@ -447,36 +571,14 @@ export const search = async (filePath, operator, comparedAtValue, logicalOperato
447
571
  ? [matchingLines, linesNumbers.size, linesNumbers]
448
572
  : [null, 0, null];
449
573
  }
574
+ catch {
575
+ return [null, 0, null];
576
+ }
450
577
  finally {
451
578
  // Close the file handle in the finally block to ensure it is closed even if an error occurs.
452
579
  await fileHandle?.close();
453
580
  }
454
581
  };
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
582
  /**
481
583
  * Asynchronously calculates the sum of numerical values from specified lines in a file.
482
584
  *