inibase 1.0.0-rc.9 → 1.0.0-rc.90
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/LICENSE +1 -1
- package/README.md +410 -100
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +272 -0
- package/dist/file.d.ts +148 -0
- package/dist/file.js +597 -0
- package/dist/index.d.ts +192 -0
- package/dist/index.js +1350 -0
- package/dist/utils.d.ts +205 -0
- package/dist/utils.js +509 -0
- package/dist/utils.server.d.ts +83 -0
- package/dist/utils.server.js +248 -0
- package/package.json +66 -19
- package/file.ts +0 -501
- package/index.test.ts +0 -210
- package/index.ts +0 -1488
- package/tsconfig.json +0 -7
- package/utils.server.ts +0 -79
- package/utils.ts +0 -212
package/dist/file.js
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { access, appendFile, copyFile, constants as fsConstants, open, readFile, unlink, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { Transform } from "node:stream";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import { createGunzip, createGzip } from "node:zlib";
|
|
7
|
+
import Inison from "inison";
|
|
8
|
+
import { detectFieldType, isArrayOfObjects, isJSON, isNumber, isObject, } from "./utils.js";
|
|
9
|
+
import { compare, encodeID, exec, gunzip, gzip } from "./utils.server.js";
|
|
10
|
+
export const lock = async (folderPath, prefix) => {
|
|
11
|
+
let lockFile = null;
|
|
12
|
+
const lockFilePath = join(folderPath, `${prefix ?? ""}.locked`);
|
|
13
|
+
try {
|
|
14
|
+
lockFile = await open(lockFilePath, "wx");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
catch ({ message }) {
|
|
18
|
+
if (message.split(":")[0] === "EEXIST")
|
|
19
|
+
return await new Promise((resolve) => setTimeout(() => resolve(lock(folderPath, prefix)), 13));
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
await lockFile?.close();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
export const unlock = async (folderPath, prefix) => {
|
|
26
|
+
try {
|
|
27
|
+
await unlink(join(folderPath, `${prefix ?? ""}.locked`));
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
};
|
|
31
|
+
export const write = async (filePath, data) => {
|
|
32
|
+
await writeFile(filePath, filePath.endsWith(".gz") ? await gzip(data) : data);
|
|
33
|
+
};
|
|
34
|
+
export const read = async (filePath) => filePath.endsWith(".gz")
|
|
35
|
+
? (await gunzip(await readFile(filePath, "utf8"))).toString()
|
|
36
|
+
: await readFile(filePath, "utf8");
|
|
37
|
+
const _pipeline = async (filePath, rl, writeStream, transform) => {
|
|
38
|
+
if (filePath.endsWith(".gz"))
|
|
39
|
+
await pipeline(rl, transform, createGzip(), writeStream);
|
|
40
|
+
else
|
|
41
|
+
await pipeline(rl, transform, writeStream);
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Creates a readline interface for a given file handle.
|
|
45
|
+
*
|
|
46
|
+
* @param fileHandle - The file handle from which to create a read stream.
|
|
47
|
+
* @returns A readline.Interface instance configured with the provided file stream.
|
|
48
|
+
*/
|
|
49
|
+
const createReadLineInternface = (filePath, fileHandle) => createInterface({
|
|
50
|
+
input: filePath.endsWith(".gz")
|
|
51
|
+
? fileHandle.createReadStream().pipe(createGunzip())
|
|
52
|
+
: fileHandle.createReadStream(),
|
|
53
|
+
crlfDelay: Number.POSITIVE_INFINITY,
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* Checks if a file or directory exists at the specified path.
|
|
57
|
+
*
|
|
58
|
+
* @param path - The path to the file or directory.
|
|
59
|
+
* @returns A Promise that resolves to true if the file/directory exists, false otherwise.
|
|
60
|
+
*/
|
|
61
|
+
export const isExists = async (path) => {
|
|
62
|
+
try {
|
|
63
|
+
await access(path, fsConstants.R_OK | fsConstants.W_OK);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Secures input by encoding/escaping characters.
|
|
72
|
+
*
|
|
73
|
+
* @param input - String, number, boolean, or null.
|
|
74
|
+
* @returns Encoded string for true/false, special characters in strings, or original input.
|
|
75
|
+
*/
|
|
76
|
+
const secureString = (input) => {
|
|
77
|
+
if (["true", "false"].includes(String(input)))
|
|
78
|
+
return input ? 1 : 0;
|
|
79
|
+
if (typeof input !== "string") {
|
|
80
|
+
if (input === null || input === undefined)
|
|
81
|
+
return "";
|
|
82
|
+
return input;
|
|
83
|
+
}
|
|
84
|
+
let decodedInput = null;
|
|
85
|
+
try {
|
|
86
|
+
decodedInput = decodeURIComponent(input);
|
|
87
|
+
}
|
|
88
|
+
catch (_error) {
|
|
89
|
+
decodedInput = decodeURIComponent(input.replace(/%(?![0-9][0-9a-fA-F]+)/g, ""));
|
|
90
|
+
}
|
|
91
|
+
// Replace characters using a single regular expression.
|
|
92
|
+
return decodedInput.replace(/\\n/g, "\n").replace(/\n/g, "\\n");
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Encodes the input using 'secureString' and 'Inison.stringify' functions.
|
|
96
|
+
* If the input is an array, it is first secured and then joined into a string.
|
|
97
|
+
* If the input is a single value, it is directly secured.
|
|
98
|
+
*
|
|
99
|
+
* @param input - A value or array of values (string, number, boolean, null).
|
|
100
|
+
* @returns The secured and/or joined string.
|
|
101
|
+
*/
|
|
102
|
+
export const encode = (input) => Array.isArray(input)
|
|
103
|
+
? input.every((_input) => typeof _input === "string" && isJSON(_input))
|
|
104
|
+
? `[${input.join(",")}]`
|
|
105
|
+
: Inison.stringify(input)
|
|
106
|
+
: secureString(input);
|
|
107
|
+
/**
|
|
108
|
+
* Reverses the encoding done by 'secureString'. Replaces encoded characters with their original symbols.
|
|
109
|
+
*
|
|
110
|
+
* @param input - Encoded string.
|
|
111
|
+
* @returns Decoded string or null if input is empty.
|
|
112
|
+
*/
|
|
113
|
+
const unSecureString = (input) => {
|
|
114
|
+
if (isNumber(input))
|
|
115
|
+
return String(input).at(0) === "0" ? input : Number(input);
|
|
116
|
+
if (typeof input === "string")
|
|
117
|
+
return input.replace(/\\n/g, "\n") || null;
|
|
118
|
+
return null;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Decodes a value based on specified field types and optional secret key.
|
|
122
|
+
* Handles different data types and structures, including nested arrays.
|
|
123
|
+
*
|
|
124
|
+
* @param value - The value to be decoded, can be string, number, or array.
|
|
125
|
+
* @param fieldType - Optional type of the field to guide decoding (e.g., 'number', 'boolean').
|
|
126
|
+
* @param fieldChildrenType - Optional type for children elements, used for arrays.
|
|
127
|
+
* @param secretKey - Optional secret key for decoding, can be string or Buffer.
|
|
128
|
+
* @returns Decoded value, transformed according to the specified field type(s).
|
|
129
|
+
*/
|
|
130
|
+
const decodeHelper = (value, fieldType, fieldChildrenType, secretKey) => {
|
|
131
|
+
if (Array.isArray(value) && fieldType !== "array")
|
|
132
|
+
return value.map((v) => decodeHelper(v, fieldType, fieldChildrenType, secretKey));
|
|
133
|
+
switch (fieldType) {
|
|
134
|
+
case "number":
|
|
135
|
+
return isNumber(value) ? Number(value) : null;
|
|
136
|
+
case "boolean":
|
|
137
|
+
return typeof value === "string" ? value === "true" : Boolean(value);
|
|
138
|
+
case "array":
|
|
139
|
+
if (!Array.isArray(value))
|
|
140
|
+
return [value];
|
|
141
|
+
if (fieldChildrenType && !isArrayOfObjects(fieldChildrenType))
|
|
142
|
+
return fieldChildrenType
|
|
143
|
+
? value.map((v) => decode(v, Array.isArray(fieldChildrenType)
|
|
144
|
+
? detectFieldType(v, fieldChildrenType)
|
|
145
|
+
: fieldChildrenType, undefined, secretKey))
|
|
146
|
+
: value;
|
|
147
|
+
break;
|
|
148
|
+
case "table":
|
|
149
|
+
case "id":
|
|
150
|
+
return isNumber(value) && secretKey
|
|
151
|
+
? encodeID(value, secretKey)
|
|
152
|
+
: value;
|
|
153
|
+
default:
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Decodes the input based on the specified field type(s) and an optional secret key.
|
|
159
|
+
* Handles different formats of input, including strings, numbers, and their array representations.
|
|
160
|
+
*
|
|
161
|
+
* @param input - The input to be decoded, can be a string, number, or null.
|
|
162
|
+
* @param fieldType - Optional type of the field to guide decoding (e.g., 'number', 'boolean').
|
|
163
|
+
* @param fieldChildrenType - Optional type for child elements in array inputs.
|
|
164
|
+
* @param secretKey - Optional secret key for decoding, can be a string or Buffer.
|
|
165
|
+
* @returns Decoded value as a string, number, boolean, or array of these, or null if no fieldType or input is null/empty.
|
|
166
|
+
*/
|
|
167
|
+
export const decode = (input, fieldType, fieldChildrenType, secretKey) => {
|
|
168
|
+
if (!fieldType)
|
|
169
|
+
return null;
|
|
170
|
+
if (input === null || input === "")
|
|
171
|
+
return null;
|
|
172
|
+
// Detect the fieldType based on the input and the provided array of possible types.
|
|
173
|
+
if (Array.isArray(fieldType))
|
|
174
|
+
fieldType = detectFieldType(String(input), fieldType);
|
|
175
|
+
// Decode the input using the decodeHelper function.
|
|
176
|
+
return decodeHelper(typeof input === "string"
|
|
177
|
+
? isJSON(input)
|
|
178
|
+
? Inison.unstringify(input)
|
|
179
|
+
: unSecureString(input)
|
|
180
|
+
: input, fieldType, fieldChildrenType, secretKey);
|
|
181
|
+
};
|
|
182
|
+
export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, secretKey, readWholeFile = false) {
|
|
183
|
+
let fileHandle = null;
|
|
184
|
+
try {
|
|
185
|
+
fileHandle = await open(filePath, "r");
|
|
186
|
+
const rl = createReadLineInternface(filePath, fileHandle), lines = {};
|
|
187
|
+
let linesCount = 0;
|
|
188
|
+
if (!lineNumbers) {
|
|
189
|
+
for await (const line of rl) {
|
|
190
|
+
linesCount++;
|
|
191
|
+
lines[linesCount] = decode(line, fieldType, fieldChildrenType, secretKey);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (lineNumbers == -1) {
|
|
195
|
+
const command = filePath.endsWith(".gz")
|
|
196
|
+
? `zcat ${filePath} | sed -n '$p'`
|
|
197
|
+
: `sed -n '$p' ${filePath}`, foundedLine = (await exec(command)).stdout.trim();
|
|
198
|
+
if (foundedLine)
|
|
199
|
+
lines[linesCount] = decode(foundedLine, fieldType, fieldChildrenType, secretKey);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
lineNumbers = Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers];
|
|
203
|
+
if (lineNumbers.some(Number.isNaN))
|
|
204
|
+
throw new Error("UNVALID_LINE_NUMBERS");
|
|
205
|
+
if (readWholeFile) {
|
|
206
|
+
const lineNumbersArray = new Set(lineNumbers);
|
|
207
|
+
for await (const line of rl) {
|
|
208
|
+
linesCount++;
|
|
209
|
+
if (!lineNumbersArray.has(linesCount))
|
|
210
|
+
continue;
|
|
211
|
+
lines[linesCount] = decode(line, fieldType, fieldChildrenType, secretKey);
|
|
212
|
+
lineNumbersArray.delete(linesCount);
|
|
213
|
+
}
|
|
214
|
+
return [lines, linesCount];
|
|
215
|
+
}
|
|
216
|
+
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.trim().split("\n");
|
|
219
|
+
let index = 0;
|
|
220
|
+
for (const line of foundedLines) {
|
|
221
|
+
lines[lineNumbers[index]] = decode(line, fieldType, fieldChildrenType, secretKey);
|
|
222
|
+
index++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return lines;
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
// Ensure that file handles are closed, even if an error occurred
|
|
229
|
+
await fileHandle?.close();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Asynchronously replaces specific lines in a file based on the provided replacements map or string.
|
|
234
|
+
*
|
|
235
|
+
* @param filePath - Path of the file to modify.
|
|
236
|
+
* @param replacements - Map of line numbers to replacement values, or a single replacement value for all lines.
|
|
237
|
+
* Can be a string, number, boolean, null, array of these types, or a Record/Map of line numbers to these types.
|
|
238
|
+
* @returns Promise<string[]>
|
|
239
|
+
*
|
|
240
|
+
* Note: If the file doesn't exist and replacements is an object, it creates a new file with the specified replacements.
|
|
241
|
+
*/
|
|
242
|
+
export const replace = async (filePath, replacements) => {
|
|
243
|
+
const fileTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/$1");
|
|
244
|
+
if (await isExists(filePath)) {
|
|
245
|
+
let fileHandle = null;
|
|
246
|
+
let fileTempHandle = null;
|
|
247
|
+
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
|
+
}));
|
|
263
|
+
return [fileTempPath, filePath];
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
// Ensure that file handles are closed, even if an error occurred
|
|
267
|
+
await fileHandle?.close();
|
|
268
|
+
await fileTempHandle?.close();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
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
|
+
return [];
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* Asynchronously appends data to the end of a file.
|
|
288
|
+
*
|
|
289
|
+
* @param filePath - Path of the file to append to.
|
|
290
|
+
* @param data - Data to append. Can be a string, number, or an array of strings/numbers.
|
|
291
|
+
* @returns Promise<string[]>. Modifies the file by appending data.
|
|
292
|
+
*
|
|
293
|
+
*/
|
|
294
|
+
export const append = async (filePath, data) => {
|
|
295
|
+
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}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else
|
|
308
|
+
await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
|
|
309
|
+
return [fileTempPath, filePath];
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* Asynchronously prepends data to the beginning of a file.
|
|
313
|
+
*
|
|
314
|
+
* @param filePath - Path of the file to append to.
|
|
315
|
+
* @param data - Data to append. Can be a string, number, or an array of strings/numbers.
|
|
316
|
+
* @returns Promise<string[]>. Modifies the file by appending data.
|
|
317
|
+
*
|
|
318
|
+
*/
|
|
319
|
+
export const prepend = async (filePath, data) => {
|
|
320
|
+
const fileTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/$1");
|
|
321
|
+
if (await isExists(filePath)) {
|
|
322
|
+
if (!filePath.endsWith(".gz")) {
|
|
323
|
+
let fileHandle = null;
|
|
324
|
+
let fileTempHandle = null;
|
|
325
|
+
try {
|
|
326
|
+
fileHandle = await open(filePath, "r");
|
|
327
|
+
fileTempHandle = await open(fileTempPath, "w");
|
|
328
|
+
const rl = createReadLineInternface(filePath, fileHandle);
|
|
329
|
+
let isAppended = false;
|
|
330
|
+
await _pipeline(filePath, rl, fileTempHandle.createWriteStream(), new Transform({
|
|
331
|
+
transform(line, _, callback) {
|
|
332
|
+
if (!isAppended) {
|
|
333
|
+
isAppended = true;
|
|
334
|
+
return callback(null, `${Array.isArray(data) ? data.join("\n") : data}\n${line.length ? `${line}\n` : ""}`);
|
|
335
|
+
}
|
|
336
|
+
return callback(null, `${line}\n`);
|
|
337
|
+
},
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
// Ensure that file handles are closed, even if an error occurred
|
|
342
|
+
await fileHandle?.close();
|
|
343
|
+
await fileTempHandle?.close();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
const fileChildTempPath = filePath.replace(/([^/]+)\/?$/, ".tmp/tmp_$1");
|
|
348
|
+
try {
|
|
349
|
+
await write(fileChildTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
|
|
350
|
+
await exec(`cat ${fileChildTempPath} ${filePath} > ${fileTempPath}`);
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
await unlink(fileChildTempPath);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else
|
|
358
|
+
await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
|
|
359
|
+
return [fileTempPath, filePath];
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* Asynchronously removes specified lines from a file.
|
|
363
|
+
*
|
|
364
|
+
* @param filePath - Path of the file from which lines are to be removed.
|
|
365
|
+
* @param linesToDelete - A single line number or an array of line numbers to be deleted.
|
|
366
|
+
* @returns Promise<string[]>. Modifies the file by removing specified lines.
|
|
367
|
+
*
|
|
368
|
+
* Note: Creates a temporary file during the process and replaces the original file with it after removing lines.
|
|
369
|
+
*/
|
|
370
|
+
export const remove = async (filePath, linesToDelete) => {
|
|
371
|
+
linesToDelete = Array.isArray(linesToDelete)
|
|
372
|
+
? linesToDelete.map(Number)
|
|
373
|
+
: [Number(linesToDelete)];
|
|
374
|
+
if (linesToDelete.some(Number.isNaN))
|
|
375
|
+
throw new Error("UNVALID_LINE_NUMBERS");
|
|
376
|
+
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];
|
|
382
|
+
};
|
|
383
|
+
/**
|
|
384
|
+
* Asynchronously searches a file for lines matching specified criteria, using comparison and logical operators.
|
|
385
|
+
*
|
|
386
|
+
* @param filePath - Path of the file to search.
|
|
387
|
+
* @param operator - Comparison operator(s) for evaluation (e.g., '=', '!=', '>', '<').
|
|
388
|
+
* @param comparedAtValue - Value(s) to compare each line against.
|
|
389
|
+
* @param logicalOperator - Optional logical operator ('and' or 'or') for combining multiple comparisons.
|
|
390
|
+
* @param fieldType - Optional type of the field to guide comparison.
|
|
391
|
+
* @param fieldChildrenType - Optional type for child elements in array inputs.
|
|
392
|
+
* @param limit - Optional limit on the number of results to return.
|
|
393
|
+
* @param offset - Optional offset to start returning results from.
|
|
394
|
+
* @param readWholeFile - Flag to indicate whether to continue reading the file after reaching the limit.
|
|
395
|
+
* @param secretKey - Optional secret key for decoding, can be a string or Buffer.
|
|
396
|
+
* @returns Promise resolving to a tuple:
|
|
397
|
+
* 1. Record of line numbers and their content that match the criteria or null if none.
|
|
398
|
+
* 2. The count of found items or processed items based on the 'readWholeFile' flag.
|
|
399
|
+
*
|
|
400
|
+
* Note: Decodes each line for comparison and can handle complex queries with multiple conditions.
|
|
401
|
+
*/
|
|
402
|
+
export const search = async (filePath, operator, comparedAtValue, logicalOperator, fieldType, fieldChildrenType, limit, offset, readWholeFile, secretKey) => {
|
|
403
|
+
// Initialize a Map to store the matching lines with their line numbers.
|
|
404
|
+
const matchingLines = {};
|
|
405
|
+
// Initialize counters for line number, found items, and processed items.
|
|
406
|
+
let linesCount = 0;
|
|
407
|
+
const linesNumbers = new Set();
|
|
408
|
+
let fileHandle = null;
|
|
409
|
+
try {
|
|
410
|
+
// Open the file for reading.
|
|
411
|
+
fileHandle = await open(filePath, "r");
|
|
412
|
+
// Create a Readline interface to read the file line by line.
|
|
413
|
+
const rl = createReadLineInternface(filePath, fileHandle);
|
|
414
|
+
// Iterate through each line in the file.
|
|
415
|
+
for await (const line of rl) {
|
|
416
|
+
// Increment the line count for each line.
|
|
417
|
+
linesCount++;
|
|
418
|
+
// Decode the line for comparison.
|
|
419
|
+
const decodedLine = decode(line, fieldType, fieldChildrenType, secretKey);
|
|
420
|
+
// 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));
|
|
428
|
+
// If the line meets the conditions, process it.
|
|
429
|
+
if (meetsConditions) {
|
|
430
|
+
// Increment the found items counter.
|
|
431
|
+
linesNumbers.add(linesCount);
|
|
432
|
+
// Check if the line should be skipped based on the offset.
|
|
433
|
+
if (offset && linesNumbers.size < offset)
|
|
434
|
+
continue;
|
|
435
|
+
// Check if the limit has been reached.
|
|
436
|
+
if (limit && linesNumbers.size > limit + (offset ?? 0)) {
|
|
437
|
+
if (readWholeFile)
|
|
438
|
+
continue;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
// Store the decoded line in the result object.
|
|
442
|
+
matchingLines[linesCount] = decodedLine;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Convert the Map to an object using Object.fromEntries and return the result.
|
|
446
|
+
return linesNumbers.size
|
|
447
|
+
? [matchingLines, linesNumbers.size, linesNumbers]
|
|
448
|
+
: [null, 0, null];
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
// Close the file handle in the finally block to ensure it is closed even if an error occurs.
|
|
452
|
+
await fileHandle?.close();
|
|
453
|
+
}
|
|
454
|
+
};
|
|
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.trim());
|
|
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
|
+
/**
|
|
481
|
+
* Asynchronously calculates the sum of numerical values from specified lines in a file.
|
|
482
|
+
*
|
|
483
|
+
* @param filePath - Path of the file to read.
|
|
484
|
+
* @param lineNumbers - Optional specific line number(s) to include in the sum. If not provided, sums all lines.
|
|
485
|
+
* @returns Promise<number>. The sum of numerical values from the specified lines.
|
|
486
|
+
*
|
|
487
|
+
* Note: Decodes each line as a number using the 'decode' function. Non-numeric lines contribute 0 to the sum.
|
|
488
|
+
*/
|
|
489
|
+
export const sum = async (filePath, lineNumbers) => {
|
|
490
|
+
let sum = 0, fileHandle = null;
|
|
491
|
+
try {
|
|
492
|
+
fileHandle = await open(filePath, "r");
|
|
493
|
+
const rl = createReadLineInternface(filePath, fileHandle);
|
|
494
|
+
if (lineNumbers) {
|
|
495
|
+
let linesCount = 0;
|
|
496
|
+
const lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
|
|
497
|
+
for await (const line of rl) {
|
|
498
|
+
linesCount++;
|
|
499
|
+
if (!lineNumbersArray.has(linesCount))
|
|
500
|
+
continue;
|
|
501
|
+
sum += +(decode(line, "number") ?? 0);
|
|
502
|
+
lineNumbersArray.delete(linesCount);
|
|
503
|
+
if (!lineNumbersArray.size)
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else
|
|
508
|
+
for await (const line of rl)
|
|
509
|
+
sum += +(decode(line, "number") ?? 0);
|
|
510
|
+
return sum;
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
await fileHandle?.close();
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
/**
|
|
517
|
+
* Asynchronously finds the maximum numerical value from specified lines in a file.
|
|
518
|
+
*
|
|
519
|
+
* @param filePath - Path of the file to read.
|
|
520
|
+
* @param lineNumbers - Optional specific line number(s) to consider for finding the maximum value. If not provided, considers all lines.
|
|
521
|
+
* @returns Promise<number>. The maximum numerical value found in the specified lines.
|
|
522
|
+
*
|
|
523
|
+
* Note: Decodes each line as a number using the 'decode' function. Considers only numerical values for determining the maximum.
|
|
524
|
+
*/
|
|
525
|
+
export const max = async (filePath, lineNumbers) => {
|
|
526
|
+
let max = 0, fileHandle = null, rl = null;
|
|
527
|
+
try {
|
|
528
|
+
fileHandle = await open(filePath, "r");
|
|
529
|
+
rl = createReadLineInternface(filePath, fileHandle);
|
|
530
|
+
if (lineNumbers) {
|
|
531
|
+
let linesCount = 0;
|
|
532
|
+
const lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
|
|
533
|
+
for await (const line of rl) {
|
|
534
|
+
linesCount++;
|
|
535
|
+
if (!lineNumbersArray.has(linesCount))
|
|
536
|
+
continue;
|
|
537
|
+
const lineContentNum = +(decode(line, "number") ?? 0);
|
|
538
|
+
if (lineContentNum > max)
|
|
539
|
+
max = lineContentNum;
|
|
540
|
+
lineNumbersArray.delete(linesCount);
|
|
541
|
+
if (!lineNumbersArray.size)
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else
|
|
546
|
+
for await (const line of rl) {
|
|
547
|
+
const lineContentNum = +(decode(line, "number") ?? 0);
|
|
548
|
+
if (lineContentNum > max)
|
|
549
|
+
max = lineContentNum;
|
|
550
|
+
}
|
|
551
|
+
return max;
|
|
552
|
+
}
|
|
553
|
+
finally {
|
|
554
|
+
await fileHandle?.close();
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
/**
|
|
558
|
+
* Asynchronously finds the minimum numerical value from specified lines in a file.
|
|
559
|
+
*
|
|
560
|
+
* @param filePath - Path of the file to read.
|
|
561
|
+
* @param lineNumbers - Optional specific line number(s) to consider for finding the minimum value. If not provided, considers all lines.
|
|
562
|
+
* @returns Promise<number>. The minimum numerical value found in the specified lines.
|
|
563
|
+
*
|
|
564
|
+
* Note: Decodes each line as a number using the 'decode' function. Considers only numerical values for determining the minimum.
|
|
565
|
+
*/
|
|
566
|
+
export const min = async (filePath, lineNumbers) => {
|
|
567
|
+
let min = 0, fileHandle = null;
|
|
568
|
+
try {
|
|
569
|
+
fileHandle = await open(filePath, "r");
|
|
570
|
+
const rl = createReadLineInternface(filePath, fileHandle);
|
|
571
|
+
if (lineNumbers) {
|
|
572
|
+
let linesCount = 0;
|
|
573
|
+
const lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
|
|
574
|
+
for await (const line of rl) {
|
|
575
|
+
linesCount++;
|
|
576
|
+
if (!lineNumbersArray.has(linesCount))
|
|
577
|
+
continue;
|
|
578
|
+
const lineContentNum = +(decode(line, "number") ?? 0);
|
|
579
|
+
if (lineContentNum < min)
|
|
580
|
+
min = lineContentNum;
|
|
581
|
+
lineNumbersArray.delete(linesCount);
|
|
582
|
+
if (!lineNumbersArray.size)
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else
|
|
587
|
+
for await (const line of rl) {
|
|
588
|
+
const lineContentNum = +(decode(line, "number") ?? 0);
|
|
589
|
+
if (lineContentNum < min)
|
|
590
|
+
min = lineContentNum;
|
|
591
|
+
}
|
|
592
|
+
return min;
|
|
593
|
+
}
|
|
594
|
+
finally {
|
|
595
|
+
await fileHandle?.close();
|
|
596
|
+
}
|
|
597
|
+
};
|