@brianbuie/node-kit 0.5.1 → 0.5.2
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/dist/Cache.d.ts +1 -1
- package/dist/Dir.js +2 -2
- package/dist/Fetcher.d.ts +21 -8
- package/dist/Fetcher.js +10 -4
- package/dist/File.d.ts +20 -7
- package/dist/File.js +78 -12
- package/dist/Log.js +3 -4
- package/dist/TypeWriter.js +1 -1
- package/dist/_index.d.ts +1 -1
- package/package.json +6 -2
- package/src/Dir.ts +2 -2
- package/src/Fetcher.ts +11 -4
- package/src/File.test.ts +18 -7
- package/src/File.ts +85 -15
- package/src/Log.ts +3 -5
- package/src/TypeWriter.ts +1 -1
- package/src/_index.ts +1 -1
package/dist/Cache.d.ts
CHANGED
package/dist/Dir.js
CHANGED
package/dist/Fetcher.d.ts
CHANGED
|
@@ -17,17 +17,26 @@ export type FetchOptions = RequestInit & {
|
|
|
17
17
|
*/
|
|
18
18
|
export declare class Fetcher {
|
|
19
19
|
defaultOptions: {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
body?: BodyInit | null;
|
|
21
|
+
cache?: RequestCache;
|
|
22
|
+
credentials?: RequestCredentials;
|
|
23
|
+
headers?: (HeadersInit & Record<string, string>) | undefined;
|
|
24
|
+
integrity?: string;
|
|
25
|
+
keepalive?: boolean;
|
|
26
|
+
method?: string;
|
|
27
|
+
mode?: RequestMode;
|
|
28
|
+
priority?: RequestPriority;
|
|
29
|
+
redirect?: RequestRedirect;
|
|
30
|
+
referrer?: string;
|
|
31
|
+
referrerPolicy?: ReferrerPolicy;
|
|
32
|
+
signal?: AbortSignal | null;
|
|
33
|
+
window?: null;
|
|
24
34
|
base?: string;
|
|
25
35
|
query?: Query;
|
|
26
|
-
headers?: Record<string, string>;
|
|
27
36
|
data?: any;
|
|
28
|
-
timeout
|
|
29
|
-
retries
|
|
30
|
-
retryDelay
|
|
37
|
+
timeout: number;
|
|
38
|
+
retries: number;
|
|
39
|
+
retryDelay: number;
|
|
31
40
|
};
|
|
32
41
|
constructor(opts?: FetchOptions);
|
|
33
42
|
/**
|
|
@@ -35,6 +44,10 @@ export declare class Fetcher {
|
|
|
35
44
|
* Also returns domain, to help with cookies
|
|
36
45
|
*/
|
|
37
46
|
buildUrl(route: Route, opts?: FetchOptions): [URL, string];
|
|
47
|
+
/**
|
|
48
|
+
* Merges options to get headers. Useful when extending the Fetcher class to add custom auth.
|
|
49
|
+
*/
|
|
50
|
+
buildHeaders(route: Route, opts?: FetchOptions): HeadersInit & Record<string, string>;
|
|
38
51
|
/**
|
|
39
52
|
* Builds request, merging defaultOptions and provided options
|
|
40
53
|
* Includes Abort signal for timeout
|
package/dist/Fetcher.js
CHANGED
|
@@ -8,12 +8,12 @@ import extractDomain from 'extract-domain';
|
|
|
8
8
|
export class Fetcher {
|
|
9
9
|
defaultOptions;
|
|
10
10
|
constructor(opts = {}) {
|
|
11
|
-
|
|
11
|
+
this.defaultOptions = {
|
|
12
12
|
timeout: 60000,
|
|
13
13
|
retries: 0,
|
|
14
14
|
retryDelay: 3000,
|
|
15
|
+
...opts,
|
|
15
16
|
};
|
|
16
|
-
this.defaultOptions = merge(defaultOptions, opts);
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Build URL with URLSearchParams if query is provided
|
|
@@ -39,6 +39,13 @@ export class Fetcher {
|
|
|
39
39
|
const domain = extractDomain(url.href);
|
|
40
40
|
return [url, domain];
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Merges options to get headers. Useful when extending the Fetcher class to add custom auth.
|
|
44
|
+
*/
|
|
45
|
+
buildHeaders(route, opts = {}) {
|
|
46
|
+
const { headers } = merge({}, this.defaultOptions, opts);
|
|
47
|
+
return headers || {};
|
|
48
|
+
}
|
|
42
49
|
/**
|
|
43
50
|
* Builds request, merging defaultOptions and provided options
|
|
44
51
|
* Includes Abort signal for timeout
|
|
@@ -46,8 +53,8 @@ export class Fetcher {
|
|
|
46
53
|
buildRequest(route, opts = {}) {
|
|
47
54
|
const mergedOptions = merge({}, this.defaultOptions, opts);
|
|
48
55
|
const { query, data, timeout, retries, ...init } = mergedOptions;
|
|
56
|
+
init.headers = this.buildHeaders(route, mergedOptions);
|
|
49
57
|
if (data) {
|
|
50
|
-
init.headers = init.headers || {};
|
|
51
58
|
init.headers['content-type'] = init.headers['content-type'] || 'application/json';
|
|
52
59
|
init.method = init.method || 'POST';
|
|
53
60
|
init.body = JSON.stringify(data);
|
|
@@ -70,7 +77,6 @@ export class Fetcher {
|
|
|
70
77
|
let attempt = 0;
|
|
71
78
|
while (attempt < maxAttempts) {
|
|
72
79
|
attempt++;
|
|
73
|
-
// Rebuild request on every attempt to reset AbortSignal.timeout
|
|
74
80
|
const [req] = this.buildRequest(route, opts);
|
|
75
81
|
const res = await fetch(req)
|
|
76
82
|
.then((r) => {
|
package/dist/File.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* WARNING: API will change!
|
|
4
|
+
*/
|
|
1
5
|
export declare class File {
|
|
2
6
|
path: string;
|
|
3
7
|
constructor(filepath: string);
|
|
4
8
|
get exists(): boolean;
|
|
9
|
+
createWriteStream(options?: Parameters<typeof fs.createWriteStream>[1]): fs.WriteStream;
|
|
10
|
+
delete(): void;
|
|
5
11
|
read(): string | undefined;
|
|
6
12
|
write(contents: string): void;
|
|
7
13
|
/**
|
|
@@ -13,28 +19,35 @@ export declare class File {
|
|
|
13
19
|
* @returns lines as strings, removes trailing '\n'
|
|
14
20
|
*/
|
|
15
21
|
lines(): string[];
|
|
16
|
-
|
|
17
|
-
static get Adaptor(): typeof FileAdaptor;
|
|
22
|
+
static get Adaptor(): typeof Adaptor;
|
|
18
23
|
json<T>(contents?: T): JsonFile<T>;
|
|
19
24
|
static get json(): typeof JsonFile;
|
|
20
|
-
ndjson<T>(lines?: T | T[]): NdjsonFile<T>;
|
|
25
|
+
ndjson<T extends object>(lines?: T | T[]): NdjsonFile<T>;
|
|
21
26
|
static get ndjson(): typeof NdjsonFile;
|
|
27
|
+
csv<T extends object>(rows?: T[], keys?: (keyof T)[]): Promise<CsvFile<T>>;
|
|
28
|
+
static get csv(): typeof CsvFile;
|
|
22
29
|
}
|
|
23
|
-
declare class
|
|
30
|
+
declare class Adaptor<T = string> {
|
|
24
31
|
file: File;
|
|
25
32
|
constructor(filepath: string, contents?: T);
|
|
26
33
|
get exists(): boolean;
|
|
27
|
-
get path(): string;
|
|
28
34
|
delete(): void;
|
|
35
|
+
get path(): string;
|
|
29
36
|
}
|
|
30
|
-
declare class JsonFile<T> extends
|
|
37
|
+
declare class JsonFile<T> extends Adaptor {
|
|
31
38
|
constructor(filepath: string, contents?: T);
|
|
32
39
|
read(): T | undefined;
|
|
33
40
|
write(contents: T): void;
|
|
34
41
|
}
|
|
35
|
-
declare class NdjsonFile<T> extends
|
|
42
|
+
declare class NdjsonFile<T extends object> extends Adaptor {
|
|
36
43
|
constructor(filepath: string, lines?: T | T[]);
|
|
37
44
|
append(lines: T | T[]): void;
|
|
38
45
|
lines(): T[];
|
|
39
46
|
}
|
|
47
|
+
type Key<T extends object> = keyof T;
|
|
48
|
+
declare class CsvFile<Row extends object> extends Adaptor {
|
|
49
|
+
constructor(filepath: string);
|
|
50
|
+
write(rows: Row[], keys?: Key<Row>[]): Promise<void>;
|
|
51
|
+
read(): Promise<Row[]>;
|
|
52
|
+
}
|
|
40
53
|
export {};
|
package/dist/File.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { writeToString, parseString } from 'fast-csv';
|
|
3
4
|
import { snapshot } from './snapshot.js';
|
|
5
|
+
/**
|
|
6
|
+
* WARNING: API will change!
|
|
7
|
+
*/
|
|
4
8
|
export class File {
|
|
5
9
|
path;
|
|
6
10
|
constructor(filepath) {
|
|
@@ -9,6 +13,12 @@ export class File {
|
|
|
9
13
|
get exists() {
|
|
10
14
|
return fs.existsSync(this.path);
|
|
11
15
|
}
|
|
16
|
+
createWriteStream(options = {}) {
|
|
17
|
+
return fs.createWriteStream(this.path, options);
|
|
18
|
+
}
|
|
19
|
+
delete() {
|
|
20
|
+
fs.rmSync(this.path, { force: true });
|
|
21
|
+
}
|
|
12
22
|
read() {
|
|
13
23
|
return this.exists ? fs.readFileSync(this.path, 'utf8') : undefined;
|
|
14
24
|
}
|
|
@@ -33,11 +43,8 @@ export class File {
|
|
|
33
43
|
const contents = (this.read() || '').split('\n');
|
|
34
44
|
return contents.slice(0, contents.length - 1);
|
|
35
45
|
}
|
|
36
|
-
delete() {
|
|
37
|
-
fs.rmSync(this.path, { force: true });
|
|
38
|
-
}
|
|
39
46
|
static get Adaptor() {
|
|
40
|
-
return
|
|
47
|
+
return Adaptor;
|
|
41
48
|
}
|
|
42
49
|
json(contents) {
|
|
43
50
|
return new JsonFile(this.path, contents);
|
|
@@ -51,8 +58,17 @@ export class File {
|
|
|
51
58
|
static get ndjson() {
|
|
52
59
|
return NdjsonFile;
|
|
53
60
|
}
|
|
61
|
+
async csv(rows, keys) {
|
|
62
|
+
const csvFile = new CsvFile(this.path);
|
|
63
|
+
if (rows)
|
|
64
|
+
await csvFile.write(rows, keys);
|
|
65
|
+
return csvFile;
|
|
66
|
+
}
|
|
67
|
+
static get csv() {
|
|
68
|
+
return CsvFile;
|
|
69
|
+
}
|
|
54
70
|
}
|
|
55
|
-
class
|
|
71
|
+
class Adaptor {
|
|
56
72
|
file;
|
|
57
73
|
constructor(filepath, contents) {
|
|
58
74
|
this.file = new File(filepath);
|
|
@@ -66,14 +82,14 @@ class FileAdaptor {
|
|
|
66
82
|
get exists() {
|
|
67
83
|
return this.file.exists;
|
|
68
84
|
}
|
|
69
|
-
get path() {
|
|
70
|
-
return this.file.path;
|
|
71
|
-
}
|
|
72
85
|
delete() {
|
|
73
86
|
this.file.delete();
|
|
74
87
|
}
|
|
88
|
+
get path() {
|
|
89
|
+
return this.file.path;
|
|
90
|
+
}
|
|
75
91
|
}
|
|
76
|
-
class JsonFile extends
|
|
92
|
+
class JsonFile extends Adaptor {
|
|
77
93
|
constructor(filepath, contents) {
|
|
78
94
|
super(filepath.endsWith('.json') ? filepath : filepath + '.json');
|
|
79
95
|
if (contents)
|
|
@@ -87,7 +103,7 @@ class JsonFile extends FileAdaptor {
|
|
|
87
103
|
this.file.write(JSON.stringify(snapshot(contents), null, 2));
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
|
-
class NdjsonFile extends
|
|
106
|
+
class NdjsonFile extends Adaptor {
|
|
91
107
|
constructor(filepath, lines) {
|
|
92
108
|
super(filepath.endsWith('.ndjson') ? filepath : filepath + '.ndjson');
|
|
93
109
|
if (lines)
|
|
@@ -100,3 +116,53 @@ class NdjsonFile extends FileAdaptor {
|
|
|
100
116
|
return this.file.lines().map((l) => JSON.parse(l));
|
|
101
117
|
}
|
|
102
118
|
}
|
|
119
|
+
class CsvFile extends Adaptor {
|
|
120
|
+
constructor(filepath) {
|
|
121
|
+
super(filepath.endsWith('.csv') ? filepath : filepath + '.csv');
|
|
122
|
+
}
|
|
123
|
+
async write(rows, keys) {
|
|
124
|
+
const headerSet = new Set();
|
|
125
|
+
if (keys) {
|
|
126
|
+
for (const key of keys)
|
|
127
|
+
headerSet.add(key);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
for (const row of rows) {
|
|
131
|
+
for (const key in row)
|
|
132
|
+
headerSet.add(key);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const headers = Array.from(headerSet);
|
|
136
|
+
const outRows = rows.map((row) => headers.map((key) => row[key]));
|
|
137
|
+
const contents = await writeToString([headers, ...outRows]);
|
|
138
|
+
this.file.write(contents);
|
|
139
|
+
}
|
|
140
|
+
async read() {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const parsed = [];
|
|
143
|
+
const content = this.file.read();
|
|
144
|
+
if (!content)
|
|
145
|
+
return resolve(parsed);
|
|
146
|
+
function parseVal(val) {
|
|
147
|
+
if (val.toLowerCase() === 'false')
|
|
148
|
+
return false;
|
|
149
|
+
if (val.toLowerCase() === 'true')
|
|
150
|
+
return true;
|
|
151
|
+
if (val.length === 0)
|
|
152
|
+
return null;
|
|
153
|
+
if (/^[\.0-9]+$/.test(val))
|
|
154
|
+
return Number(val);
|
|
155
|
+
return val;
|
|
156
|
+
}
|
|
157
|
+
parseString(content, { headers: true })
|
|
158
|
+
.on('error', (e) => reject(e))
|
|
159
|
+
.on('end', () => resolve(parsed))
|
|
160
|
+
.on('data', (raw) => {
|
|
161
|
+
parsed.push(Object.entries(raw).reduce((all, [key, val]) => ({
|
|
162
|
+
...all,
|
|
163
|
+
[key]: parseVal(val),
|
|
164
|
+
}), {}));
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
package/dist/Log.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { inspect } from 'util';
|
|
1
|
+
import { inspect } from 'node:util';
|
|
3
2
|
import { isObjectLike } from 'lodash-es';
|
|
4
3
|
import chalk from 'chalk';
|
|
5
4
|
import { snapshot } from './snapshot.js';
|
|
6
|
-
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
7
5
|
export class Log {
|
|
8
|
-
|
|
6
|
+
// Only silence logs when THIS package is running its own tests
|
|
7
|
+
static isTest = process.env.npm_package_name === '@brianbuie/node-kit' && process.env.npm_lifecycle_event === 'test';
|
|
9
8
|
/**
|
|
10
9
|
* Gcloud parses JSON in stdout
|
|
11
10
|
*/
|
package/dist/TypeWriter.js
CHANGED
package/dist/_index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Dir, TempDir, temp } from './Dir.js';
|
|
2
2
|
export { Cache } from './Cache.js';
|
|
3
|
-
export { Fetcher, type Route, Query, FetchOptions } from './Fetcher.js';
|
|
3
|
+
export { Fetcher, type Route, type Query, type FetchOptions } from './Fetcher.js';
|
|
4
4
|
export { File } from './File.js';
|
|
5
5
|
export { Jwt } from './Jwt.js';
|
|
6
6
|
export { Log } from './Log.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brianbuie/node-kit",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
},
|
|
14
14
|
"type": "module",
|
|
15
15
|
"exports": {
|
|
16
|
-
".":
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/_index.d.ts",
|
|
18
|
+
"default": "./dist/_index.js"
|
|
19
|
+
}
|
|
17
20
|
},
|
|
18
21
|
"files": [
|
|
19
22
|
"src",
|
|
@@ -27,6 +30,7 @@
|
|
|
27
30
|
"dependencies": {
|
|
28
31
|
"chalk": "^5.6.2",
|
|
29
32
|
"extract-domain": "^5.0.2",
|
|
33
|
+
"fast-csv": "^5.0.5",
|
|
30
34
|
"jsonwebtoken": "^9.0.2",
|
|
31
35
|
"lodash-es": "^4.17.21",
|
|
32
36
|
"quicktype-core": "^23.2.6",
|
package/src/Dir.ts
CHANGED
package/src/Fetcher.ts
CHANGED
|
@@ -25,12 +25,12 @@ export class Fetcher {
|
|
|
25
25
|
defaultOptions;
|
|
26
26
|
|
|
27
27
|
constructor(opts: FetchOptions = {}) {
|
|
28
|
-
|
|
28
|
+
this.defaultOptions = {
|
|
29
29
|
timeout: 60000,
|
|
30
30
|
retries: 0,
|
|
31
31
|
retryDelay: 3000,
|
|
32
|
+
...opts,
|
|
32
33
|
};
|
|
33
|
-
this.defaultOptions = merge(defaultOptions, opts);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -56,6 +56,14 @@ export class Fetcher {
|
|
|
56
56
|
return [url, domain];
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Merges options to get headers. Useful when extending the Fetcher class to add custom auth.
|
|
61
|
+
*/
|
|
62
|
+
buildHeaders(route: Route, opts: FetchOptions = {}) {
|
|
63
|
+
const { headers } = merge({}, this.defaultOptions, opts);
|
|
64
|
+
return headers || {};
|
|
65
|
+
}
|
|
66
|
+
|
|
59
67
|
/**
|
|
60
68
|
* Builds request, merging defaultOptions and provided options
|
|
61
69
|
* Includes Abort signal for timeout
|
|
@@ -63,8 +71,8 @@ export class Fetcher {
|
|
|
63
71
|
buildRequest(route: Route, opts: FetchOptions = {}): [Request, FetchOptions, string] {
|
|
64
72
|
const mergedOptions = merge({}, this.defaultOptions, opts);
|
|
65
73
|
const { query, data, timeout, retries, ...init } = mergedOptions;
|
|
74
|
+
init.headers = this.buildHeaders(route, mergedOptions);
|
|
66
75
|
if (data) {
|
|
67
|
-
init.headers = init.headers || {};
|
|
68
76
|
init.headers['content-type'] = init.headers['content-type'] || 'application/json';
|
|
69
77
|
init.method = init.method || 'POST';
|
|
70
78
|
init.body = JSON.stringify(data);
|
|
@@ -88,7 +96,6 @@ export class Fetcher {
|
|
|
88
96
|
let attempt = 0;
|
|
89
97
|
while (attempt < maxAttempts) {
|
|
90
98
|
attempt++;
|
|
91
|
-
// Rebuild request on every attempt to reset AbortSignal.timeout
|
|
92
99
|
const [req] = this.buildRequest(route, opts);
|
|
93
100
|
const res = await fetch(req)
|
|
94
101
|
.then((r) => {
|
package/src/File.test.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
-
import { isEqual } from 'lodash-es';
|
|
4
3
|
import { temp } from './Dir.js';
|
|
5
4
|
import { File } from './File.js';
|
|
6
5
|
|
|
@@ -11,14 +10,14 @@ const thing = {
|
|
|
11
10
|
a: 'string',
|
|
12
11
|
b: 2,
|
|
13
12
|
c: true,
|
|
14
|
-
d:
|
|
13
|
+
d: false,
|
|
14
|
+
e: null,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
describe('FileAdaptor', () => {
|
|
18
18
|
it('Creates instances', () => {
|
|
19
19
|
const test1 = new File.Adaptor(testDir.filepath('test1.txt'));
|
|
20
20
|
assert(test1.file.path.includes('test1.txt'));
|
|
21
|
-
|
|
22
21
|
const base = 'test2';
|
|
23
22
|
const eg1 = new File.json(testDir.filepath(base));
|
|
24
23
|
const eg2 = testDir.file(base).json();
|
|
@@ -38,13 +37,14 @@ describe('FileAdaptor', () => {
|
|
|
38
37
|
|
|
39
38
|
describe('File.ndjson', () => {
|
|
40
39
|
it('Appends new lines correctly', () => {
|
|
41
|
-
const file = testDir.file('
|
|
40
|
+
const file = testDir.file('appends-lines').ndjson();
|
|
41
|
+
file.delete();
|
|
42
42
|
file.append([thing, thing]);
|
|
43
43
|
assert(file.lines().length === 2);
|
|
44
44
|
file.append(thing);
|
|
45
45
|
assert(file.lines().length === 3);
|
|
46
46
|
file.lines().forEach((line) => {
|
|
47
|
-
assert(
|
|
47
|
+
assert.deepStrictEqual(line, thing);
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
50
|
|
|
@@ -60,9 +60,9 @@ describe('File.ndjson', () => {
|
|
|
60
60
|
describe('File.json', () => {
|
|
61
61
|
it('Saves data as json', () => {
|
|
62
62
|
const file = testDir.file('jsonfile-data').json(thing);
|
|
63
|
-
assert(
|
|
63
|
+
assert.deepStrictEqual(file.read(), thing);
|
|
64
64
|
file.write(thing);
|
|
65
|
-
assert(
|
|
65
|
+
assert.deepStrictEqual(file.read(), thing);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
it('Does not create file when reading', () => {
|
|
@@ -72,3 +72,14 @@ describe('File.json', () => {
|
|
|
72
72
|
assert(!file.exists);
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
|
+
|
|
76
|
+
describe('File.csv', () => {
|
|
77
|
+
it('Saves data as csv', async () => {
|
|
78
|
+
const things = [thing, thing, thing];
|
|
79
|
+
const file = await testDir.file('csv-data').csv(things);
|
|
80
|
+
const parsed = await file.read();
|
|
81
|
+
parsed.forEach((row) => {
|
|
82
|
+
assert.deepEqual(row, thing);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
package/src/File.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { writeToString, parseString } from 'fast-csv';
|
|
3
4
|
import { snapshot } from './snapshot.js';
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* WARNING: API will change!
|
|
8
|
+
*/
|
|
5
9
|
export class File {
|
|
6
10
|
path;
|
|
7
11
|
|
|
@@ -13,6 +17,14 @@ export class File {
|
|
|
13
17
|
return fs.existsSync(this.path);
|
|
14
18
|
}
|
|
15
19
|
|
|
20
|
+
createWriteStream(options: Parameters<typeof fs.createWriteStream>[1] = {}) {
|
|
21
|
+
return fs.createWriteStream(this.path, options);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
delete() {
|
|
25
|
+
fs.rmSync(this.path, { force: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
read() {
|
|
17
29
|
return this.exists ? fs.readFileSync(this.path, 'utf8') : undefined;
|
|
18
30
|
}
|
|
@@ -40,12 +52,8 @@ export class File {
|
|
|
40
52
|
return contents.slice(0, contents.length - 1);
|
|
41
53
|
}
|
|
42
54
|
|
|
43
|
-
delete() {
|
|
44
|
-
fs.rmSync(this.path, { force: true });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
55
|
static get Adaptor() {
|
|
48
|
-
return
|
|
56
|
+
return Adaptor;
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
json<T>(contents?: T) {
|
|
@@ -56,16 +64,26 @@ export class File {
|
|
|
56
64
|
return JsonFile;
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
ndjson<T>(lines?: T | T[]) {
|
|
67
|
+
ndjson<T extends object>(lines?: T | T[]) {
|
|
60
68
|
return new NdjsonFile<T>(this.path, lines);
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
static get ndjson() {
|
|
64
72
|
return NdjsonFile;
|
|
65
73
|
}
|
|
74
|
+
|
|
75
|
+
async csv<T extends object>(rows?: T[], keys?: (keyof T)[]) {
|
|
76
|
+
const csvFile = new CsvFile<T>(this.path);
|
|
77
|
+
if (rows) await csvFile.write(rows, keys);
|
|
78
|
+
return csvFile;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static get csv() {
|
|
82
|
+
return CsvFile;
|
|
83
|
+
}
|
|
66
84
|
}
|
|
67
85
|
|
|
68
|
-
class
|
|
86
|
+
class Adaptor<T = string> {
|
|
69
87
|
file;
|
|
70
88
|
|
|
71
89
|
constructor(filepath: string, contents?: T) {
|
|
@@ -82,16 +100,16 @@ class FileAdaptor<T = string> {
|
|
|
82
100
|
return this.file.exists;
|
|
83
101
|
}
|
|
84
102
|
|
|
85
|
-
get path() {
|
|
86
|
-
return this.file.path;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
103
|
delete() {
|
|
90
104
|
this.file.delete();
|
|
91
105
|
}
|
|
106
|
+
|
|
107
|
+
get path() {
|
|
108
|
+
return this.file.path;
|
|
109
|
+
}
|
|
92
110
|
}
|
|
93
111
|
|
|
94
|
-
class JsonFile<T> extends
|
|
112
|
+
class JsonFile<T> extends Adaptor {
|
|
95
113
|
constructor(filepath: string, contents?: T) {
|
|
96
114
|
super(filepath.endsWith('.json') ? filepath : filepath + '.json');
|
|
97
115
|
if (contents) this.write(contents);
|
|
@@ -107,7 +125,7 @@ class JsonFile<T> extends FileAdaptor {
|
|
|
107
125
|
}
|
|
108
126
|
}
|
|
109
127
|
|
|
110
|
-
class NdjsonFile<T> extends
|
|
128
|
+
class NdjsonFile<T extends object> extends Adaptor {
|
|
111
129
|
constructor(filepath: string, lines?: T | T[]) {
|
|
112
130
|
super(filepath.endsWith('.ndjson') ? filepath : filepath + '.ndjson');
|
|
113
131
|
if (lines) this.append(lines);
|
|
@@ -123,3 +141,55 @@ class NdjsonFile<T> extends FileAdaptor {
|
|
|
123
141
|
return this.file.lines().map((l) => JSON.parse(l) as T);
|
|
124
142
|
}
|
|
125
143
|
}
|
|
144
|
+
|
|
145
|
+
type Key<T extends object> = keyof T;
|
|
146
|
+
|
|
147
|
+
class CsvFile<Row extends object> extends Adaptor {
|
|
148
|
+
constructor(filepath: string) {
|
|
149
|
+
super(filepath.endsWith('.csv') ? filepath : filepath + '.csv');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async write(rows: Row[], keys?: Key<Row>[]) {
|
|
153
|
+
const headerSet = new Set<Key<Row>>();
|
|
154
|
+
if (keys) {
|
|
155
|
+
for (const key of keys) headerSet.add(key);
|
|
156
|
+
} else {
|
|
157
|
+
for (const row of rows) {
|
|
158
|
+
for (const key in row) headerSet.add(key);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const headers = Array.from(headerSet);
|
|
162
|
+
const outRows = rows.map((row) => headers.map((key) => row[key]));
|
|
163
|
+
const contents = await writeToString([headers, ...outRows]);
|
|
164
|
+
this.file.write(contents);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async read() {
|
|
168
|
+
return new Promise<Row[]>((resolve, reject) => {
|
|
169
|
+
const parsed: Row[] = [];
|
|
170
|
+
const content = this.file.read();
|
|
171
|
+
if (!content) return resolve(parsed);
|
|
172
|
+
function parseVal(val: string) {
|
|
173
|
+
if (val.toLowerCase() === 'false') return false;
|
|
174
|
+
if (val.toLowerCase() === 'true') return true;
|
|
175
|
+
if (val.length === 0) return null;
|
|
176
|
+
if (/^[\.0-9]+$/.test(val)) return Number(val);
|
|
177
|
+
return val;
|
|
178
|
+
}
|
|
179
|
+
parseString(content, { headers: true })
|
|
180
|
+
.on('error', (e) => reject(e))
|
|
181
|
+
.on('end', () => resolve(parsed))
|
|
182
|
+
.on('data', (raw: Record<Key<Row>, string>) => {
|
|
183
|
+
parsed.push(
|
|
184
|
+
Object.entries(raw).reduce(
|
|
185
|
+
(all, [key, val]) => ({
|
|
186
|
+
...all,
|
|
187
|
+
[key]: parseVal(val as string),
|
|
188
|
+
}),
|
|
189
|
+
{} as Row
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/Log.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { inspect } from 'util';
|
|
1
|
+
import { inspect } from 'node:util';
|
|
3
2
|
import { isObjectLike } from 'lodash-es';
|
|
4
3
|
import chalk, { type ChalkInstance } from 'chalk';
|
|
5
4
|
import { snapshot } from './snapshot.js';
|
|
6
5
|
|
|
7
|
-
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
8
|
-
|
|
9
6
|
type Severity = 'DEFAULT' | 'DEBUG' | 'INFO' | 'NOTICE' | 'WARNING' | 'ERROR' | 'CRITICAL' | 'ALERT' | 'EMERGENCY';
|
|
10
7
|
|
|
11
8
|
type Options = {
|
|
@@ -20,7 +17,8 @@ type Entry = {
|
|
|
20
17
|
};
|
|
21
18
|
|
|
22
19
|
export class Log {
|
|
23
|
-
|
|
20
|
+
// Only silence logs when THIS package is running its own tests
|
|
21
|
+
static isTest = process.env.npm_package_name === '@brianbuie/node-kit' && process.env.npm_lifecycle_event === 'test';
|
|
24
22
|
|
|
25
23
|
/**
|
|
26
24
|
* Gcloud parses JSON in stdout
|
package/src/TypeWriter.ts
CHANGED
package/src/_index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Dir, TempDir, temp } from './Dir.js';
|
|
2
2
|
export { Cache } from './Cache.js';
|
|
3
|
-
export { Fetcher, type Route, Query, FetchOptions } from './Fetcher.js';
|
|
3
|
+
export { Fetcher, type Route, type Query, type FetchOptions } from './Fetcher.js';
|
|
4
4
|
export { File } from './File.js';
|
|
5
5
|
export { Jwt } from './Jwt.js';
|
|
6
6
|
export { Log } from './Log.js';
|