inibase 1.0.0-rc.25 → 1.0.0-rc.27

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,16 +1,51 @@
1
- import { open, rename, stat, writeFile, appendFile, } 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
+ import { Transform } from "node:stream";
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";
3
7
  import { detectFieldType, isArrayOfArrays, isNumber, isObject, } from "./utils.js";
4
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
+ };
26
+ /**
27
+ * Creates a readline interface for a given file handle.
28
+ *
29
+ * @param fileHandle - The file handle from which to create a read stream.
30
+ * @returns A readline.Interface instance configured with the provided file stream.
31
+ */
5
32
  const readLineInternface = (fileHandle) => {
6
33
  return createInterface({
7
- input: fileHandle.createReadStream(),
34
+ input: Config.isCompressionEnabled
35
+ ? fileHandle.createReadStream().pipe(createGunzip())
36
+ : fileHandle.createReadStream(),
8
37
  crlfDelay: Infinity,
9
38
  });
10
39
  };
40
+ /**
41
+ * Checks if a file or directory exists at the specified path.
42
+ *
43
+ * @param path - The path to the file or directory.
44
+ * @returns A Promise that resolves to true if the file/directory exists, false otherwise.
45
+ */
11
46
  export const isExists = async (path) => {
12
47
  try {
13
- await stat(path);
48
+ await access(path, fsConstants.R_OK | fsConstants.W_OK);
14
49
  return true;
15
50
  }
16
51
  catch {
@@ -18,28 +53,57 @@ export const isExists = async (path) => {
18
53
  }
19
54
  };
20
55
  const delimiters = [",", "|", "&", "$", "#", "@", "^", ":", "!", ";"];
56
+ /**
57
+ * Secures input by encoding/escaping characters.
58
+ *
59
+ * @param input - String, number, boolean, or null.
60
+ * @returns Encoded string for true/false, special characters in strings, or original input.
61
+ */
21
62
  const secureString = (input) => {
22
63
  if (["true", "false"].includes(String(input)))
23
64
  return input ? 1 : 0;
24
- return typeof input === "string"
25
- ? decodeURIComponent(input.replace(/%(?![0-9][0-9a-fA-F]+)/g, ""))
26
- .replaceAll("<", "&lt;")
27
- .replaceAll(">", "&gt;")
28
- .replaceAll(",", "%2C")
29
- .replaceAll("|", "%7C")
30
- .replaceAll("&", "%26")
31
- .replaceAll("$", "%24")
32
- .replaceAll("#", "%23")
33
- .replaceAll("@", "%40")
34
- .replaceAll("^", "%5E")
35
- .replaceAll(":", "%3A")
36
- .replaceAll("!", "%21")
37
- .replaceAll(";", "%3B")
38
- .replaceAll("\n", "\\n")
39
- .replaceAll("\r", "\\r")
40
- : 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]);
41
92
  };
93
+ /**
94
+ * Secures each element in an array or a single value using secureString.
95
+ *
96
+ * @param arr_str - An array or a single value of any type.
97
+ * @returns An array with each element secured, or a single secured value.
98
+ */
42
99
  const secureArray = (arr_str) => Array.isArray(arr_str) ? arr_str.map(secureArray) : secureString(arr_str);
100
+ /**
101
+ * Joins elements of a multidimensional array into a string.
102
+ *
103
+ * @param arr - A multidimensional array or a single level array.
104
+ * @param delimiter_index - Index for selecting delimiter, defaults to 0.
105
+ * @returns Joined string of array elements with appropriate delimiters.
106
+ */
43
107
  const joinMultidimensionalArray = (arr, delimiter_index = 0) => {
44
108
  delimiter_index++;
45
109
  if (isArrayOfArrays(arr))
@@ -47,283 +111,539 @@ const joinMultidimensionalArray = (arr, delimiter_index = 0) => {
47
111
  delimiter_index--;
48
112
  return arr.join(delimiters[delimiter_index]);
49
113
  };
114
+ /**
115
+ * Encodes the input using 'secureString' and 'joinMultidimensionalArray' functions.
116
+ * If the input is an array, it is first secured and then joined into a string.
117
+ * If the input is a single value, it is directly secured.
118
+ *
119
+ * @param input - A value or array of values (string, number, boolean, null).
120
+ * @param secretKey - Optional secret key for encoding, can be a string or Buffer.
121
+ * @returns The secured and/or joined string.
122
+ */
50
123
  export const encode = (input, secretKey) => {
124
+ // Use the optimized secureArray and joinMultidimensionalArray functions.
51
125
  return Array.isArray(input)
52
126
  ? joinMultidimensionalArray(secureArray(input))
53
127
  : secureString(input);
54
128
  };
55
- const unSecureString = (input) => input
56
- .replaceAll("&lt;", "<")
57
- .replaceAll("&gt;", ">")
58
- .replaceAll("%2C", ",")
59
- .replaceAll("%7C", "|")
60
- .replaceAll("%26", "&")
61
- .replaceAll("%24", "$")
62
- .replaceAll("%23", "#")
63
- .replaceAll("%40", "@")
64
- .replaceAll("%5E", "^")
65
- .replaceAll("%3A", ":")
66
- .replaceAll("%21", "!")
67
- .replaceAll("%3B", ";")
68
- .replaceAll("\\n", "\n")
69
- .replaceAll("\\r", "\r") || null;
129
+ /**
130
+ * Decodes each element in an array or a single value using unSecureString.
131
+ *
132
+ * @param arr_str - An array or a single value of any type.
133
+ * @returns An array with each element decoded, or a single decoded value.
134
+ */
70
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
+ };
162
+ /**
163
+ * Reverses the process of 'joinMultidimensionalArray', splitting a string back into a multidimensional array.
164
+ * It identifies delimiters used in the joined string and applies them recursively to reconstruct the original array structure.
165
+ *
166
+ * @param joinedString - A string, array, or multidimensional array.
167
+ * @returns Original array structure before joining, or the input if no delimiters are found.
168
+ */
71
169
  const reverseJoinMultidimensionalArray = (joinedString) => {
170
+ // Helper function for recursive array splitting based on delimiters.
72
171
  const reverseJoinMultidimensionalArrayHelper = (arr, delimiter) => Array.isArray(arr)
73
172
  ? arr.map((ar) => reverseJoinMultidimensionalArrayHelper(ar, delimiter))
74
173
  : arr.split(delimiter);
174
+ // Identify available delimiters in the input.
75
175
  const availableDelimiters = delimiters.filter((delimiter) => joinedString.includes(delimiter));
76
- for (const delimiter of availableDelimiters) {
77
- joinedString = Array.isArray(joinedString)
78
- ? reverseJoinMultidimensionalArrayHelper(joinedString, delimiter)
79
- : joinedString.split(delimiter);
80
- }
81
- return joinedString;
176
+ // Apply delimiters recursively to reconstruct the original array structure.
177
+ return availableDelimiters.reduce((acc, delimiter) => reverseJoinMultidimensionalArrayHelper(acc, delimiter), joinedString);
82
178
  };
179
+ /**
180
+ * Decodes a value based on specified field types and optional secret key.
181
+ * Handles different data types and structures, including nested arrays.
182
+ *
183
+ * @param value - The value to be decoded, can be string, number, or array.
184
+ * @param fieldType - Optional type of the field to guide decoding (e.g., 'number', 'boolean').
185
+ * @param fieldChildrenType - Optional type for children elements, used for arrays.
186
+ * @param secretKey - Optional secret key for decoding, can be string or Buffer.
187
+ * @returns Decoded value, transformed according to the specified field type(s).
188
+ */
83
189
  const decodeHelper = (value, fieldType, fieldChildrenType, secretKey) => {
84
- if (Array.isArray(value) && fieldType !== "array")
85
- return value.map((v) => decodeHelper(v, fieldType, fieldChildrenType, secretKey));
86
- switch (fieldType) {
87
- case "table":
88
- case "number":
89
- return isNumber(value) ? Number(value) : null;
90
- case "boolean":
91
- return typeof value === "string" ? value === "true" : Boolean(value);
92
- case "array":
93
- if (!Array.isArray(value))
94
- return [value];
95
- if (fieldChildrenType)
96
- return fieldChildrenType
97
- ? value.map((v) => decode(v, Array.isArray(fieldChildrenType)
98
- ? detectFieldType(v, fieldChildrenType)
99
- : fieldChildrenType, undefined, secretKey))
100
- : value;
101
- case "id":
102
- return isNumber(value) && secretKey
103
- ? encodeID(value, secretKey)
104
- : value;
105
- default:
106
- return value;
190
+ // Use a stack-based approach for efficient processing without recursion.
191
+ const stack = [{ value }];
192
+ while (stack.length > 0) {
193
+ // Explicitly check if stack.pop() is not undefined.
194
+ const stackItem = stack.pop();
195
+ if (!stackItem) {
196
+ // Skip the rest of the loop if the stack item is undefined.
197
+ continue;
198
+ }
199
+ const { value } = stackItem;
200
+ if (Array.isArray(value) && fieldType !== "array") {
201
+ // If the value is an array and the fieldType is not 'array', process each element.
202
+ stack.push(...value.map((v) => ({ value: v })));
203
+ }
204
+ else {
205
+ switch (fieldType) {
206
+ // Handle different field types with appropriate decoding logic.
207
+ case "table":
208
+ case "number":
209
+ return isNumber(value) ? Number(value) : null;
210
+ case "boolean":
211
+ return typeof value === "string" ? value === "true" : Boolean(value);
212
+ case "array":
213
+ if (!Array.isArray(value))
214
+ return [value];
215
+ if (fieldChildrenType)
216
+ // Decode each element in the array based on the specified fieldChildrenType.
217
+ return fieldChildrenType
218
+ ? value.map((v) => decode(v, Array.isArray(fieldChildrenType)
219
+ ? detectFieldType(v, fieldChildrenType)
220
+ : fieldChildrenType, undefined, secretKey))
221
+ : value;
222
+ case "id":
223
+ return isNumber(value) && secretKey
224
+ ? encodeID(value, secretKey)
225
+ : value;
226
+ default:
227
+ return value;
228
+ }
229
+ }
107
230
  }
108
231
  };
232
+ /**
233
+ * Decodes the input based on the specified field type(s) and an optional secret key.
234
+ * Handles different formats of input, including strings, numbers, and their array representations.
235
+ *
236
+ * @param input - The input to be decoded, can be a string, number, or null.
237
+ * @param fieldType - Optional type of the field to guide decoding (e.g., 'number', 'boolean').
238
+ * @param fieldChildrenType - Optional type for child elements in array inputs.
239
+ * @param secretKey - Optional secret key for decoding, can be a string or Buffer.
240
+ * @returns Decoded value as a string, number, boolean, or array of these, or null if no fieldType or input is null/empty.
241
+ */
109
242
  export const decode = (input, fieldType, fieldChildrenType, secretKey) => {
110
243
  if (!fieldType)
111
244
  return null;
112
245
  if (input === null || input === "")
113
246
  return null;
114
247
  if (Array.isArray(fieldType))
248
+ // Detect the fieldType based on the input and the provided array of possible types.
115
249
  fieldType = detectFieldType(String(input), fieldType);
250
+ // Decode the input using the decodeHelper function.
116
251
  return decodeHelper(typeof input === "string"
117
252
  ? input.includes(",")
118
253
  ? unSecureArray(reverseJoinMultidimensionalArray(input))
119
254
  : unSecureString(input)
120
255
  : input, fieldType, fieldChildrenType, secretKey);
121
256
  };
122
- export const get = async (filePath, lineNumbers, fieldType, fieldChildrenType, secretKey) => {
123
- const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
124
- let lines = new Map(), lineCount = 0;
125
- if (!lineNumbers) {
126
- for await (const line of rl)
127
- lineCount++,
128
- lines.set(lineCount, decode(line, fieldType, fieldChildrenType, secretKey));
129
- }
130
- else if (lineNumbers === -1) {
131
- let lastLine = null;
132
- for await (const line of rl)
133
- lineCount++, (lastLine = line);
134
- if (lastLine)
135
- lines.set(lineCount, decode(lastLine, fieldType, fieldChildrenType, secretKey));
136
- }
137
- else {
138
- let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
139
- for await (const line of rl) {
140
- lineCount++;
141
- if (!lineNumbersArray.has(lineCount))
142
- continue;
143
- lines.set(lineCount, decode(line, fieldType, fieldChildrenType, secretKey));
144
- lineNumbersArray.delete(lineCount);
145
- if (!lineNumbersArray.size)
146
- break;
257
+ export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, secretKey, readWholeFile) {
258
+ let fileHandle, rl;
259
+ try {
260
+ fileHandle = await open(filePath, "r");
261
+ rl = readLineInternface(fileHandle);
262
+ let lines = {}, linesCount = 0;
263
+ if (!lineNumbers) {
264
+ for await (const line of rl)
265
+ linesCount++,
266
+ (lines[linesCount] = decode(line, fieldType, fieldChildrenType, secretKey));
147
267
  }
148
- }
149
- await fileHandle.close();
150
- return [lines.size ? Object.fromEntries(lines) : null, lineCount];
151
- };
152
- export const replace = async (filePath, replacements) => {
153
- if (await isExists(filePath)) {
154
- let lineCount = 0;
155
- const fileHandle = await open(filePath, "r"), fileTempPath = `${filePath.replace(".inib", "")}-${Date.now()}.tmp`, fileTempHandle = await open(fileTempPath, "w"), rl = readLineInternface(fileHandle), writeStream = fileTempHandle.createWriteStream();
156
- if (isObject(replacements)) {
157
- if (!(replacements instanceof Map))
158
- replacements = new Map(Object.entries(replacements));
268
+ else if (lineNumbers === -1) {
269
+ let lastLine = null;
270
+ for await (const line of rl)
271
+ linesCount++, (lastLine = line);
272
+ if (lastLine)
273
+ lines[linesCount] = decode(lastLine, fieldType, fieldChildrenType, secretKey);
274
+ }
275
+ else {
276
+ let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
159
277
  for await (const line of rl) {
160
- lineCount++;
161
- writeStream.write((replacements.has(lineCount.toString())
162
- ? replacements.get(lineCount.toString())
163
- : line) + "\n");
164
- }
165
- const newLinesNumbers = new Set([...replacements.keys()].filter((num) => num > lineCount));
166
- if (newLinesNumbers.size) {
167
- if (Math.min(...newLinesNumbers) - lineCount - 1 > 1)
168
- writeStream.write("\n".repeat(Math.min(...newLinesNumbers) - lineCount - 1));
169
- for await (const newLineNumber of newLinesNumbers)
170
- writeStream.write(replacements.get(newLineNumber.toString()) + "\n");
278
+ linesCount++;
279
+ if (!lineNumbersArray.has(linesCount))
280
+ continue;
281
+ lines[linesCount] = decode(line, fieldType, fieldChildrenType, secretKey);
282
+ lineNumbersArray.delete(linesCount);
283
+ if (!lineNumbersArray.size && !readWholeFile)
284
+ break;
171
285
  }
172
286
  }
173
- else
174
- for await (const _line of rl)
175
- writeStream.write(replacements + "\n");
176
- await fileHandle.close();
177
- await fileTempHandle.close();
178
- await rename(fileTempPath, filePath);
287
+ return readWholeFile ? [lines, linesCount] : lines;
179
288
  }
180
- else if (isObject(replacements)) {
181
- if (!(replacements instanceof Map))
182
- replacements = new Map(Object.entries(replacements).map(([key, value]) => [Number(key), value]));
183
- await writeFile(filePath, (Math.min(...replacements.keys()) - 1 > 1
184
- ? "\n".repeat(Math.min(...replacements.keys()) - 1)
185
- : "") +
186
- Array.from(new Map([...replacements.entries()].sort(([keyA], [keyB]) => keyA - keyB)).values()).join("\n") +
187
- "\n");
289
+ finally {
290
+ // Ensure that file handles are closed, even if an error occurred
291
+ rl?.close();
292
+ await fileHandle?.close();
293
+ }
294
+ }
295
+ /**
296
+ * Asynchronously replaces specific lines in a file based on the provided replacements map or string.
297
+ *
298
+ * @param filePath - Path of the file to modify.
299
+ * @param replacements - Map of line numbers to replacement values, or a single replacement value for all lines.
300
+ * Can be a string, number, boolean, null, array of these types, or a Record/Map of line numbers to these types.
301
+ * @returns Promise<string[]>
302
+ *
303
+ * Note: If the file doesn't exist and replacements is an object, it creates a new file with the specified replacements.
304
+ */
305
+ export const replace = async (filePath, replacements) => {
306
+ let fileHandle, fileTempHandle, rl;
307
+ const fileTempPath = filePath.replace(/([^/]+)\/?$/, `.tmp/${Date.now()}-$1`);
308
+ try {
309
+ let linesCount = 0;
310
+ fileHandle = await open(filePath, "r");
311
+ fileTempHandle = await open(fileTempPath, "w");
312
+ rl = readLineInternface(fileHandle);
313
+ await _pipeline(rl, fileTempHandle.createWriteStream(), new Transform({
314
+ transform(line, encoding, callback) {
315
+ linesCount++;
316
+ const replacement = isObject(replacements)
317
+ ? replacements.hasOwnProperty(linesCount)
318
+ ? replacements[linesCount]
319
+ : line
320
+ : replacements;
321
+ callback(null, replacement + "\n");
322
+ },
323
+ }));
324
+ return [fileTempPath, filePath];
325
+ }
326
+ finally {
327
+ // Ensure that file handles are closed, even if an error occurred
328
+ rl?.close();
329
+ await fileHandle?.close();
330
+ await fileTempHandle?.close();
188
331
  }
189
332
  };
190
- export const append = async (filePath, data, startsAt = 1) => {
191
- let currentNumberOfLines = 0;
192
- const doesFileExists = await isExists(filePath);
193
- if (doesFileExists)
194
- currentNumberOfLines = await count(filePath);
195
- await appendFile(filePath, (currentNumberOfLines > 0
196
- ? startsAt - currentNumberOfLines - 1 > 0
197
- ? "\n".repeat(startsAt - currentNumberOfLines - 1)
198
- : ""
199
- : "") +
200
- (Array.isArray(data) ? data.join("\n") : data) +
201
- "\n");
333
+ /**
334
+ * Asynchronously appends data to the beginning of a file.
335
+ *
336
+ * @param filePath - Path of the file to append to.
337
+ * @param data - Data to append. Can be a string, number, or an array of strings/numbers.
338
+ * @returns Promise<string[]>. Modifies the file by appending data.
339
+ *
340
+ */
341
+ export const append = async (filePath, data) => {
342
+ const fileTempPath = filePath.replace(/([^/]+)\/?$/, `.tmp/${Date.now()}-$1`);
343
+ if (await isExists(filePath)) {
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
+ callback(null, `${Array.isArray(data) ? data.join("\n") : data}\n${line}\n`);
355
+ }
356
+ else
357
+ 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
+ }
367
+ }
368
+ else
369
+ await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
370
+ return [fileTempPath, filePath];
202
371
  };
372
+ /**
373
+ * Asynchronously removes specified lines from a file.
374
+ *
375
+ * @param filePath - Path of the file from which lines are to be removed.
376
+ * @param linesToDelete - A single line number or an array of line numbers to be deleted.
377
+ * @returns Promise<string[]>. Modifies the file by removing specified lines.
378
+ *
379
+ * Note: Creates a temporary file during the process and replaces the original file with it after removing lines.
380
+ */
203
381
  export const remove = async (filePath, linesToDelete) => {
204
- let lineCount = 0;
205
- const fileHandle = await open(filePath, "r"), fileTempPath = `${filePath.replace(".inib", "")}-${Date.now()}.tmp`, fileTempHandle = await open(fileTempPath, "w"), linesToDeleteArray = new Set(Array.isArray(linesToDelete)
382
+ let linesCount = 0;
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)
206
384
  ? linesToDelete.map(Number)
207
- : [Number(linesToDelete)]), rl = readLineInternface(fileHandle), writeStream = fileTempHandle.createWriteStream();
208
- for await (const line of rl) {
209
- lineCount++;
210
- if (!linesToDeleteArray.has(lineCount))
211
- writeStream.write(`${line}\n`);
212
- }
213
- await rename(fileTempPath, filePath);
385
+ : [Number(linesToDelete)]), rl = readLineInternface(fileHandle);
386
+ await _pipeline(rl, fileTempHandle.createWriteStream(), new Transform({
387
+ transform(line, encoding, callback) {
388
+ linesCount++;
389
+ if (!linesToDeleteArray.has(linesCount))
390
+ callback(null, `${line}\n`);
391
+ callback();
392
+ },
393
+ }));
214
394
  await fileTempHandle.close();
215
395
  await fileHandle.close();
396
+ return [fileTempPath, filePath];
216
397
  };
217
- export const count = async (filePath) => {
218
- // return Number((await exec(`wc -l < ${filePath}`)).stdout.trim());
219
- let lineCount = 0;
220
- const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
221
- for await (const line of rl)
222
- lineCount++;
223
- await fileHandle.close();
224
- return lineCount;
225
- };
398
+ /**
399
+ * Evaluates a comparison between two values based on a specified operator and field types.
400
+ *
401
+ * @param operator - The comparison operator (e.g., '=', '!=', '>', '<', '>=', '<=', '[]', '![]', '*', '!*').
402
+ * @param originalValue - The value to compare, can be a single value or an array of values.
403
+ * @param comparedAtValue - The value or values to compare against.
404
+ * @param fieldType - Optional type of the field to guide comparison (e.g., 'password', 'boolean').
405
+ * @param fieldChildrenType - Optional type for child elements in array inputs.
406
+ * @returns boolean - Result of the comparison operation.
407
+ *
408
+ * Note: Handles various data types and comparison logic, including special handling for passwords and regex patterns.
409
+ */
226
410
  const handleComparisonOperator = (operator, originalValue, comparedAtValue, fieldType, fieldChildrenType) => {
227
- if (Array.isArray(fieldType))
411
+ // Determine the field type if it's an array of potential types.
412
+ if (Array.isArray(fieldType)) {
228
413
  fieldType = detectFieldType(String(originalValue), fieldType);
229
- if (Array.isArray(comparedAtValue) && !["[]", "![]"].includes(operator))
414
+ }
415
+ // Handle comparisons involving arrays.
416
+ if (Array.isArray(comparedAtValue) && !["[]", "![]"].includes(operator)) {
230
417
  return comparedAtValue.some((comparedAtValueSingle) => handleComparisonOperator(operator, originalValue, comparedAtValueSingle, fieldType));
231
- // check if not array or object // it can't be array or object!
418
+ }
419
+ // Switch statement for different comparison operators.
232
420
  switch (operator) {
421
+ // Equal (Case Insensitive for strings, specific handling for passwords and booleans).
233
422
  case "=":
234
- switch (fieldType) {
235
- case "password":
236
- return typeof originalValue === "string" &&
237
- typeof comparedAtValue === "string"
238
- ? comparePassword(originalValue, comparedAtValue)
239
- : false;
240
- case "boolean":
241
- return Number(originalValue) - Number(comparedAtValue) === 0;
242
- default:
243
- return originalValue === comparedAtValue;
244
- }
423
+ return isEqual(originalValue, comparedAtValue, fieldType);
424
+ // Not Equal.
245
425
  case "!=":
246
- return !handleComparisonOperator("=", originalValue, comparedAtValue, fieldType);
426
+ return !isEqual(originalValue, comparedAtValue, fieldType);
427
+ // Greater Than.
247
428
  case ">":
248
429
  return (originalValue !== null &&
249
430
  comparedAtValue !== null &&
250
431
  originalValue > comparedAtValue);
432
+ // Less Than.
251
433
  case "<":
252
434
  return (originalValue !== null &&
253
435
  comparedAtValue !== null &&
254
436
  originalValue < comparedAtValue);
437
+ // Greater Than or Equal.
255
438
  case ">=":
256
439
  return (originalValue !== null &&
257
440
  comparedAtValue !== null &&
258
441
  originalValue >= comparedAtValue);
442
+ // Less Than or Equal.
259
443
  case "<=":
260
444
  return (originalValue !== null &&
261
445
  comparedAtValue !== null &&
262
446
  originalValue <= comparedAtValue);
447
+ // Array Contains (equality check for arrays).
263
448
  case "[]":
264
- return ((Array.isArray(originalValue) &&
265
- Array.isArray(comparedAtValue) &&
266
- originalValue.some(comparedAtValue.includes)) ||
267
- (Array.isArray(originalValue) &&
268
- !Array.isArray(comparedAtValue) &&
269
- originalValue.includes(comparedAtValue)) ||
270
- (!Array.isArray(originalValue) &&
271
- Array.isArray(comparedAtValue) &&
272
- comparedAtValue.includes(originalValue)));
449
+ return isArrayEqual(originalValue, comparedAtValue);
450
+ // Array Does Not Contain.
273
451
  case "![]":
274
- return !handleComparisonOperator("[]", originalValue, comparedAtValue, fieldType);
452
+ return !isArrayEqual(originalValue, comparedAtValue);
453
+ // Wildcard Match (using regex pattern).
275
454
  case "*":
276
- return new RegExp(`^${(String(comparedAtValue).includes("%")
277
- ? String(comparedAtValue)
278
- : "%" + String(comparedAtValue) + "%").replace(/%/g, ".*")}$`, "i").test(String(originalValue));
455
+ return isWildcardMatch(originalValue, comparedAtValue);
456
+ // Not Wildcard Match.
279
457
  case "!*":
280
- return !handleComparisonOperator("*", originalValue, comparedAtValue, fieldType);
458
+ return !isWildcardMatch(originalValue, comparedAtValue);
459
+ // Unsupported operator.
281
460
  default:
282
- throw new Error(operator);
461
+ throw new Error(`Unsupported operator: ${operator}`);
283
462
  }
284
463
  };
285
- export const search = async (filePath, operator, comparedAtValue, logicalOperator, fieldType, fieldChildrenType, limit, offset, readWholeFile, secretKey) => {
286
- let RETURN = new Map(), lineCount = 0, foundItems = 0;
287
- const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
288
- for await (const line of rl) {
289
- lineCount++;
290
- const decodedLine = decode(line, fieldType, fieldChildrenType, secretKey);
291
- if ((Array.isArray(operator) &&
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) &&
292
504
  Array.isArray(comparedAtValue) &&
293
- ((logicalOperator &&
294
- logicalOperator === "or" &&
295
- operator.some((single_operator, index) => handleComparisonOperator(single_operator, decodedLine, comparedAtValue[index], fieldType))) ||
296
- operator.every((single_operator, index) => handleComparisonOperator(single_operator, decodedLine, comparedAtValue[index], fieldType)))) ||
297
- (!Array.isArray(operator) &&
298
- handleComparisonOperator(operator, decodedLine, comparedAtValue, fieldType))) {
299
- foundItems++;
300
- if (offset && foundItems < offset)
301
- continue;
302
- if (limit && foundItems > limit)
303
- if (readWholeFile)
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
+ };
523
+ /**
524
+ * Asynchronously searches a file for lines matching specified criteria, using comparison and logical operators.
525
+ *
526
+ * @param filePath - Path of the file to search.
527
+ * @param operator - Comparison operator(s) for evaluation (e.g., '=', '!=', '>', '<').
528
+ * @param comparedAtValue - Value(s) to compare each line against.
529
+ * @param logicalOperator - Optional logical operator ('and' or 'or') for combining multiple comparisons.
530
+ * @param fieldType - Optional type of the field to guide comparison.
531
+ * @param fieldChildrenType - Optional type for child elements in array inputs.
532
+ * @param limit - Optional limit on the number of results to return.
533
+ * @param offset - Optional offset to start returning results from.
534
+ * @param readWholeFile - Flag to indicate whether to continue reading the file after reaching the limit.
535
+ * @param secretKey - Optional secret key for decoding, can be a string or Buffer.
536
+ * @returns Promise resolving to a tuple:
537
+ * 1. Record of line numbers and their content that match the criteria or null if none.
538
+ * 2. The count of found items or processed items based on the 'readWholeFile' flag.
539
+ *
540
+ * Note: Decodes each line for comparison and can handle complex queries with multiple conditions.
541
+ */
542
+ export const search = async (filePath, operator, comparedAtValue, logicalOperator, fieldType, fieldChildrenType, limit, offset, readWholeFile, secretKey) => {
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)
304
574
  continue;
305
- else
306
- break;
307
- 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
+ }
308
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];
593
+ }
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();
309
598
  }
310
- await fileHandle.close();
311
- return foundItems
312
- ? [Object.fromEntries(RETURN), readWholeFile ? foundItems : foundItems - 1]
313
- : [null, 0];
314
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;
625
+ };
626
+ /**
627
+ * Asynchronously calculates the sum of numerical values from specified lines in a file.
628
+ *
629
+ * @param filePath - Path of the file to read.
630
+ * @param lineNumbers - Optional specific line number(s) to include in the sum. If not provided, sums all lines.
631
+ * @returns Promise<number>. The sum of numerical values from the specified lines.
632
+ *
633
+ * Note: Decodes each line as a number using the 'decode' function. Non-numeric lines contribute 0 to the sum.
634
+ */
315
635
  export const sum = async (filePath, lineNumbers) => {
316
636
  let sum = 0;
317
637
  const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
318
638
  if (lineNumbers) {
319
- let lineCount = 0;
639
+ let linesCount = 0;
320
640
  let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
321
641
  for await (const line of rl) {
322
- lineCount++;
323
- if (!lineNumbersArray.has(lineCount))
642
+ linesCount++;
643
+ if (!lineNumbersArray.has(linesCount))
324
644
  continue;
325
645
  sum += +(decode(line, "number") ?? 0);
326
- lineNumbersArray.delete(lineCount);
646
+ lineNumbersArray.delete(linesCount);
327
647
  if (!lineNumbersArray.size)
328
648
  break;
329
649
  }
@@ -334,20 +654,29 @@ export const sum = async (filePath, lineNumbers) => {
334
654
  await fileHandle.close();
335
655
  return sum;
336
656
  };
657
+ /**
658
+ * Asynchronously finds the maximum numerical value from specified lines in a file.
659
+ *
660
+ * @param filePath - Path of the file to read.
661
+ * @param lineNumbers - Optional specific line number(s) to consider for finding the maximum value. If not provided, considers all lines.
662
+ * @returns Promise<number>. The maximum numerical value found in the specified lines.
663
+ *
664
+ * Note: Decodes each line as a number using the 'decode' function. Considers only numerical values for determining the maximum.
665
+ */
337
666
  export const max = async (filePath, lineNumbers) => {
338
667
  let max = 0;
339
668
  const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
340
669
  if (lineNumbers) {
341
- let lineCount = 0;
670
+ let linesCount = 0;
342
671
  let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
343
672
  for await (const line of rl) {
344
- lineCount++;
345
- if (!lineNumbersArray.has(lineCount))
673
+ linesCount++;
674
+ if (!lineNumbersArray.has(linesCount))
346
675
  continue;
347
676
  const lineContentNum = +(decode(line, "number") ?? 0);
348
677
  if (lineContentNum > max)
349
678
  max = lineContentNum;
350
- lineNumbersArray.delete(lineCount);
679
+ lineNumbersArray.delete(linesCount);
351
680
  if (!lineNumbersArray.size)
352
681
  break;
353
682
  }
@@ -361,20 +690,29 @@ export const max = async (filePath, lineNumbers) => {
361
690
  await fileHandle.close();
362
691
  return max;
363
692
  };
693
+ /**
694
+ * Asynchronously finds the minimum numerical value from specified lines in a file.
695
+ *
696
+ * @param filePath - Path of the file to read.
697
+ * @param lineNumbers - Optional specific line number(s) to consider for finding the minimum value. If not provided, considers all lines.
698
+ * @returns Promise<number>. The minimum numerical value found in the specified lines.
699
+ *
700
+ * Note: Decodes each line as a number using the 'decode' function. Considers only numerical values for determining the minimum.
701
+ */
364
702
  export const min = async (filePath, lineNumbers) => {
365
703
  let min = 0;
366
704
  const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
367
705
  if (lineNumbers) {
368
- let lineCount = 0;
706
+ let linesCount = 0;
369
707
  let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
370
708
  for await (const line of rl) {
371
- lineCount++;
372
- if (!lineNumbersArray.has(lineCount))
709
+ linesCount++;
710
+ if (!lineNumbersArray.has(linesCount))
373
711
  continue;
374
712
  const lineContentNum = +(decode(line, "number") ?? 0);
375
713
  if (lineContentNum < min)
376
714
  min = lineContentNum;
377
- lineNumbersArray.delete(lineCount);
715
+ lineNumbersArray.delete(linesCount);
378
716
  if (!lineNumbersArray.size)
379
717
  break;
380
718
  }
@@ -388,13 +726,23 @@ export const min = async (filePath, lineNumbers) => {
388
726
  await fileHandle.close();
389
727
  return min;
390
728
  };
729
+ /**
730
+ * Asynchronously sorts the lines in a file in the specified direction.
731
+ *
732
+ * @param filePath - Path of the file to be sorted.
733
+ * @param sortDirection - Direction for sorting: 1 or 'asc' for ascending, -1 or 'desc' for descending.
734
+ * @param lineNumbers - Optional specific line numbers to sort. If not provided, sorts all lines.
735
+ * @param _lineNumbersPerChunk - Optional parameter for handling large files, specifying the number of lines per chunk.
736
+ * @returns Promise<void>. Modifies the file by sorting specified lines.
737
+ *
738
+ * Note: The sorting is applied either to the entire file or to the specified lines. Large files are handled in chunks.
739
+ */
391
740
  export const sort = async (filePath, sortDirection, lineNumbers, _lineNumbersPerChunk = 100000) => { };
392
741
  export default class File {
393
742
  static get = get;
394
743
  static remove = remove;
395
744
  static search = search;
396
745
  static replace = replace;
397
- static count = count;
398
746
  static encode = encode;
399
747
  static decode = decode;
400
748
  static isExists = isExists;
@@ -402,4 +750,7 @@ export default class File {
402
750
  static min = min;
403
751
  static max = max;
404
752
  static append = append;
753
+ static count = count;
754
+ static write = write;
755
+ static read = read;
405
756
  }