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