@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 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<T = string> {
302
+ export class FileType {
300
303
  file;
301
- constructor(filepath: string, contents?: T)
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
- write(contents: string): void;
12
- streamFrom(...options: Parameters<(typeof Readable)['from']>): Promise<void>;
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
- export declare class FileType<T = string> {
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?: T);
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 { writeToString, parseString } from 'fast-csv';
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
- write(contents) {
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.writeFileSync(this.path, contents);
36
+ return fs.createWriteStream(this.path);
27
37
  }
28
- async streamFrom(...options) {
29
- return finished(Readable.from(...options).pipe(fs.createWriteStream(this.path)));
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
- const contents = await writeToString([headers, ...outRows]);
140
- this.file.write(contents);
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
- const content = this.file.read();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brianbuie/node-kit",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "license": "ISC",
5
5
  "repository": {
6
6
  "type": "git",
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('File', () => {
39
- it('Handles request body as stream input', async () => {
40
- const res = await fetch('https://testingbot.com/free-online-tools/random-avatar/300');
41
- const img = testDir.file('image.jpg');
42
- if (res.body) {
43
- await img.streamFrom(res.body);
44
- assert(img.exists);
45
- } else {
46
- assert(false, 'No response body');
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('File.ndjson', () => {
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('File.json', () => {
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 { writeToString, parseString } from 'fast-csv';
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
- write(contents: string) {
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.writeFileSync(this.path, contents);
44
+ return fs.createWriteStream(this.path);
33
45
  }
34
46
 
35
- async streamFrom(...options: Parameters<(typeof Readable)['from']>) {
36
- return finished(Readable.from(...options).pipe(fs.createWriteStream(this.path)));
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
- export class FileType<T = string> {
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?: T) {
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
- const contents = await writeToString([headers, ...outRows]);
166
- this.file.write(contents);
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
- const content = this.file.read();
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
  }