inibase 1.0.0-rc.26 → 1.0.0-rc.28

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/dist/file.js CHANGED
@@ -1,9 +1,28 @@
1
- import { open, access, constants, writeFile, } from "node:fs/promises";
1
+ import { open, access, writeFile, readFile, constants as fsConstants, } from "node:fs/promises";
2
2
  import { createInterface } from "node:readline";
3
3
  import { Transform } from "node:stream";
4
4
  import { pipeline } from "node:stream/promises";
5
+ import { createGzip, createGunzip, gzip as gzipAsync, gunzip as gunzipAsync, } from "node:zlib";
6
+ import { promisify } from "node:util";
5
7
  import { detectFieldType, isArrayOfArrays, isNumber, isObject, } from "./utils.js";
6
8
  import { encodeID, comparePassword } from "./utils.server.js";
9
+ import Config from "./config.js";
10
+ const gzip = promisify(gzipAsync);
11
+ const gunzip = promisify(gunzipAsync);
12
+ export const write = async (filePath, data, disableCompression = false) => {
13
+ await writeFile(filePath, Config.isCompressionEnabled && !disableCompression ? await gzip(data) : data);
14
+ };
15
+ export const read = async (filePath, disableCompression = false) => {
16
+ return Config.isCompressionEnabled && !disableCompression
17
+ ? (await gunzip(await readFile(filePath))).toString()
18
+ : (await readFile(filePath)).toString();
19
+ };
20
+ const _pipeline = async (rl, writeStream, transform) => {
21
+ if (Config.isCompressionEnabled)
22
+ await pipeline(rl, transform, createGzip(), writeStream);
23
+ else
24
+ await pipeline(rl, transform, writeStream);
25
+ };
7
26
  /**
8
27
  * Creates a readline interface for a given file handle.
9
28
  *
@@ -11,13 +30,12 @@ import { encodeID, comparePassword } from "./utils.server.js";
11
30
  * @returns A readline.Interface instance configured with the provided file stream.
12
31
  */
13
32
  const readLineInternface = (fileHandle) => {
14
- const [major, minor, patch] = process.versions.node.split(".").map(Number);
15
- return major > 18 || (major === 18 && minor >= 11)
16
- ? fileHandle.readLines()
17
- : createInterface({
18
- input: fileHandle.createReadStream(), // .pipe(createInflate())
19
- crlfDelay: Infinity,
20
- });
33
+ return createInterface({
34
+ input: Config.isCompressionEnabled
35
+ ? fileHandle.createReadStream().pipe(createGunzip())
36
+ : fileHandle.createReadStream(),
37
+ crlfDelay: Infinity,
38
+ });
21
39
  };
22
40
  /**
23
41
  * Checks if a file or directory exists at the specified path.
@@ -27,7 +45,7 @@ const readLineInternface = (fileHandle) => {
27
45
  */
28
46
  export const isExists = async (path) => {
29
47
  try {
30
- await access(path, constants.R_OK | constants.W_OK);
48
+ await access(path, fsConstants.R_OK | fsConstants.W_OK);
31
49
  return true;
32
50
  }
33
51
  catch {
@@ -44,23 +62,33 @@ const delimiters = [",", "|", "&", "$", "#", "@", "^", ":", "!", ";"];
44
62
  const secureString = (input) => {
45
63
  if (["true", "false"].includes(String(input)))
46
64
  return input ? 1 : 0;
47
- return typeof input === "string"
48
- ? decodeURIComponent(input.replace(/%(?![0-9][0-9a-fA-F]+)/g, ""))
49
- .replaceAll("<", "&lt;")
50
- .replaceAll(">", "&gt;")
51
- .replaceAll(",", "%2C")
52
- .replaceAll("|", "%7C")
53
- .replaceAll("&", "%26")
54
- .replaceAll("$", "%24")
55
- .replaceAll("#", "%23")
56
- .replaceAll("@", "%40")
57
- .replaceAll("^", "%5E")
58
- .replaceAll(":", "%3A")
59
- .replaceAll("!", "%21")
60
- .replaceAll(";", "%3B")
61
- .replaceAll("\n", "\\n")
62
- .replaceAll("\r", "\\r")
63
- : input;
65
+ if (typeof input !== "string")
66
+ return input;
67
+ let decodedInput = null;
68
+ try {
69
+ decodedInput = decodeURIComponent(input);
70
+ }
71
+ catch (_error) {
72
+ decodedInput = decodeURIComponent(input.replace(/%(?![0-9][0-9a-fA-F]+)/g, ""));
73
+ }
74
+ const replacements = {
75
+ "<": "&lt;",
76
+ ">": "&gt;",
77
+ ",": "%2C",
78
+ "|": "%7C",
79
+ "&": "%26",
80
+ $: "%24",
81
+ "#": "%23",
82
+ "@": "%40",
83
+ "^": "%5E",
84
+ ":": "%3A",
85
+ "!": "%21",
86
+ ";": "%3B",
87
+ "\n": "\\n",
88
+ "\r": "\\r",
89
+ };
90
+ // Replace characters using a single regular expression.
91
+ return decodedInput.replace(/[<>,|&$#@^:!\n\r]/g, (match) => replacements[match]);
64
92
  };
65
93
  /**
66
94
  * Secures each element in an array or a single value using secureString.
@@ -93,31 +121,11 @@ const joinMultidimensionalArray = (arr, delimiter_index = 0) => {
93
121
  * @returns The secured and/or joined string.
94
122
  */
95
123
  export const encode = (input, secretKey) => {
124
+ // Use the optimized secureArray and joinMultidimensionalArray functions.
96
125
  return Array.isArray(input)
97
126
  ? joinMultidimensionalArray(secureArray(input))
98
127
  : secureString(input);
99
128
  };
100
- /**
101
- * Reverses the encoding done by 'secureString'. Replaces encoded characters with their original symbols.
102
- *
103
- * @param input - Encoded string.
104
- * @returns Decoded string or null if input is empty.
105
- */
106
- const unSecureString = (input) => input
107
- .replaceAll("&lt;", "<")
108
- .replaceAll("&gt;", ">")
109
- .replaceAll("%2C", ",")
110
- .replaceAll("%7C", "|")
111
- .replaceAll("%26", "&")
112
- .replaceAll("%24", "$")
113
- .replaceAll("%23", "#")
114
- .replaceAll("%40", "@")
115
- .replaceAll("%5E", "^")
116
- .replaceAll("%3A", ":")
117
- .replaceAll("%21", "!")
118
- .replaceAll("%3B", ";")
119
- .replaceAll("\\n", "\n")
120
- .replaceAll("\\r", "\r") || null;
121
129
  /**
122
130
  * Decodes each element in an array or a single value using unSecureString.
123
131
  *
@@ -125,6 +133,32 @@ const unSecureString = (input) => input
125
133
  * @returns An array with each element decoded, or a single decoded value.
126
134
  */
127
135
  const unSecureArray = (arr_str) => Array.isArray(arr_str) ? arr_str.map(unSecureArray) : unSecureString(arr_str);
136
+ /**
137
+ * Reverses the encoding done by 'secureString'. Replaces encoded characters with their original symbols.
138
+ *
139
+ * @param input - Encoded string.
140
+ * @returns Decoded string or null if input is empty.
141
+ */
142
+ const unSecureString = (input) => {
143
+ // Define a mapping of encoded characters to their original symbols.
144
+ const replacements = {
145
+ "&lt;": "<",
146
+ "%2C": ",",
147
+ "%7C": "|",
148
+ "%26": "&",
149
+ "%24": "$",
150
+ "%23": "#",
151
+ "%40": "@",
152
+ "%5E": "^",
153
+ "%3A": ":",
154
+ "%21": "!",
155
+ "%3B": ";",
156
+ "\\n": "\n",
157
+ "\\r": "\r",
158
+ };
159
+ // Replace encoded characters with their original symbols using the defined mapping.
160
+ return (input.replace(/%(2C|7C|26|24|23|40|5E|3A|21|3B|\\n|\\r)/g, (match) => replacements[match]) || null);
161
+ };
128
162
  /**
129
163
  * Reverses the process of 'joinMultidimensionalArray', splitting a string back into a multidimensional array.
130
164
  * It identifies delimiters used in the joined string and applies them recursively to reconstruct the original array structure.
@@ -133,16 +167,14 @@ const unSecureArray = (arr_str) => Array.isArray(arr_str) ? arr_str.map(unSecure
133
167
  * @returns Original array structure before joining, or the input if no delimiters are found.
134
168
  */
135
169
  const reverseJoinMultidimensionalArray = (joinedString) => {
170
+ // Helper function for recursive array splitting based on delimiters.
136
171
  const reverseJoinMultidimensionalArrayHelper = (arr, delimiter) => Array.isArray(arr)
137
172
  ? arr.map((ar) => reverseJoinMultidimensionalArrayHelper(ar, delimiter))
138
173
  : arr.split(delimiter);
174
+ // Identify available delimiters in the input.
139
175
  const availableDelimiters = delimiters.filter((delimiter) => joinedString.includes(delimiter));
140
- for (const delimiter of availableDelimiters) {
141
- joinedString = Array.isArray(joinedString)
142
- ? reverseJoinMultidimensionalArrayHelper(joinedString, delimiter)
143
- : joinedString.split(delimiter);
144
- }
145
- return joinedString;
176
+ // Apply delimiters recursively to reconstruct the original array structure.
177
+ return availableDelimiters.reduce((acc, delimiter) => reverseJoinMultidimensionalArrayHelper(acc, delimiter), joinedString);
146
178
  };
147
179
  /**
148
180
  * Decodes a value based on specified field types and optional secret key.
@@ -196,56 +228,53 @@ export const decode = (input, fieldType, fieldChildrenType, secretKey) => {
196
228
  if (input === null || input === "")
197
229
  return null;
198
230
  if (Array.isArray(fieldType))
231
+ // Detect the fieldType based on the input and the provided array of possible types.
199
232
  fieldType = detectFieldType(String(input), fieldType);
233
+ // Decode the input using the decodeHelper function.
200
234
  return decodeHelper(typeof input === "string"
201
235
  ? input.includes(",")
202
236
  ? unSecureArray(reverseJoinMultidimensionalArray(input))
203
237
  : unSecureString(input)
204
238
  : input, fieldType, fieldChildrenType, secretKey);
205
239
  };
206
- /**
207
- * Asynchronously reads and decodes data from a file at specified line numbers.
208
- * Decodes each line based on specified field types and an optional secret key.
209
- *
210
- * @param filePath - Path of the file to be read.
211
- * @param lineNumbers - Optional line number(s) to read from the file. If -1, reads the last line.
212
- * @param fieldType - Optional type of the field to guide decoding (e.g., 'number', 'boolean').
213
- * @param fieldChildrenType - Optional type for child elements in array inputs.
214
- * @param secretKey - Optional secret key for decoding, can be a string or Buffer.
215
- * @returns Promise resolving to a tuple:
216
- * 1. Record of line numbers and their decoded content or null if no lines are read.
217
- * 2. Total count of lines processed.
218
- */
219
- export const get = async (filePath, lineNumbers, fieldType, fieldChildrenType, secretKey) => {
220
- const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
221
- let lines = new Map(), lineCount = 0;
222
- if (!lineNumbers) {
223
- for await (const line of rl)
224
- lineCount++,
225
- lines.set(lineCount, decode(line, fieldType, fieldChildrenType, secretKey));
226
- }
227
- else if (lineNumbers === -1) {
228
- let lastLine = null;
229
- for await (const line of rl)
230
- lineCount++, (lastLine = line);
231
- if (lastLine)
232
- lines.set(lineCount, decode(lastLine, fieldType, fieldChildrenType, secretKey));
233
- }
234
- else {
235
- let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
236
- for await (const line of rl) {
237
- lineCount++;
238
- if (!lineNumbersArray.has(lineCount))
239
- continue;
240
- lines.set(lineCount, decode(line, fieldType, fieldChildrenType, secretKey));
241
- lineNumbersArray.delete(lineCount);
242
- if (!lineNumbersArray.size)
243
- break;
240
+ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, secretKey, readWholeFile = false) {
241
+ let fileHandle, rl;
242
+ try {
243
+ fileHandle = await open(filePath, "r");
244
+ rl = readLineInternface(fileHandle);
245
+ let lines = {}, linesCount = 0;
246
+ if (!lineNumbers) {
247
+ for await (const line of rl)
248
+ linesCount++,
249
+ (lines[linesCount] = decode(line, fieldType, fieldChildrenType, secretKey));
244
250
  }
251
+ else if (lineNumbers === -1) {
252
+ let lastLine = null;
253
+ for await (const line of rl)
254
+ linesCount++, (lastLine = line);
255
+ if (lastLine)
256
+ lines[linesCount] = decode(lastLine, fieldType, fieldChildrenType, secretKey);
257
+ }
258
+ else {
259
+ let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
260
+ for await (const line of rl) {
261
+ linesCount++;
262
+ if (!lineNumbersArray.has(linesCount))
263
+ continue;
264
+ lines[linesCount] = decode(line, fieldType, fieldChildrenType, secretKey);
265
+ lineNumbersArray.delete(linesCount);
266
+ if (!lineNumbersArray.size && !readWholeFile)
267
+ break;
268
+ }
269
+ }
270
+ return readWholeFile ? [lines, linesCount] : lines;
245
271
  }
246
- await fileHandle.close();
247
- return [lines.size ? Object.fromEntries(lines) : null, lineCount];
248
- };
272
+ finally {
273
+ // Ensure that file handles are closed, even if an error occurred
274
+ rl?.close();
275
+ await fileHandle?.close();
276
+ }
277
+ }
249
278
  /**
250
279
  * Asynchronously replaces specific lines in a file based on the provided replacements map or string.
251
280
  *
@@ -257,47 +286,52 @@ export const get = async (filePath, lineNumbers, fieldType, fieldChildrenType, s
257
286
  * Note: If the file doesn't exist and replacements is an object, it creates a new file with the specified replacements.
258
287
  */
259
288
  export const replace = async (filePath, replacements) => {
260
- let lineCount = 0;
261
- const fileHandle = await open(filePath, "r"), fileTempPath = filePath.replace(/([^/]+)\/?$/, `.tmp/${Date.now()}-$1`), fileTempHandle = await open(fileTempPath, "w"), rl = readLineInternface(fileHandle);
262
- if (isObject(replacements))
263
- await pipeline(rl, new Transform({
264
- transform(line, encoding, callback) {
265
- lineCount++;
266
- callback(null, (replacements.hasOwnProperty(lineCount)
267
- ? replacements[lineCount]
268
- : line) + "\n");
269
- },
270
- final(callback) {
271
- const newLinesNumbers = Object.entries(replacements)
272
- .map(([key, value]) => Number(key))
273
- .filter((num) => num > lineCount);
274
- if (newLinesNumbers.length) {
275
- if (Math.min(...newLinesNumbers) - lineCount - 1 > 1)
276
- this.push("\n".repeat(Math.min(...newLinesNumbers) - lineCount - 1));
277
- this.push(newLinesNumbers
278
- .map((newLineNumber) => replacements[newLineNumber])
279
- .join("\n") + "\n");
280
- }
281
- callback();
282
- },
283
- }),
284
- // createDeflate(),
285
- fileTempHandle.createWriteStream());
286
- else
287
- await pipeline(rl, new Transform({
288
- transform(line, encoding, callback) {
289
- lineCount++;
290
- callback(null, replacements + "\n");
291
- },
292
- }),
293
- // createDeflate(),
294
- fileTempHandle.createWriteStream());
295
- await fileHandle.close();
296
- await fileTempHandle.close();
297
- return [fileTempPath, filePath];
289
+ const fileTempPath = filePath.replace(/([^/]+)\/?$/, `.tmp/${Date.now()}-$1`);
290
+ if (await isExists(filePath)) {
291
+ let fileHandle, fileTempHandle, rl;
292
+ try {
293
+ let linesCount = 0;
294
+ fileHandle = await open(filePath, "r");
295
+ fileTempHandle = await open(fileTempPath, "w");
296
+ rl = readLineInternface(fileHandle);
297
+ await _pipeline(rl, fileTempHandle.createWriteStream(), new Transform({
298
+ transform(line, encoding, callback) {
299
+ linesCount++;
300
+ const replacement = isObject(replacements)
301
+ ? replacements.hasOwnProperty(linesCount)
302
+ ? replacements[linesCount]
303
+ : line
304
+ : replacements;
305
+ return callback(null, replacement + "\n");
306
+ },
307
+ }));
308
+ return [fileTempPath, filePath];
309
+ }
310
+ finally {
311
+ // Ensure that file handles are closed, even if an error occurred
312
+ rl?.close();
313
+ await fileHandle?.close();
314
+ await fileTempHandle?.close();
315
+ }
316
+ }
317
+ else if (isObject(replacements)) {
318
+ let replacementsKeys = Object.keys(replacements)
319
+ .map(Number)
320
+ .toSorted((a, b) => a - b);
321
+ await write(fileTempPath, "\n".repeat(replacementsKeys[0] - 1) +
322
+ replacementsKeys
323
+ .map((lineNumber, index) => index === 0 || lineNumber - replacementsKeys[index - 1] - 1 === 0
324
+ ? replacements[lineNumber]
325
+ : "\n".repeat(lineNumber - replacementsKeys[index - 1] - 1) +
326
+ replacements[lineNumber])
327
+ .join("\n") +
328
+ "\n");
329
+ return [fileTempPath, filePath];
330
+ }
331
+ return [];
298
332
  };
299
333
  /**
300
- * Asynchronously appends data to a file.
334
+ * Asynchronously appends data to the beginning of a file.
301
335
  *
302
336
  * @param filePath - Path of the file to append to.
303
337
  * @param data - Data to append. Can be a string, number, or an array of strings/numbers.
@@ -307,23 +341,32 @@ export const replace = async (filePath, replacements) => {
307
341
  export const append = async (filePath, data) => {
308
342
  const fileTempPath = filePath.replace(/([^/]+)\/?$/, `.tmp/${Date.now()}-$1`);
309
343
  if (await isExists(filePath)) {
310
- const fileHandle = await open(filePath, "r"), fileTempHandle = await open(fileTempPath, "w"), rl = readLineInternface(fileHandle);
311
- await pipeline(rl, new Transform({
312
- transform(line, encoding, callback) {
313
- callback(null, `${line}\n`);
314
- },
315
- final(callback) {
316
- this.push((Array.isArray(data) ? data.join("\n") : data) + "\n");
317
- callback();
318
- },
319
- }),
320
- // createDeflate(),
321
- fileTempHandle.createWriteStream());
322
- await fileHandle.close();
323
- await fileTempHandle.close();
344
+ let fileHandle, fileTempHandle, rl;
345
+ try {
346
+ fileHandle = await open(filePath, "r");
347
+ fileTempHandle = await open(fileTempPath, "w");
348
+ rl = readLineInternface(fileHandle);
349
+ let isAppended = false;
350
+ await _pipeline(rl, fileTempHandle.createWriteStream(), new Transform({
351
+ transform(line, encoding, callback) {
352
+ if (!isAppended) {
353
+ isAppended = true;
354
+ return callback(null, `${Array.isArray(data) ? data.join("\n") : data}\n${line}\n`);
355
+ }
356
+ else
357
+ return callback(null, `${line}\n`);
358
+ },
359
+ }));
360
+ }
361
+ finally {
362
+ // Ensure that file handles are closed, even if an error occurred
363
+ rl?.close();
364
+ await fileHandle?.close();
365
+ await fileTempHandle?.close();
366
+ }
324
367
  }
325
368
  else
326
- await writeFile(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
369
+ await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
327
370
  return [fileTempPath, filePath];
328
371
  };
329
372
  /**
@@ -336,41 +379,22 @@ export const append = async (filePath, data) => {
336
379
  * Note: Creates a temporary file during the process and replaces the original file with it after removing lines.
337
380
  */
338
381
  export const remove = async (filePath, linesToDelete) => {
339
- let lineCount = 0;
382
+ let linesCount = 0;
340
383
  const fileHandle = await open(filePath, "r"), fileTempPath = filePath.replace(/([^/]+)\/?$/, `.tmp/${Date.now()}-$1`), fileTempHandle = await open(fileTempPath, "w"), linesToDeleteArray = new Set(Array.isArray(linesToDelete)
341
384
  ? linesToDelete.map(Number)
342
385
  : [Number(linesToDelete)]), rl = readLineInternface(fileHandle);
343
- await pipeline(rl, new Transform({
386
+ await _pipeline(rl, fileTempHandle.createWriteStream(), new Transform({
344
387
  transform(line, encoding, callback) {
345
- lineCount++;
346
- if (!linesToDeleteArray.has(lineCount))
347
- callback(null, `${line}\n`);
348
- callback();
388
+ linesCount++;
389
+ return !linesToDeleteArray.has(linesCount)
390
+ ? callback(null, `${line}\n`)
391
+ : callback();
349
392
  },
350
- }),
351
- // createDeflate(),
352
- fileTempHandle.createWriteStream());
393
+ }));
353
394
  await fileTempHandle.close();
354
395
  await fileHandle.close();
355
396
  return [fileTempPath, filePath];
356
397
  };
357
- /**
358
- * Asynchronously counts the number of lines in a file.
359
- *
360
- * @param filePath - Path of the file to count lines in.
361
- * @returns Promise<number>. The number of lines in the file.
362
- *
363
- * Note: Reads through the file line by line to count the total number of lines.
364
- */
365
- export const count = async (filePath) => {
366
- // return Number((await exec(`wc -l < ${filePath}`)).stdout.trim());
367
- let lineCount = 0;
368
- const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
369
- for await (const line of rl)
370
- lineCount++;
371
- await fileHandle.close();
372
- return lineCount;
373
- };
374
398
  /**
375
399
  * Evaluates a comparison between two values based on a specified operator and field types.
376
400
  *
@@ -384,64 +408,118 @@ export const count = async (filePath) => {
384
408
  * Note: Handles various data types and comparison logic, including special handling for passwords and regex patterns.
385
409
  */
386
410
  const handleComparisonOperator = (operator, originalValue, comparedAtValue, fieldType, fieldChildrenType) => {
387
- if (Array.isArray(fieldType))
411
+ // Determine the field type if it's an array of potential types.
412
+ if (Array.isArray(fieldType)) {
388
413
  fieldType = detectFieldType(String(originalValue), fieldType);
389
- if (Array.isArray(comparedAtValue) && !["[]", "![]"].includes(operator))
414
+ }
415
+ // Handle comparisons involving arrays.
416
+ if (Array.isArray(comparedAtValue) && !["[]", "![]"].includes(operator)) {
390
417
  return comparedAtValue.some((comparedAtValueSingle) => handleComparisonOperator(operator, originalValue, comparedAtValueSingle, fieldType));
391
- // check if not array or object // it can't be array or object!
418
+ }
419
+ // Switch statement for different comparison operators.
392
420
  switch (operator) {
421
+ // Equal (Case Insensitive for strings, specific handling for passwords and booleans).
393
422
  case "=":
394
- switch (fieldType) {
395
- case "password":
396
- return typeof originalValue === "string" &&
397
- typeof comparedAtValue === "string"
398
- ? comparePassword(originalValue, comparedAtValue)
399
- : false;
400
- case "boolean":
401
- return Number(originalValue) - Number(comparedAtValue) === 0;
402
- default:
403
- return originalValue === comparedAtValue;
404
- }
423
+ return isEqual(originalValue, comparedAtValue, fieldType);
424
+ // Not Equal.
405
425
  case "!=":
406
- return !handleComparisonOperator("=", originalValue, comparedAtValue, fieldType);
426
+ return !isEqual(originalValue, comparedAtValue, fieldType);
427
+ // Greater Than.
407
428
  case ">":
408
429
  return (originalValue !== null &&
409
430
  comparedAtValue !== null &&
410
431
  originalValue > comparedAtValue);
432
+ // Less Than.
411
433
  case "<":
412
434
  return (originalValue !== null &&
413
435
  comparedAtValue !== null &&
414
436
  originalValue < comparedAtValue);
437
+ // Greater Than or Equal.
415
438
  case ">=":
416
439
  return (originalValue !== null &&
417
440
  comparedAtValue !== null &&
418
441
  originalValue >= comparedAtValue);
442
+ // Less Than or Equal.
419
443
  case "<=":
420
444
  return (originalValue !== null &&
421
445
  comparedAtValue !== null &&
422
446
  originalValue <= comparedAtValue);
447
+ // Array Contains (equality check for arrays).
423
448
  case "[]":
424
- return ((Array.isArray(originalValue) &&
425
- Array.isArray(comparedAtValue) &&
426
- originalValue.some(comparedAtValue.includes)) ||
427
- (Array.isArray(originalValue) &&
428
- !Array.isArray(comparedAtValue) &&
429
- originalValue.includes(comparedAtValue)) ||
430
- (!Array.isArray(originalValue) &&
431
- Array.isArray(comparedAtValue) &&
432
- comparedAtValue.includes(originalValue)));
449
+ return isArrayEqual(originalValue, comparedAtValue);
450
+ // Array Does Not Contain.
433
451
  case "![]":
434
- return !handleComparisonOperator("[]", originalValue, comparedAtValue, fieldType);
452
+ return !isArrayEqual(originalValue, comparedAtValue);
453
+ // Wildcard Match (using regex pattern).
435
454
  case "*":
436
- return new RegExp(`^${(String(comparedAtValue).includes("%")
437
- ? String(comparedAtValue)
438
- : "%" + String(comparedAtValue) + "%").replace(/%/g, ".*")}$`, "i").test(String(originalValue));
455
+ return isWildcardMatch(originalValue, comparedAtValue);
456
+ // Not Wildcard Match.
439
457
  case "!*":
440
- return !handleComparisonOperator("*", originalValue, comparedAtValue, fieldType);
458
+ return !isWildcardMatch(originalValue, comparedAtValue);
459
+ // Unsupported operator.
441
460
  default:
442
- throw new Error(operator);
461
+ throw new Error(`Unsupported operator: ${operator}`);
443
462
  }
444
463
  };
464
+ /**
465
+ * Helper function to check equality based on the field type.
466
+ *
467
+ * @param originalValue - The original value.
468
+ * @param comparedAtValue - The value to compare against.
469
+ * @param fieldType - Type of the field.
470
+ * @returns boolean - Result of the equality check.
471
+ */
472
+ const isEqual = (originalValue, comparedAtValue, fieldType) => {
473
+ // Switch based on the field type for specific handling.
474
+ switch (fieldType) {
475
+ // Password comparison.
476
+ case "password":
477
+ return typeof originalValue === "string" &&
478
+ typeof comparedAtValue === "string"
479
+ ? comparePassword(originalValue, comparedAtValue)
480
+ : false;
481
+ // Boolean comparison.
482
+ case "boolean":
483
+ return Number(originalValue) === Number(comparedAtValue);
484
+ // Default comparison.
485
+ default:
486
+ return originalValue === comparedAtValue;
487
+ }
488
+ };
489
+ /**
490
+ * Helper function to check array equality.
491
+ *
492
+ * @param originalValue - The original value.
493
+ * @param comparedAtValue - The value to compare against.
494
+ * @returns boolean - Result of the array equality check.
495
+ */
496
+ const isArrayEqual = (originalValue, comparedAtValue) => {
497
+ return ((Array.isArray(originalValue) &&
498
+ Array.isArray(comparedAtValue) &&
499
+ originalValue.some((v) => comparedAtValue.includes(v))) ||
500
+ (Array.isArray(originalValue) &&
501
+ !Array.isArray(comparedAtValue) &&
502
+ originalValue.includes(comparedAtValue)) ||
503
+ (!Array.isArray(originalValue) &&
504
+ Array.isArray(comparedAtValue) &&
505
+ comparedAtValue.includes(originalValue)) ||
506
+ (!Array.isArray(originalValue) &&
507
+ !Array.isArray(comparedAtValue) &&
508
+ comparedAtValue === originalValue));
509
+ };
510
+ /**
511
+ * Helper function to check wildcard pattern matching using regex.
512
+ *
513
+ * @param originalValue - The original value.
514
+ * @param comparedAtValue - The value with wildcard pattern.
515
+ * @returns boolean - Result of the wildcard pattern matching.
516
+ */
517
+ const isWildcardMatch = (originalValue, comparedAtValue) => {
518
+ const wildcardPattern = `^${(String(comparedAtValue).includes("%")
519
+ ? String(comparedAtValue)
520
+ : "%" + String(comparedAtValue) + "%").replace(/%/g, ".*")}$`;
521
+ return new RegExp(wildcardPattern, "i").test(String(originalValue));
522
+ };
445
523
  /**
446
524
  * Asynchronously searches a file for lines matching specified criteria, using comparison and logical operators.
447
525
  *
@@ -462,34 +540,88 @@ const handleComparisonOperator = (operator, originalValue, comparedAtValue, fiel
462
540
  * Note: Decodes each line for comparison and can handle complex queries with multiple conditions.
463
541
  */
464
542
  export const search = async (filePath, operator, comparedAtValue, logicalOperator, fieldType, fieldChildrenType, limit, offset, readWholeFile, secretKey) => {
465
- let RETURN = new Map(), lineCount = 0, foundItems = 0;
466
- const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
467
- for await (const line of rl) {
468
- lineCount++;
469
- const decodedLine = decode(line, fieldType, fieldChildrenType, secretKey);
470
- if ((Array.isArray(operator) &&
471
- Array.isArray(comparedAtValue) &&
472
- ((logicalOperator &&
473
- logicalOperator === "or" &&
474
- operator.some((single_operator, index) => handleComparisonOperator(single_operator, decodedLine, comparedAtValue[index], fieldType))) ||
475
- operator.every((single_operator, index) => handleComparisonOperator(single_operator, decodedLine, comparedAtValue[index], fieldType)))) ||
476
- (!Array.isArray(operator) &&
477
- handleComparisonOperator(operator, decodedLine, comparedAtValue, fieldType))) {
478
- foundItems++;
479
- if (offset && foundItems < offset)
480
- continue;
481
- if (limit && foundItems > limit)
482
- if (readWholeFile)
543
+ // Initialize a Map to store the matching lines with their line numbers.
544
+ const matchingLines = {};
545
+ // Initialize counters for line number, found items, and processed items.
546
+ let linesCount = 0, foundItems = 0, linesNumbers = new Set();
547
+ let fileHandle, rl;
548
+ try {
549
+ // Open the file for reading.
550
+ fileHandle = await open(filePath, "r");
551
+ // Create a Readline interface to read the file line by line.
552
+ rl = readLineInternface(fileHandle);
553
+ // Iterate through each line in the file.
554
+ for await (const line of rl) {
555
+ // Increment the line count for each line.
556
+ linesCount++;
557
+ // Decode the line for comparison.
558
+ const decodedLine = decode(line, fieldType, fieldChildrenType, secretKey);
559
+ // Check if the line meets the specified conditions based on comparison and logical operators.
560
+ const meetsConditions = (Array.isArray(operator) &&
561
+ Array.isArray(comparedAtValue) &&
562
+ ((logicalOperator === "or" &&
563
+ operator.some((single_operator, index) => handleComparisonOperator(single_operator, decodedLine, comparedAtValue[index], fieldType))) ||
564
+ operator.every((single_operator, index) => handleComparisonOperator(single_operator, decodedLine, comparedAtValue[index], fieldType)))) ||
565
+ (!Array.isArray(operator) &&
566
+ handleComparisonOperator(operator, decodedLine, comparedAtValue, fieldType));
567
+ // If the line meets the conditions, process it.
568
+ if (meetsConditions) {
569
+ // Increment the found items counter.
570
+ foundItems++;
571
+ linesNumbers.add(linesCount);
572
+ // Check if the line should be skipped based on the offset.
573
+ if (offset && foundItems < offset)
483
574
  continue;
484
- else
485
- break;
486
- RETURN.set(lineCount, decodedLine);
575
+ // Check if the limit has been reached.
576
+ if (limit && foundItems > limit)
577
+ if (readWholeFile)
578
+ continue;
579
+ else
580
+ break;
581
+ // Store the decoded line in the result object.
582
+ matchingLines[linesCount] = decodedLine;
583
+ }
487
584
  }
585
+ // Convert the Map to an object using Object.fromEntries and return the result.
586
+ return foundItems
587
+ ? [
588
+ matchingLines,
589
+ readWholeFile ? foundItems : foundItems - 1,
590
+ linesNumbers.size ? linesNumbers : null,
591
+ ]
592
+ : [null, 0, null];
488
593
  }
489
- await fileHandle.close();
490
- return foundItems
491
- ? [Object.fromEntries(RETURN), readWholeFile ? foundItems : foundItems - 1]
492
- : [null, 0];
594
+ finally {
595
+ // Close the file handle in the finally block to ensure it is closed even if an error occurs.
596
+ rl?.close();
597
+ await fileHandle?.close();
598
+ }
599
+ };
600
+ /**
601
+ * Asynchronously counts the number of lines in a file.
602
+ *
603
+ * @param filePath - Path of the file to count lines in.
604
+ * @returns Promise<number>. The number of lines in the file.
605
+ *
606
+ * Note: Reads through the file line by line to count the total number of lines.
607
+ */
608
+ export const count = async (filePath) => {
609
+ // return Number((await exec(`wc -l < ${filePath}`)).stdout.trim());
610
+ let linesCount = 0;
611
+ if (await isExists(filePath)) {
612
+ let fileHandle, rl;
613
+ try {
614
+ (fileHandle = await open(filePath, "r")),
615
+ (rl = readLineInternface(fileHandle));
616
+ for await (const line of rl)
617
+ linesCount++;
618
+ }
619
+ finally {
620
+ rl?.close();
621
+ await fileHandle?.close();
622
+ }
623
+ }
624
+ return linesCount;
493
625
  };
494
626
  /**
495
627
  * Asynchronously calculates the sum of numerical values from specified lines in a file.
@@ -504,14 +636,14 @@ export const sum = async (filePath, lineNumbers) => {
504
636
  let sum = 0;
505
637
  const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
506
638
  if (lineNumbers) {
507
- let lineCount = 0;
639
+ let linesCount = 0;
508
640
  let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
509
641
  for await (const line of rl) {
510
- lineCount++;
511
- if (!lineNumbersArray.has(lineCount))
642
+ linesCount++;
643
+ if (!lineNumbersArray.has(linesCount))
512
644
  continue;
513
645
  sum += +(decode(line, "number") ?? 0);
514
- lineNumbersArray.delete(lineCount);
646
+ lineNumbersArray.delete(linesCount);
515
647
  if (!lineNumbersArray.size)
516
648
  break;
517
649
  }
@@ -535,16 +667,16 @@ export const max = async (filePath, lineNumbers) => {
535
667
  let max = 0;
536
668
  const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
537
669
  if (lineNumbers) {
538
- let lineCount = 0;
670
+ let linesCount = 0;
539
671
  let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
540
672
  for await (const line of rl) {
541
- lineCount++;
542
- if (!lineNumbersArray.has(lineCount))
673
+ linesCount++;
674
+ if (!lineNumbersArray.has(linesCount))
543
675
  continue;
544
676
  const lineContentNum = +(decode(line, "number") ?? 0);
545
677
  if (lineContentNum > max)
546
678
  max = lineContentNum;
547
- lineNumbersArray.delete(lineCount);
679
+ lineNumbersArray.delete(linesCount);
548
680
  if (!lineNumbersArray.size)
549
681
  break;
550
682
  }
@@ -571,16 +703,16 @@ export const min = async (filePath, lineNumbers) => {
571
703
  let min = 0;
572
704
  const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
573
705
  if (lineNumbers) {
574
- let lineCount = 0;
706
+ let linesCount = 0;
575
707
  let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
576
708
  for await (const line of rl) {
577
- lineCount++;
578
- if (!lineNumbersArray.has(lineCount))
709
+ linesCount++;
710
+ if (!lineNumbersArray.has(linesCount))
579
711
  continue;
580
712
  const lineContentNum = +(decode(line, "number") ?? 0);
581
713
  if (lineContentNum < min)
582
714
  min = lineContentNum;
583
- lineNumbersArray.delete(lineCount);
715
+ lineNumbersArray.delete(linesCount);
584
716
  if (!lineNumbersArray.size)
585
717
  break;
586
718
  }
@@ -611,7 +743,6 @@ export default class File {
611
743
  static remove = remove;
612
744
  static search = search;
613
745
  static replace = replace;
614
- static count = count;
615
746
  static encode = encode;
616
747
  static decode = decode;
617
748
  static isExists = isExists;
@@ -619,4 +750,7 @@ export default class File {
619
750
  static min = min;
620
751
  static max = max;
621
752
  static append = append;
753
+ static count = count;
754
+ static write = write;
755
+ static read = read;
622
756
  }