@brianbuie/node-kit 0.8.0 → 0.9.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 +18 -6
- package/dist/File.d.ts +27 -9
- package/dist/File.js +55 -40
- package/package.json +1 -1
- package/src/File.test.ts +32 -28
- package/src/File.ts +55 -40
package/README.md
CHANGED
|
@@ -249,10 +249,11 @@ export class File {
|
|
|
249
249
|
get exists()
|
|
250
250
|
delete()
|
|
251
251
|
read()
|
|
252
|
-
write(contents: string)
|
|
253
|
-
async streamFrom(...options: Parameters<(typeof Readable)["from"]>)
|
|
254
|
-
append(lines: string | string[])
|
|
255
252
|
lines()
|
|
253
|
+
get readStream()
|
|
254
|
+
get writeStream()
|
|
255
|
+
write(contents: string | ReadableStream)
|
|
256
|
+
append(lines: string | string[])
|
|
256
257
|
static get FileType()
|
|
257
258
|
json<T>(contents?: T)
|
|
258
259
|
static get json()
|
|
@@ -295,13 +296,15 @@ Links: [API](#api), [Classes](#classes), [Functions](#functions), [Types](#types
|
|
|
295
296
|
---
|
|
296
297
|
## Class: FileType
|
|
297
298
|
|
|
299
|
+
A generic file adaptor, extended by specific file type implementations
|
|
300
|
+
|
|
298
301
|
```ts
|
|
299
|
-
export class FileType
|
|
302
|
+
export class FileType {
|
|
300
303
|
file;
|
|
301
|
-
constructor(filepath: string, contents?:
|
|
304
|
+
constructor(filepath: string, contents?: string)
|
|
302
305
|
get exists()
|
|
303
|
-
delete()
|
|
304
306
|
get path()
|
|
307
|
+
delete()
|
|
305
308
|
}
|
|
306
309
|
```
|
|
307
310
|
|
|
@@ -310,10 +313,14 @@ Links: [API](#api), [Classes](#classes), [Functions](#functions), [Types](#types
|
|
|
310
313
|
---
|
|
311
314
|
## Class: FileTypeCsv
|
|
312
315
|
|
|
316
|
+
Comma separated values (.csv).
|
|
317
|
+
Input rows as objects, keys are used as column headers
|
|
318
|
+
|
|
313
319
|
```ts
|
|
314
320
|
export class FileTypeCsv<Row extends object> extends FileType {
|
|
315
321
|
constructor(filepath: string)
|
|
316
322
|
async write(rows: Row[], keys?: Key<Row>[])
|
|
323
|
+
#parseVal(val: string)
|
|
317
324
|
async read()
|
|
318
325
|
}
|
|
319
326
|
```
|
|
@@ -325,6 +332,9 @@ Links: [API](#api), [Classes](#classes), [Functions](#functions), [Types](#types
|
|
|
325
332
|
---
|
|
326
333
|
## Class: FileTypeJson
|
|
327
334
|
|
|
335
|
+
A .json file that maintains data type when reading/writing.
|
|
336
|
+
This is unsafe! Type is not checked at runtime, avoid using on files manipulated outside of your application.
|
|
337
|
+
|
|
328
338
|
```ts
|
|
329
339
|
export class FileTypeJson<T> extends FileType {
|
|
330
340
|
constructor(filepath: string, contents?: T)
|
|
@@ -340,6 +350,8 @@ Links: [API](#api), [Classes](#classes), [Functions](#functions), [Types](#types
|
|
|
340
350
|
---
|
|
341
351
|
## Class: FileTypeNdjson
|
|
342
352
|
|
|
353
|
+
New-line delimited json file (.ndjson)
|
|
354
|
+
|
|
343
355
|
```ts
|
|
344
356
|
export class FileTypeNdjson<T extends object> extends FileType {
|
|
345
357
|
constructor(filepath: string, lines?: T | T[])
|
package/dist/File.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
1
2
|
import { Readable } from 'node:stream';
|
|
2
3
|
/**
|
|
3
4
|
* WARNING: API will change!
|
|
@@ -8,17 +9,18 @@ export declare class File {
|
|
|
8
9
|
get exists(): boolean;
|
|
9
10
|
delete(): void;
|
|
10
11
|
read(): string | undefined;
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* @returns lines as strings, removes trailing '\n'
|
|
14
|
+
*/
|
|
15
|
+
lines(): string[];
|
|
16
|
+
get readStream(): fs.ReadStream | Readable;
|
|
17
|
+
get writeStream(): fs.WriteStream;
|
|
18
|
+
write(contents: string | ReadableStream): void | Promise<void>;
|
|
13
19
|
/**
|
|
14
20
|
* creates file if it doesn't exist, appends string or array of strings as new lines.
|
|
15
21
|
* File always ends with '\n', so contents don't need to be read before appending
|
|
16
22
|
*/
|
|
17
23
|
append(lines: string | string[]): void;
|
|
18
|
-
/**
|
|
19
|
-
* @returns lines as strings, removes trailing '\n'
|
|
20
|
-
*/
|
|
21
|
-
lines(): string[];
|
|
22
24
|
static get FileType(): typeof FileType;
|
|
23
25
|
json<T>(contents?: T): FileTypeJson<T>;
|
|
24
26
|
static get json(): typeof FileTypeJson;
|
|
@@ -27,25 +29,41 @@ export declare class File {
|
|
|
27
29
|
csv<T extends object>(rows?: T[], keys?: (keyof T)[]): Promise<FileTypeCsv<T>>;
|
|
28
30
|
static get csv(): typeof FileTypeCsv;
|
|
29
31
|
}
|
|
30
|
-
|
|
32
|
+
/**
|
|
33
|
+
* A generic file adaptor, extended by specific file type implementations
|
|
34
|
+
*/
|
|
35
|
+
export declare class FileType {
|
|
31
36
|
file: File;
|
|
32
|
-
constructor(filepath: string, contents?:
|
|
37
|
+
constructor(filepath: string, contents?: string);
|
|
33
38
|
get exists(): boolean;
|
|
34
|
-
delete(): void;
|
|
35
39
|
get path(): string;
|
|
40
|
+
delete(): void;
|
|
36
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* A .json file that maintains data type when reading/writing.
|
|
44
|
+
* This is unsafe! Type is not checked at runtime, avoid using on files manipulated outside of your application.
|
|
45
|
+
*/
|
|
37
46
|
export declare class FileTypeJson<T> extends FileType {
|
|
38
47
|
constructor(filepath: string, contents?: T);
|
|
39
48
|
read(): T | undefined;
|
|
40
49
|
write(contents: T): void;
|
|
41
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* New-line delimited json file (.ndjson)
|
|
53
|
+
* @see https://jsonltools.com/ndjson-format-specification
|
|
54
|
+
*/
|
|
42
55
|
export declare class FileTypeNdjson<T extends object> extends FileType {
|
|
43
56
|
constructor(filepath: string, lines?: T | T[]);
|
|
44
57
|
append(lines: T | T[]): void;
|
|
45
58
|
lines(): T[];
|
|
46
59
|
}
|
|
47
60
|
type Key<T extends object> = keyof T;
|
|
61
|
+
/**
|
|
62
|
+
* Comma separated values (.csv).
|
|
63
|
+
* Input rows as objects, keys are used as column headers
|
|
64
|
+
*/
|
|
48
65
|
export declare class FileTypeCsv<Row extends object> extends FileType {
|
|
66
|
+
#private;
|
|
49
67
|
constructor(filepath: string);
|
|
50
68
|
write(rows: Row[], keys?: Key<Row>[]): Promise<void>;
|
|
51
69
|
read(): Promise<Row[]>;
|
package/dist/File.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { Readable } from 'node:stream';
|
|
4
4
|
import { finished } from 'node:stream/promises';
|
|
5
|
-
import {
|
|
5
|
+
import { writeToStream, parseStream } from 'fast-csv';
|
|
6
6
|
import { snapshot } from './snapshot.js';
|
|
7
7
|
/**
|
|
8
8
|
* WARNING: API will change!
|
|
@@ -21,12 +21,27 @@ export class File {
|
|
|
21
21
|
read() {
|
|
22
22
|
return this.exists ? fs.readFileSync(this.path, 'utf8') : undefined;
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* @returns lines as strings, removes trailing '\n'
|
|
26
|
+
*/
|
|
27
|
+
lines() {
|
|
28
|
+
const contents = (this.read() || '').split('\n');
|
|
29
|
+
return contents.slice(0, contents.length - 1);
|
|
30
|
+
}
|
|
31
|
+
get readStream() {
|
|
32
|
+
return this.exists ? fs.createReadStream(this.path) : Readable.from([]);
|
|
33
|
+
}
|
|
34
|
+
get writeStream() {
|
|
25
35
|
fs.mkdirSync(path.parse(this.path).dir, { recursive: true });
|
|
26
|
-
fs.
|
|
36
|
+
return fs.createWriteStream(this.path);
|
|
27
37
|
}
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
write(contents) {
|
|
39
|
+
fs.mkdirSync(path.parse(this.path).dir, { recursive: true });
|
|
40
|
+
if (typeof contents === 'string')
|
|
41
|
+
return fs.writeFileSync(this.path, contents);
|
|
42
|
+
if (contents instanceof ReadableStream)
|
|
43
|
+
return finished(Readable.from(contents).pipe(this.writeStream));
|
|
44
|
+
throw new Error(`Invalid content type: ${typeof contents}`);
|
|
30
45
|
}
|
|
31
46
|
/**
|
|
32
47
|
* creates file if it doesn't exist, appends string or array of strings as new lines.
|
|
@@ -38,13 +53,6 @@ export class File {
|
|
|
38
53
|
const contents = Array.isArray(lines) ? lines.join('\n') : lines;
|
|
39
54
|
fs.appendFileSync(this.path, contents + '\n');
|
|
40
55
|
}
|
|
41
|
-
/**
|
|
42
|
-
* @returns lines as strings, removes trailing '\n'
|
|
43
|
-
*/
|
|
44
|
-
lines() {
|
|
45
|
-
const contents = (this.read() || '').split('\n');
|
|
46
|
-
return contents.slice(0, contents.length - 1);
|
|
47
|
-
}
|
|
48
56
|
static get FileType() {
|
|
49
57
|
return FileType;
|
|
50
58
|
}
|
|
@@ -70,27 +78,30 @@ export class File {
|
|
|
70
78
|
return FileTypeCsv;
|
|
71
79
|
}
|
|
72
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* A generic file adaptor, extended by specific file type implementations
|
|
83
|
+
*/
|
|
73
84
|
export class FileType {
|
|
74
85
|
file;
|
|
75
86
|
constructor(filepath, contents) {
|
|
76
87
|
this.file = new File(filepath);
|
|
77
|
-
if (contents)
|
|
78
|
-
if (typeof contents !== 'string') {
|
|
79
|
-
throw new Error('File contents must be a string');
|
|
80
|
-
}
|
|
88
|
+
if (contents)
|
|
81
89
|
this.file.write(contents);
|
|
82
|
-
}
|
|
83
90
|
}
|
|
84
91
|
get exists() {
|
|
85
92
|
return this.file.exists;
|
|
86
93
|
}
|
|
87
|
-
delete() {
|
|
88
|
-
this.file.delete();
|
|
89
|
-
}
|
|
90
94
|
get path() {
|
|
91
95
|
return this.file.path;
|
|
92
96
|
}
|
|
97
|
+
delete() {
|
|
98
|
+
this.file.delete();
|
|
99
|
+
}
|
|
93
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* A .json file that maintains data type when reading/writing.
|
|
103
|
+
* This is unsafe! Type is not checked at runtime, avoid using on files manipulated outside of your application.
|
|
104
|
+
*/
|
|
94
105
|
export class FileTypeJson extends FileType {
|
|
95
106
|
constructor(filepath, contents) {
|
|
96
107
|
super(filepath.endsWith('.json') ? filepath : filepath + '.json');
|
|
@@ -105,6 +116,10 @@ export class FileTypeJson extends FileType {
|
|
|
105
116
|
this.file.write(JSON.stringify(snapshot(contents), null, 2));
|
|
106
117
|
}
|
|
107
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* New-line delimited json file (.ndjson)
|
|
121
|
+
* @see https://jsonltools.com/ndjson-format-specification
|
|
122
|
+
*/
|
|
108
123
|
export class FileTypeNdjson extends FileType {
|
|
109
124
|
constructor(filepath, lines) {
|
|
110
125
|
super(filepath.endsWith('.ndjson') ? filepath : filepath + '.ndjson');
|
|
@@ -118,6 +133,10 @@ export class FileTypeNdjson extends FileType {
|
|
|
118
133
|
return this.file.lines().map((l) => JSON.parse(l));
|
|
119
134
|
}
|
|
120
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Comma separated values (.csv).
|
|
138
|
+
* Input rows as objects, keys are used as column headers
|
|
139
|
+
*/
|
|
121
140
|
export class FileTypeCsv extends FileType {
|
|
122
141
|
constructor(filepath) {
|
|
123
142
|
super(filepath.endsWith('.csv') ? filepath : filepath + '.csv');
|
|
@@ -136,35 +155,31 @@ export class FileTypeCsv extends FileType {
|
|
|
136
155
|
}
|
|
137
156
|
const headers = Array.from(headerSet);
|
|
138
157
|
const outRows = rows.map((row) => headers.map((key) => row[key]));
|
|
139
|
-
|
|
140
|
-
|
|
158
|
+
return finished(writeToStream(this.file.writeStream, [headers, ...outRows]));
|
|
159
|
+
}
|
|
160
|
+
#parseVal(val) {
|
|
161
|
+
if (val.toLowerCase() === 'false')
|
|
162
|
+
return false;
|
|
163
|
+
if (val.toLowerCase() === 'true')
|
|
164
|
+
return true;
|
|
165
|
+
if (val.length === 0)
|
|
166
|
+
return null;
|
|
167
|
+
if (/^[\.0-9]+$/.test(val))
|
|
168
|
+
return Number(val);
|
|
169
|
+
return val;
|
|
141
170
|
}
|
|
142
171
|
async read() {
|
|
143
172
|
return new Promise((resolve, reject) => {
|
|
144
173
|
const parsed = [];
|
|
145
|
-
|
|
146
|
-
if (!content)
|
|
147
|
-
return resolve(parsed);
|
|
148
|
-
function parseVal(val) {
|
|
149
|
-
if (val.toLowerCase() === 'false')
|
|
150
|
-
return false;
|
|
151
|
-
if (val.toLowerCase() === 'true')
|
|
152
|
-
return true;
|
|
153
|
-
if (val.length === 0)
|
|
154
|
-
return null;
|
|
155
|
-
if (/^[\.0-9]+$/.test(val))
|
|
156
|
-
return Number(val);
|
|
157
|
-
return val;
|
|
158
|
-
}
|
|
159
|
-
parseString(content, { headers: true })
|
|
174
|
+
parseStream(this.file.readStream, { headers: true })
|
|
160
175
|
.on('error', (e) => reject(e))
|
|
161
|
-
.on('end', () => resolve(parsed))
|
|
162
176
|
.on('data', (raw) => {
|
|
163
177
|
parsed.push(Object.entries(raw).reduce((all, [key, val]) => ({
|
|
164
178
|
...all,
|
|
165
|
-
[key]: parseVal(val),
|
|
179
|
+
[key]: this.#parseVal(val),
|
|
166
180
|
}), {}));
|
|
167
|
-
})
|
|
181
|
+
})
|
|
182
|
+
.on('end', () => resolve(parsed));
|
|
168
183
|
});
|
|
169
184
|
}
|
|
170
185
|
}
|
package/package.json
CHANGED
package/src/File.test.ts
CHANGED
|
@@ -14,6 +14,17 @@ const thing = {
|
|
|
14
14
|
e: null,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
describe('File', () => {
|
|
18
|
+
it('Handles request body as stream input', async () => {
|
|
19
|
+
const img = testDir.file('image.jpg');
|
|
20
|
+
await fetch('https://testingbot.com/free-online-tools/random-avatar/300').then((res) => {
|
|
21
|
+
if (!res.body) throw new Error('No response body');
|
|
22
|
+
return img.write(res.body);
|
|
23
|
+
});
|
|
24
|
+
assert(img.exists);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
17
28
|
describe('FileType', () => {
|
|
18
29
|
it('Creates instances', () => {
|
|
19
30
|
const test1 = new File.FileType(testDir.filepath('test1.txt'));
|
|
@@ -35,20 +46,23 @@ describe('FileType', () => {
|
|
|
35
46
|
});
|
|
36
47
|
});
|
|
37
48
|
|
|
38
|
-
describe('
|
|
39
|
-
it('
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
describe('FileTypeJson', () => {
|
|
50
|
+
it('Saves data as json', () => {
|
|
51
|
+
const file = testDir.file('jsonfile-data').json(thing);
|
|
52
|
+
assert.deepStrictEqual(file.read(), thing);
|
|
53
|
+
file.write(thing);
|
|
54
|
+
assert.deepStrictEqual(file.read(), thing);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('Does not create file when reading', () => {
|
|
58
|
+
const file = testDir.file('test123').json();
|
|
59
|
+
const contents = file.read();
|
|
60
|
+
assert(contents === undefined);
|
|
61
|
+
assert(!file.exists);
|
|
48
62
|
});
|
|
49
63
|
});
|
|
50
64
|
|
|
51
|
-
describe('
|
|
65
|
+
describe('FileTypeNdjson', () => {
|
|
52
66
|
it('Appends new lines correctly', () => {
|
|
53
67
|
const file = testDir.file('appends-lines').ndjson();
|
|
54
68
|
file.delete();
|
|
@@ -70,23 +84,7 @@ describe('File.ndjson', () => {
|
|
|
70
84
|
});
|
|
71
85
|
});
|
|
72
86
|
|
|
73
|
-
describe('
|
|
74
|
-
it('Saves data as json', () => {
|
|
75
|
-
const file = testDir.file('jsonfile-data').json(thing);
|
|
76
|
-
assert.deepStrictEqual(file.read(), thing);
|
|
77
|
-
file.write(thing);
|
|
78
|
-
assert.deepStrictEqual(file.read(), thing);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('Does not create file when reading', () => {
|
|
82
|
-
const file = testDir.file('test123').json();
|
|
83
|
-
const contents = file.read();
|
|
84
|
-
assert(contents === undefined);
|
|
85
|
-
assert(!file.exists);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe('File.csv', () => {
|
|
87
|
+
describe('FileTypeCsv', () => {
|
|
90
88
|
it('Saves data as csv', async () => {
|
|
91
89
|
const things = [thing, thing, thing];
|
|
92
90
|
const file = await testDir.file('csv-data').csv(things);
|
|
@@ -95,4 +93,10 @@ describe('File.csv', () => {
|
|
|
95
93
|
assert.deepEqual(row, thing);
|
|
96
94
|
});
|
|
97
95
|
});
|
|
96
|
+
it('Reads file that does not exist', async () => {
|
|
97
|
+
const file = await testDir.file('bogus').csv();
|
|
98
|
+
const contents = await file.read();
|
|
99
|
+
assert(Array.isArray(contents));
|
|
100
|
+
assert(contents.length === 0);
|
|
101
|
+
});
|
|
98
102
|
});
|
package/src/File.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { Readable } from 'node:stream';
|
|
4
4
|
import { finished } from 'node:stream/promises';
|
|
5
|
-
import {
|
|
5
|
+
import { writeToStream, parseStream } from 'fast-csv';
|
|
6
6
|
import { snapshot } from './snapshot.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -27,13 +27,28 @@ export class File {
|
|
|
27
27
|
return this.exists ? fs.readFileSync(this.path, 'utf8') : undefined;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* @returns lines as strings, removes trailing '\n'
|
|
32
|
+
*/
|
|
33
|
+
lines() {
|
|
34
|
+
const contents = (this.read() || '').split('\n');
|
|
35
|
+
return contents.slice(0, contents.length - 1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get readStream() {
|
|
39
|
+
return this.exists ? fs.createReadStream(this.path) : Readable.from([]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get writeStream() {
|
|
31
43
|
fs.mkdirSync(path.parse(this.path).dir, { recursive: true });
|
|
32
|
-
fs.
|
|
44
|
+
return fs.createWriteStream(this.path);
|
|
33
45
|
}
|
|
34
46
|
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
write(contents: string | ReadableStream) {
|
|
48
|
+
fs.mkdirSync(path.parse(this.path).dir, { recursive: true });
|
|
49
|
+
if (typeof contents === 'string') return fs.writeFileSync(this.path, contents);
|
|
50
|
+
if (contents instanceof ReadableStream) return finished(Readable.from(contents).pipe(this.writeStream));
|
|
51
|
+
throw new Error(`Invalid content type: ${typeof contents}`);
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
/**
|
|
@@ -46,14 +61,6 @@ export class File {
|
|
|
46
61
|
fs.appendFileSync(this.path, contents + '\n');
|
|
47
62
|
}
|
|
48
63
|
|
|
49
|
-
/**
|
|
50
|
-
* @returns lines as strings, removes trailing '\n'
|
|
51
|
-
*/
|
|
52
|
-
lines() {
|
|
53
|
-
const contents = (this.read() || '').split('\n');
|
|
54
|
-
return contents.slice(0, contents.length - 1);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
64
|
static get FileType() {
|
|
58
65
|
return FileType;
|
|
59
66
|
}
|
|
@@ -85,32 +92,34 @@ export class File {
|
|
|
85
92
|
}
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
|
|
95
|
+
/**
|
|
96
|
+
* A generic file adaptor, extended by specific file type implementations
|
|
97
|
+
*/
|
|
98
|
+
export class FileType {
|
|
89
99
|
file;
|
|
90
100
|
|
|
91
|
-
constructor(filepath: string, contents?:
|
|
101
|
+
constructor(filepath: string, contents?: string) {
|
|
92
102
|
this.file = new File(filepath);
|
|
93
|
-
if (contents)
|
|
94
|
-
if (typeof contents !== 'string') {
|
|
95
|
-
throw new Error('File contents must be a string');
|
|
96
|
-
}
|
|
97
|
-
this.file.write(contents);
|
|
98
|
-
}
|
|
103
|
+
if (contents) this.file.write(contents);
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
get exists() {
|
|
102
107
|
return this.file.exists;
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
delete() {
|
|
106
|
-
this.file.delete();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
110
|
get path() {
|
|
110
111
|
return this.file.path;
|
|
111
112
|
}
|
|
113
|
+
|
|
114
|
+
delete() {
|
|
115
|
+
this.file.delete();
|
|
116
|
+
}
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
/**
|
|
120
|
+
* A .json file that maintains data type when reading/writing.
|
|
121
|
+
* This is unsafe! Type is not checked at runtime, avoid using on files manipulated outside of your application.
|
|
122
|
+
*/
|
|
114
123
|
export class FileTypeJson<T> extends FileType {
|
|
115
124
|
constructor(filepath: string, contents?: T) {
|
|
116
125
|
super(filepath.endsWith('.json') ? filepath : filepath + '.json');
|
|
@@ -127,6 +136,10 @@ export class FileTypeJson<T> extends FileType {
|
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
|
|
139
|
+
/**
|
|
140
|
+
* New-line delimited json file (.ndjson)
|
|
141
|
+
* @see https://jsonltools.com/ndjson-format-specification
|
|
142
|
+
*/
|
|
130
143
|
export class FileTypeNdjson<T extends object> extends FileType {
|
|
131
144
|
constructor(filepath: string, lines?: T | T[]) {
|
|
132
145
|
super(filepath.endsWith('.ndjson') ? filepath : filepath + '.ndjson');
|
|
@@ -146,6 +159,10 @@ export class FileTypeNdjson<T extends object> extends FileType {
|
|
|
146
159
|
|
|
147
160
|
type Key<T extends object> = keyof T;
|
|
148
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Comma separated values (.csv).
|
|
164
|
+
* Input rows as objects, keys are used as column headers
|
|
165
|
+
*/
|
|
149
166
|
export class FileTypeCsv<Row extends object> extends FileType {
|
|
150
167
|
constructor(filepath: string) {
|
|
151
168
|
super(filepath.endsWith('.csv') ? filepath : filepath + '.csv');
|
|
@@ -162,36 +179,34 @@ export class FileTypeCsv<Row extends object> extends FileType {
|
|
|
162
179
|
}
|
|
163
180
|
const headers = Array.from(headerSet);
|
|
164
181
|
const outRows = rows.map((row) => headers.map((key) => row[key]));
|
|
165
|
-
|
|
166
|
-
|
|
182
|
+
return finished(writeToStream(this.file.writeStream, [headers, ...outRows]));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#parseVal(val: string) {
|
|
186
|
+
if (val.toLowerCase() === 'false') return false;
|
|
187
|
+
if (val.toLowerCase() === 'true') return true;
|
|
188
|
+
if (val.length === 0) return null;
|
|
189
|
+
if (/^[\.0-9]+$/.test(val)) return Number(val);
|
|
190
|
+
return val;
|
|
167
191
|
}
|
|
168
192
|
|
|
169
193
|
async read() {
|
|
170
194
|
return new Promise<Row[]>((resolve, reject) => {
|
|
171
195
|
const parsed: Row[] = [];
|
|
172
|
-
|
|
173
|
-
if (!content) return resolve(parsed);
|
|
174
|
-
function parseVal(val: string) {
|
|
175
|
-
if (val.toLowerCase() === 'false') return false;
|
|
176
|
-
if (val.toLowerCase() === 'true') return true;
|
|
177
|
-
if (val.length === 0) return null;
|
|
178
|
-
if (/^[\.0-9]+$/.test(val)) return Number(val);
|
|
179
|
-
return val;
|
|
180
|
-
}
|
|
181
|
-
parseString(content, { headers: true })
|
|
196
|
+
parseStream(this.file.readStream, { headers: true })
|
|
182
197
|
.on('error', (e) => reject(e))
|
|
183
|
-
.on('end', () => resolve(parsed))
|
|
184
198
|
.on('data', (raw: Record<Key<Row>, string>) => {
|
|
185
199
|
parsed.push(
|
|
186
200
|
Object.entries(raw).reduce(
|
|
187
201
|
(all, [key, val]) => ({
|
|
188
202
|
...all,
|
|
189
|
-
[key]: parseVal(val as string),
|
|
203
|
+
[key]: this.#parseVal(val as string),
|
|
190
204
|
}),
|
|
191
205
|
{} as Row,
|
|
192
206
|
),
|
|
193
207
|
);
|
|
194
|
-
})
|
|
208
|
+
})
|
|
209
|
+
.on('end', () => resolve(parsed));
|
|
195
210
|
});
|
|
196
211
|
}
|
|
197
212
|
}
|