inibase 1.0.0-rc.10 → 1.0.0-rc.12
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/.env.example +1 -0
- package/README.md +50 -0
- package/file.ts +246 -73
- package/index.test.ts +45 -3
- package/index.ts +442 -349
- package/package.json +2 -1
- package/utils.ts +174 -20
- package/utils.server.ts +0 -79
package/.env.example
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
INIBASE_SECRET=
|
package/README.md
CHANGED
|
@@ -385,6 +385,56 @@ await db.put("user", { isActive: false });
|
|
|
385
385
|
|
|
386
386
|
</details>
|
|
387
387
|
|
|
388
|
+
<details>
|
|
389
|
+
<summary>SUM</summary>
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
import Inibase from "inibase";
|
|
393
|
+
const db = new Inibase("/database_name");
|
|
394
|
+
|
|
395
|
+
// get the sum of column "age" in "user" table
|
|
396
|
+
await db.sum("user", "age");
|
|
397
|
+
|
|
398
|
+
// get the sum of column "age" by criteria (where "isActive" is equal to "false") in "user" table
|
|
399
|
+
await db.sum("user", ["age", ...], { isActive: false });
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
</details>
|
|
403
|
+
|
|
404
|
+
<details>
|
|
405
|
+
<summary>MAX</summary>
|
|
406
|
+
|
|
407
|
+
```js
|
|
408
|
+
import Inibase from "inibase";
|
|
409
|
+
const db = new Inibase("/database_name");
|
|
410
|
+
|
|
411
|
+
// get the biggest number of column "age" in "user" table
|
|
412
|
+
await db.max("user", "age");
|
|
413
|
+
|
|
414
|
+
// get the biggest number of column "age" by criteria (where "isActive" is equal to "false") in "user" table
|
|
415
|
+
await db.max("user", ["age", ...], { isActive: false });
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
</details>
|
|
419
|
+
|
|
420
|
+
<details>
|
|
421
|
+
<summary>MIN</summary>
|
|
422
|
+
|
|
423
|
+
```js
|
|
424
|
+
import Inibase from "inibase";
|
|
425
|
+
const db = new Inibase("/database_name");
|
|
426
|
+
|
|
427
|
+
// get the smallest number of column "age" in "user" table
|
|
428
|
+
await db.min("user", "age");
|
|
429
|
+
|
|
430
|
+
// get the smallest number of column "age" by criteria (where "isActive" is equal to "false") in "user" table
|
|
431
|
+
await db.min("user", ["age", ...], { isActive: false });
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
</details>
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
|
|
388
438
|
## Roadmap
|
|
389
439
|
|
|
390
440
|
- [x] Actions:
|
package/file.ts
CHANGED
|
@@ -3,8 +3,13 @@ import { open, unlink, rename, stat } from "node:fs/promises";
|
|
|
3
3
|
import { Interface, createInterface } from "node:readline";
|
|
4
4
|
import { parse } from "node:path";
|
|
5
5
|
import { ComparisonOperator, FieldType } from ".";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
detectFieldType,
|
|
8
|
+
isArrayOfArrays,
|
|
9
|
+
isNumber,
|
|
10
|
+
encodeID,
|
|
11
|
+
comparePassword,
|
|
12
|
+
} from "./utils";
|
|
8
13
|
|
|
9
14
|
const doesSupportReadLines = () => {
|
|
10
15
|
const [major, minor, patch] = process.versions.node.split(".").map(Number);
|
|
@@ -20,6 +25,8 @@ export const isExists = async (path: string) => {
|
|
|
20
25
|
}
|
|
21
26
|
};
|
|
22
27
|
|
|
28
|
+
const delimiters = [",", "|", "&", "$", "#", "@", "^", "%", ":", "!", ";"];
|
|
29
|
+
|
|
23
30
|
export const encode = (
|
|
24
31
|
input:
|
|
25
32
|
| string
|
|
@@ -30,23 +37,42 @@ export const encode = (
|
|
|
30
37
|
secretKey?: string | Buffer
|
|
31
38
|
) => {
|
|
32
39
|
const secureString = (input: string | number | boolean | null) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
if (["true", "false"].includes(String(input))) return input ? 1 : 0;
|
|
41
|
+
return typeof input === "string"
|
|
42
|
+
? decodeURIComponent(input)
|
|
43
|
+
.replaceAll("<", "<")
|
|
44
|
+
.replaceAll(">", ">")
|
|
45
|
+
.replaceAll(",", "%2C")
|
|
46
|
+
.replaceAll("|", "%7C")
|
|
47
|
+
.replaceAll("&", "%26")
|
|
48
|
+
.replaceAll("$", "%24")
|
|
49
|
+
.replaceAll("#", "%23")
|
|
50
|
+
.replaceAll("@", "%40")
|
|
51
|
+
.replaceAll("^", "%5E")
|
|
52
|
+
.replaceAll("%", "%25")
|
|
53
|
+
.replaceAll(":", "%3A")
|
|
54
|
+
.replaceAll("!", "%21")
|
|
55
|
+
.replaceAll(";", "%3B")
|
|
56
|
+
.replaceAll("\n", "\\n")
|
|
57
|
+
.replaceAll("\r", "\\r")
|
|
58
|
+
: input;
|
|
59
|
+
},
|
|
60
|
+
secureArray = (arr_str: any[] | any): any[] | any =>
|
|
61
|
+
Array.isArray(arr_str) ? arr_str.map(secureArray) : secureString(arr_str),
|
|
62
|
+
joinMultidimensionalArray = (
|
|
63
|
+
arr: any[] | any[][],
|
|
64
|
+
delimiter_index = 0
|
|
65
|
+
): string => {
|
|
66
|
+
delimiter_index++;
|
|
67
|
+
if (isArrayOfArrays(arr))
|
|
68
|
+
arr = arr.map((ar: any[]) =>
|
|
69
|
+
joinMultidimensionalArray(ar, delimiter_index)
|
|
70
|
+
);
|
|
71
|
+
delimiter_index--;
|
|
72
|
+
return arr.join(delimiters[delimiter_index]);
|
|
73
|
+
};
|
|
44
74
|
return Array.isArray(input)
|
|
45
|
-
?
|
|
46
|
-
? (input as any[])
|
|
47
|
-
.map((_input) => _input.map(secureString).join(","))
|
|
48
|
-
.join("|")
|
|
49
|
-
: input.map(secureString).join(",")
|
|
75
|
+
? joinMultidimensionalArray(secureArray(input))
|
|
50
76
|
: secureString(input);
|
|
51
77
|
};
|
|
52
78
|
|
|
@@ -58,65 +84,87 @@ export const decode = (
|
|
|
58
84
|
): string | number | boolean | null | (string | number | null | boolean)[] => {
|
|
59
85
|
if (!fieldType) return null;
|
|
60
86
|
const unSecureString = (input: string) =>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
decodeURIComponent(input)
|
|
88
|
+
.replaceAll("<", "<")
|
|
89
|
+
.replaceAll(">", ">")
|
|
90
|
+
.replaceAll("%2C", ",")
|
|
91
|
+
.replaceAll("%7C", "|")
|
|
92
|
+
.replaceAll("%26", "&")
|
|
93
|
+
.replaceAll("%24", "$")
|
|
94
|
+
.replaceAll("%23", "#")
|
|
95
|
+
.replaceAll("%40", "@")
|
|
96
|
+
.replaceAll("%5E", "^")
|
|
97
|
+
.replaceAll("%25", "%")
|
|
98
|
+
.replaceAll("%3A", ":")
|
|
99
|
+
.replaceAll("%21", "!")
|
|
100
|
+
.replaceAll("%3B", ";")
|
|
101
|
+
.replaceAll("\\n", "\n")
|
|
102
|
+
.replaceAll("\\r", "\r") || null,
|
|
103
|
+
unSecureArray = (arr_str: any[] | any): any[] | any =>
|
|
104
|
+
Array.isArray(arr_str)
|
|
105
|
+
? arr_str.map(unSecureArray)
|
|
106
|
+
: unSecureString(arr_str),
|
|
107
|
+
reverseJoinMultidimensionalArray = (
|
|
108
|
+
joinedString: string | any[] | any[][]
|
|
109
|
+
): any | any[] | any[][] => {
|
|
110
|
+
const reverseJoinMultidimensionalArrayHelper = (
|
|
111
|
+
arr: any | any[] | any[][],
|
|
112
|
+
delimiter: string
|
|
113
|
+
) =>
|
|
114
|
+
Array.isArray(arr)
|
|
115
|
+
? arr.map((ar: any) =>
|
|
116
|
+
reverseJoinMultidimensionalArrayHelper(ar, delimiter)
|
|
117
|
+
)
|
|
118
|
+
: arr.split(delimiter);
|
|
119
|
+
|
|
120
|
+
const availableDelimiters = delimiters.filter((delimiter) =>
|
|
121
|
+
joinedString.includes(delimiter)
|
|
122
|
+
);
|
|
123
|
+
for (const delimiter of availableDelimiters) {
|
|
124
|
+
joinedString = Array.isArray(joinedString)
|
|
125
|
+
? reverseJoinMultidimensionalArrayHelper(joinedString, delimiter)
|
|
126
|
+
: joinedString.split(delimiter);
|
|
127
|
+
}
|
|
128
|
+
return joinedString;
|
|
129
|
+
},
|
|
130
|
+
decodeHelper = (value: string | number | any[]) => {
|
|
131
|
+
if (Array.isArray(value) && fieldType !== "array")
|
|
132
|
+
return value.map(decodeHelper);
|
|
133
|
+
switch (fieldType as FieldType) {
|
|
134
|
+
case "table":
|
|
135
|
+
case "number":
|
|
136
|
+
return isNumber(value) ? Number(value) : null;
|
|
137
|
+
case "boolean":
|
|
138
|
+
return typeof value === "string" ? value === "true" : Boolean(value);
|
|
139
|
+
case "array":
|
|
140
|
+
if (!Array.isArray(value)) return [value];
|
|
141
|
+
|
|
142
|
+
if (fieldChildrenType)
|
|
143
|
+
return value.map(
|
|
144
|
+
(v) =>
|
|
145
|
+
decode(
|
|
146
|
+
v,
|
|
147
|
+
Array.isArray(fieldChildrenType)
|
|
148
|
+
? detectFieldType(v, fieldChildrenType)
|
|
149
|
+
: fieldChildrenType,
|
|
150
|
+
undefined,
|
|
151
|
+
secretKey
|
|
152
|
+
) as string | number | boolean | null
|
|
153
|
+
);
|
|
154
|
+
else return value;
|
|
155
|
+
case "id":
|
|
156
|
+
return isNumber(value) ? encodeID(value as number, secretKey) : value;
|
|
157
|
+
default:
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
68
161
|
if (input === null || input === "") return null;
|
|
69
162
|
if (Array.isArray(fieldType))
|
|
70
163
|
fieldType = detectFieldType(String(input), fieldType);
|
|
71
|
-
const decodeHelper = (value: string | number | any[]) => {
|
|
72
|
-
if (Array.isArray(value) && fieldType !== "array")
|
|
73
|
-
return value.map(decodeHelper);
|
|
74
|
-
switch (fieldType as FieldType) {
|
|
75
|
-
case "table":
|
|
76
|
-
case "number":
|
|
77
|
-
return isNumber(value) ? Number(value) : null;
|
|
78
|
-
case "boolean":
|
|
79
|
-
return typeof value === "string" ? value === "true" : Boolean(value);
|
|
80
|
-
case "array":
|
|
81
|
-
if (!Array.isArray(value)) return [value];
|
|
82
|
-
|
|
83
|
-
if (fieldChildrenType)
|
|
84
|
-
return value.map(
|
|
85
|
-
(v) =>
|
|
86
|
-
decode(
|
|
87
|
-
v,
|
|
88
|
-
Array.isArray(fieldChildrenType)
|
|
89
|
-
? detectFieldType(v, fieldChildrenType)
|
|
90
|
-
: fieldChildrenType,
|
|
91
|
-
undefined,
|
|
92
|
-
secretKey
|
|
93
|
-
) as string | number | boolean | null
|
|
94
|
-
);
|
|
95
|
-
else return value;
|
|
96
|
-
case "id":
|
|
97
|
-
return isNumber(value) ? encodeID(value as number, secretKey) : value;
|
|
98
|
-
default:
|
|
99
|
-
return value;
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
164
|
return decodeHelper(
|
|
103
165
|
typeof input === "string"
|
|
104
166
|
? input.includes(",")
|
|
105
|
-
? input
|
|
106
|
-
? input
|
|
107
|
-
.split("|")
|
|
108
|
-
.map((_input) => _input.split(",").map(unSecureString))
|
|
109
|
-
: input.split(",").map(unSecureString)
|
|
110
|
-
: input.includes("|")
|
|
111
|
-
? input
|
|
112
|
-
.split("|")
|
|
113
|
-
.map((_input) => [
|
|
114
|
-
_input
|
|
115
|
-
? fieldType === "array"
|
|
116
|
-
? [unSecureString(_input)]
|
|
117
|
-
: unSecureString(_input)
|
|
118
|
-
: null,
|
|
119
|
-
])
|
|
167
|
+
? unSecureArray(reverseJoinMultidimensionalArray(input))
|
|
120
168
|
: unSecureString(input)
|
|
121
169
|
: input
|
|
122
170
|
);
|
|
@@ -180,9 +228,8 @@ export const get = async (
|
|
|
180
228
|
for await (const line of rl) {
|
|
181
229
|
lineCount++;
|
|
182
230
|
if (!lineNumbersArray.includes(lineCount)) continue;
|
|
183
|
-
const indexOfLineCount = lineNumbersArray.indexOf(lineCount);
|
|
184
231
|
lines[lineCount] = decode(line, fieldType, fieldChildrenType, secretKey);
|
|
185
|
-
lineNumbersArray[
|
|
232
|
+
lineNumbersArray[lineNumbersArray.indexOf(lineCount)] = 0;
|
|
186
233
|
if (!lineNumbersArray.filter((lineN) => lineN !== 0).length) break;
|
|
187
234
|
}
|
|
188
235
|
}
|
|
@@ -489,6 +536,129 @@ export const search = async (
|
|
|
489
536
|
} else return [null, 0];
|
|
490
537
|
};
|
|
491
538
|
|
|
539
|
+
export const sum = async (
|
|
540
|
+
filePath: string,
|
|
541
|
+
lineNumbers?: number | number[]
|
|
542
|
+
): Promise<number> => {
|
|
543
|
+
let rl: Interface;
|
|
544
|
+
if (doesSupportReadLines()) rl = (await open(filePath)).readLines();
|
|
545
|
+
else
|
|
546
|
+
rl = createInterface({
|
|
547
|
+
input: createReadStream(filePath),
|
|
548
|
+
crlfDelay: Infinity,
|
|
549
|
+
});
|
|
550
|
+
let sum = 0;
|
|
551
|
+
|
|
552
|
+
if (lineNumbers) {
|
|
553
|
+
let lineCount = 0;
|
|
554
|
+
|
|
555
|
+
let lineNumbersArray = [
|
|
556
|
+
...(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]),
|
|
557
|
+
];
|
|
558
|
+
for await (const line of rl) {
|
|
559
|
+
lineCount++;
|
|
560
|
+
if (!lineNumbersArray.includes(lineCount)) continue;
|
|
561
|
+
sum += +decode(line, "number");
|
|
562
|
+
lineNumbersArray[lineNumbersArray.indexOf(lineCount)] = 0;
|
|
563
|
+
if (!lineNumbersArray.filter((lineN) => lineN !== 0).length) break;
|
|
564
|
+
}
|
|
565
|
+
} else for await (const line of rl) sum += +decode(line, "number");
|
|
566
|
+
|
|
567
|
+
return sum;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
export const max = async (
|
|
571
|
+
filePath: string,
|
|
572
|
+
lineNumbers?: number | number[]
|
|
573
|
+
): Promise<number> => {
|
|
574
|
+
let rl: Interface;
|
|
575
|
+
if (doesSupportReadLines()) rl = (await open(filePath)).readLines();
|
|
576
|
+
else
|
|
577
|
+
rl = createInterface({
|
|
578
|
+
input: createReadStream(filePath),
|
|
579
|
+
crlfDelay: Infinity,
|
|
580
|
+
});
|
|
581
|
+
let max = 0;
|
|
582
|
+
|
|
583
|
+
if (lineNumbers) {
|
|
584
|
+
let lineCount = 0;
|
|
585
|
+
|
|
586
|
+
let lineNumbersArray = [
|
|
587
|
+
...(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]),
|
|
588
|
+
];
|
|
589
|
+
for await (const line of rl) {
|
|
590
|
+
lineCount++;
|
|
591
|
+
if (!lineNumbersArray.includes(lineCount)) continue;
|
|
592
|
+
const lineContentNum = +decode(line, "number");
|
|
593
|
+
if (lineContentNum > max) max = lineContentNum;
|
|
594
|
+
lineNumbersArray[lineNumbersArray.indexOf(lineCount)] = 0;
|
|
595
|
+
if (!lineNumbersArray.filter((lineN) => lineN !== 0).length) break;
|
|
596
|
+
}
|
|
597
|
+
} else
|
|
598
|
+
for await (const line of rl) {
|
|
599
|
+
const lineContentNum = +decode(line, "number");
|
|
600
|
+
if (lineContentNum > max) max = lineContentNum;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return max;
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
export const min = async (
|
|
607
|
+
filePath: string,
|
|
608
|
+
lineNumbers?: number | number[]
|
|
609
|
+
): Promise<number> => {
|
|
610
|
+
let rl: Interface;
|
|
611
|
+
if (doesSupportReadLines()) rl = (await open(filePath)).readLines();
|
|
612
|
+
else
|
|
613
|
+
rl = createInterface({
|
|
614
|
+
input: createReadStream(filePath),
|
|
615
|
+
crlfDelay: Infinity,
|
|
616
|
+
});
|
|
617
|
+
let min = 0;
|
|
618
|
+
|
|
619
|
+
if (lineNumbers) {
|
|
620
|
+
let lineCount = 0;
|
|
621
|
+
|
|
622
|
+
let lineNumbersArray = [
|
|
623
|
+
...(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]),
|
|
624
|
+
];
|
|
625
|
+
for await (const line of rl) {
|
|
626
|
+
lineCount++;
|
|
627
|
+
if (!lineNumbersArray.includes(lineCount)) continue;
|
|
628
|
+
const lineContentNum = +decode(line, "number");
|
|
629
|
+
if (lineContentNum < min) min = lineContentNum;
|
|
630
|
+
lineNumbersArray[lineNumbersArray.indexOf(lineCount)] = 0;
|
|
631
|
+
if (!lineNumbersArray.filter((lineN) => lineN !== 0).length) break;
|
|
632
|
+
}
|
|
633
|
+
} else
|
|
634
|
+
for await (const line of rl) {
|
|
635
|
+
const lineContentNum = +decode(line, "number");
|
|
636
|
+
if (lineContentNum < min) min = lineContentNum;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return min;
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
export const sort = async (
|
|
643
|
+
filePath: string,
|
|
644
|
+
sortDirection: 1 | -1 | "asc" | "desc",
|
|
645
|
+
lineNumbers?: number | number[],
|
|
646
|
+
_lineNumbersPerChunk: number = 100000
|
|
647
|
+
): Promise<void> => {
|
|
648
|
+
let rl: Interface;
|
|
649
|
+
if (doesSupportReadLines()) rl = (await open(filePath)).readLines();
|
|
650
|
+
else
|
|
651
|
+
rl = createInterface({
|
|
652
|
+
input: createReadStream(filePath),
|
|
653
|
+
crlfDelay: Infinity,
|
|
654
|
+
});
|
|
655
|
+
let lineCount = 0;
|
|
656
|
+
|
|
657
|
+
for await (const line of rl) {
|
|
658
|
+
lineCount++;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
|
|
492
662
|
export default class File {
|
|
493
663
|
static get = get;
|
|
494
664
|
static remove = remove;
|
|
@@ -498,4 +668,7 @@ export default class File {
|
|
|
498
668
|
static encode = encode;
|
|
499
669
|
static decode = decode;
|
|
500
670
|
static isExists = isExists;
|
|
671
|
+
static sum = sum;
|
|
672
|
+
static min = min;
|
|
673
|
+
static max = max;
|
|
501
674
|
}
|
package/index.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decode } from "./file";
|
|
1
2
|
import Inibase, { Schema, Data } from "./index";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
// import os from "os";
|
|
@@ -197,9 +198,50 @@ const data_2 = [
|
|
|
197
198
|
try {
|
|
198
199
|
// db.setTableSchema("database", schema_2);
|
|
199
200
|
// const DATA = await db.post("database", data_2);
|
|
200
|
-
const DATA = await db.
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
// const DATA = await db.delete("database", 2);
|
|
202
|
+
// const DATA = await db.post("database", {
|
|
203
|
+
// slug: "iptv",
|
|
204
|
+
// allowed_domains: ['https://iptv.kamatil.com'],
|
|
205
|
+
// tables: [
|
|
206
|
+
// {
|
|
207
|
+
// id: 1,
|
|
208
|
+
// slug: "user",
|
|
209
|
+
// allowed_methods: [
|
|
210
|
+
// {
|
|
211
|
+
// role: "user",
|
|
212
|
+
// methods: ["c", "r", "u"],
|
|
213
|
+
// },
|
|
214
|
+
// {
|
|
215
|
+
// role: "guest",
|
|
216
|
+
// methods: ["c"],
|
|
217
|
+
// },
|
|
218
|
+
// {
|
|
219
|
+
// role: "admin",
|
|
220
|
+
// methods: ["d", "u"],
|
|
221
|
+
// },
|
|
222
|
+
// ],
|
|
223
|
+
// },
|
|
224
|
+
// {
|
|
225
|
+
// id: 1,
|
|
226
|
+
// slug: "user",
|
|
227
|
+
// allowed_methods: [
|
|
228
|
+
// {
|
|
229
|
+
// role: "user",
|
|
230
|
+
// methods: ["c", "r", "u"],
|
|
231
|
+
// },
|
|
232
|
+
// {
|
|
233
|
+
// role: "guest",
|
|
234
|
+
// methods: ["c"],
|
|
235
|
+
// },
|
|
236
|
+
// {
|
|
237
|
+
// role: "admin",
|
|
238
|
+
// methods: ["d", "u"],
|
|
239
|
+
// },
|
|
240
|
+
// ],
|
|
241
|
+
// },
|
|
242
|
+
// ],
|
|
243
|
+
// });
|
|
244
|
+
const DATA = await db.get("database");
|
|
203
245
|
console.log(JSON.stringify(DATA, null, 4));
|
|
204
246
|
} catch (er) {
|
|
205
247
|
console.log(er);
|