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