@brianbuie/node-kit 0.0.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/.dist/Fetcher.d.ts +41 -0
- package/.dist/Fetcher.js +70 -0
- package/.dist/_index.d.ts +1 -0
- package/.dist/_index.js +1 -0
- package/README.md +28 -0
- package/package.json +35 -0
- package/src/Fetcher.test.ts +48 -0
- package/src/Fetcher.ts +91 -0
- package/src/_index.ts +1 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type Route = string | URL;
|
|
2
|
+
export type Query = Record<string, string | number | boolean | undefined>;
|
|
3
|
+
export type FetchOptions = RequestInit & {
|
|
4
|
+
base?: string;
|
|
5
|
+
query?: Query;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
data?: any;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
retries?: number;
|
|
10
|
+
retryDelay?: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Fetcher provides a quick way to set up a basic API connection
|
|
14
|
+
* with options applied to every request
|
|
15
|
+
* Includes basic methods for requesting and parsing responses
|
|
16
|
+
*/
|
|
17
|
+
export declare class Fetcher {
|
|
18
|
+
defaultOptions: {
|
|
19
|
+
timeout: number;
|
|
20
|
+
retries: number;
|
|
21
|
+
retryDelay: number;
|
|
22
|
+
} & RequestInit & {
|
|
23
|
+
base?: string;
|
|
24
|
+
query?: Query;
|
|
25
|
+
headers?: Record<string, string>;
|
|
26
|
+
data?: any;
|
|
27
|
+
timeout?: number;
|
|
28
|
+
retries?: number;
|
|
29
|
+
retryDelay?: number;
|
|
30
|
+
};
|
|
31
|
+
constructor(opts?: FetchOptions);
|
|
32
|
+
makeUrl(route: Route, query?: Query): URL;
|
|
33
|
+
/**
|
|
34
|
+
* Builds and performs the request, merging provided options with defaultOptions
|
|
35
|
+
* If `opts.data` is provided, method is updated to POST, content-type json, data is stringified in the body
|
|
36
|
+
* Retries on local or network error, with increasing backoff
|
|
37
|
+
*/
|
|
38
|
+
fetch(route: Route, opts?: FetchOptions): Promise<[Response, Request]>;
|
|
39
|
+
fetchText(route: Route, opts?: FetchOptions): Promise<[string, Response, Request]>;
|
|
40
|
+
fetchJson<T>(route: Route, opts?: FetchOptions): Promise<[T, Response, Request]>;
|
|
41
|
+
}
|
package/.dist/Fetcher.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { merge } from 'lodash-es';
|
|
2
|
+
/**
|
|
3
|
+
* Fetcher provides a quick way to set up a basic API connection
|
|
4
|
+
* with options applied to every request
|
|
5
|
+
* Includes basic methods for requesting and parsing responses
|
|
6
|
+
*/
|
|
7
|
+
export class Fetcher {
|
|
8
|
+
defaultOptions;
|
|
9
|
+
constructor(opts = {}) {
|
|
10
|
+
const defaultOptions = {
|
|
11
|
+
timeout: 60000,
|
|
12
|
+
retries: 3,
|
|
13
|
+
retryDelay: 3000,
|
|
14
|
+
};
|
|
15
|
+
this.defaultOptions = merge(defaultOptions, opts);
|
|
16
|
+
}
|
|
17
|
+
makeUrl(route, query = {}) {
|
|
18
|
+
const params = new URLSearchParams(Object.entries(query)
|
|
19
|
+
.filter(([_k, val]) => Boolean(val))
|
|
20
|
+
.map(([key, val]) => [key, `${val}`])).toString();
|
|
21
|
+
const search = params ? '?' + params : '';
|
|
22
|
+
return new URL(route + search, this.defaultOptions.base);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Builds and performs the request, merging provided options with defaultOptions
|
|
26
|
+
* If `opts.data` is provided, method is updated to POST, content-type json, data is stringified in the body
|
|
27
|
+
* Retries on local or network error, with increasing backoff
|
|
28
|
+
*/
|
|
29
|
+
async fetch(route, opts = {}) {
|
|
30
|
+
const { query, data, timeout, retries, ...o } = merge({}, this.defaultOptions, opts);
|
|
31
|
+
if (data) {
|
|
32
|
+
o.headers = o.headers || {};
|
|
33
|
+
o.headers['content-type'] = o.headers['content-type'] || 'application/json';
|
|
34
|
+
o.method = o.method || 'POST';
|
|
35
|
+
o.body = JSON.stringify(data);
|
|
36
|
+
}
|
|
37
|
+
const url = this.makeUrl(route, query);
|
|
38
|
+
const attempts = retries + 1;
|
|
39
|
+
let attempted = 0;
|
|
40
|
+
while (attempted < attempts) {
|
|
41
|
+
attempted++;
|
|
42
|
+
const req = new Request(url, { ...o, signal: AbortSignal.timeout(timeout) });
|
|
43
|
+
const res = await fetch(req)
|
|
44
|
+
.then(r => {
|
|
45
|
+
if (!r.ok)
|
|
46
|
+
throw new Error(r.statusText);
|
|
47
|
+
return r;
|
|
48
|
+
})
|
|
49
|
+
.catch(async (error) => {
|
|
50
|
+
if (attempted < attempts) {
|
|
51
|
+
const wait = attempted * 3000;
|
|
52
|
+
console.warn(`Fetcher (attempt ${attempted} of ${attempts})`, error);
|
|
53
|
+
await new Promise(resolve => setTimeout(resolve, wait));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
if (res)
|
|
57
|
+
return [res, req];
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Failed to fetch ${url.href}`);
|
|
60
|
+
}
|
|
61
|
+
async fetchText(route, opts = {}) {
|
|
62
|
+
return this.fetch(route, opts).then(async ([res, req]) => {
|
|
63
|
+
const text = await res.text();
|
|
64
|
+
return [text, res, req];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async fetchJson(route, opts = {}) {
|
|
68
|
+
return this.fetchText(route, opts).then(([txt, res, req]) => [JSON.parse(txt), res, req]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Fetcher, type Route, Query, FetchOptions } from './Fetcher.js';
|
package/.dist/_index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Fetcher } from './Fetcher.js';
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Node Kit
|
|
2
|
+
|
|
3
|
+
Basic tools for quick node.js projects
|
|
4
|
+
|
|
5
|
+
## Using in other projects
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm add @brianbuie/node-kit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { thing } from '@brianbuie/node-kit';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Publishing changes to this package
|
|
16
|
+
|
|
17
|
+
Commit all changes, then run
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
npm version [patch|minor|major] [-m "custom commit message"]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- Bumps version in `package.json`
|
|
24
|
+
- Runs tests
|
|
25
|
+
- Commits changes
|
|
26
|
+
- Tags commit with new version
|
|
27
|
+
- Pushes commit and tags to github
|
|
28
|
+
- The new tag will trigger github action to publish to npm
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brianbuie/node-kit",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"license": "ISC",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/brianbuie/node-kit.git"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "tsc && node --test \".dist/**/*.test.js\"",
|
|
11
|
+
"preversion": "npm test",
|
|
12
|
+
"postversion": "git push --follow-tags"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./.dist/_index.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
".dist",
|
|
21
|
+
"!.dist/**/*.test.*",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=24"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@types/lodash-es": "^4.17.12",
|
|
29
|
+
"@types/node": "^24.9.1",
|
|
30
|
+
"lodash-es": "^4.17.21"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"typescript": "^5.9.3"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { Fetcher } from './Fetcher.js';
|
|
4
|
+
|
|
5
|
+
describe('Fetcher', () => {
|
|
6
|
+
const statusApi = new Fetcher({ base: 'https://mock.httpstatus.io' });
|
|
7
|
+
|
|
8
|
+
it('should make URL', () => {
|
|
9
|
+
const route = '/example/route';
|
|
10
|
+
const url = statusApi.makeUrl(route);
|
|
11
|
+
assert(url.href.includes(route));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should throw when no base', () => {
|
|
15
|
+
try {
|
|
16
|
+
const empty = new Fetcher();
|
|
17
|
+
empty.makeUrl('/');
|
|
18
|
+
} catch (e) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
throw new Error('Ignored invalid URL');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should handle query parameters', () => {
|
|
25
|
+
const url = statusApi.makeUrl('/', { key: 'value' });
|
|
26
|
+
assert(url.href.includes('?key=value'));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should handle undefined query params', () => {
|
|
30
|
+
const url = statusApi.makeUrl('/', { key: undefined });
|
|
31
|
+
assert(!url.href.includes('key'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should handle no query params', () => {
|
|
35
|
+
const route = '/';
|
|
36
|
+
const url = statusApi.makeUrl(route);
|
|
37
|
+
assert(url.href === statusApi.defaultOptions.base + route);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should throw on bad request', async () => {
|
|
41
|
+
try {
|
|
42
|
+
await statusApi.fetch('/404', { retries: 0 });
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
throw new Error('Ignored bad request');
|
|
47
|
+
});
|
|
48
|
+
});
|
package/src/Fetcher.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { merge } from 'lodash-es';
|
|
2
|
+
|
|
3
|
+
export type Route = string | URL;
|
|
4
|
+
|
|
5
|
+
export type Query = Record<string, string | number | boolean | undefined>;
|
|
6
|
+
|
|
7
|
+
export type FetchOptions = RequestInit & {
|
|
8
|
+
base?: string;
|
|
9
|
+
query?: Query;
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
data?: any;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
retries?: number;
|
|
14
|
+
retryDelay?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fetcher provides a quick way to set up a basic API connection
|
|
19
|
+
* with options applied to every request
|
|
20
|
+
* Includes basic methods for requesting and parsing responses
|
|
21
|
+
*/
|
|
22
|
+
export class Fetcher {
|
|
23
|
+
defaultOptions;
|
|
24
|
+
|
|
25
|
+
constructor(opts: FetchOptions = {}) {
|
|
26
|
+
const defaultOptions = {
|
|
27
|
+
timeout: 60000,
|
|
28
|
+
retries: 3,
|
|
29
|
+
retryDelay: 3000,
|
|
30
|
+
};
|
|
31
|
+
this.defaultOptions = merge(defaultOptions, opts);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
makeUrl(route: Route, query: Query = {}) {
|
|
35
|
+
const params = new URLSearchParams(
|
|
36
|
+
Object.entries(query)
|
|
37
|
+
.filter(([_k, val]) => Boolean(val))
|
|
38
|
+
.map(([key, val]) => [key, `${val}`])
|
|
39
|
+
).toString();
|
|
40
|
+
const search = params ? '?' + params : '';
|
|
41
|
+
return new URL(route + search, this.defaultOptions.base);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Builds and performs the request, merging provided options with defaultOptions
|
|
46
|
+
* If `opts.data` is provided, method is updated to POST, content-type json, data is stringified in the body
|
|
47
|
+
* Retries on local or network error, with increasing backoff
|
|
48
|
+
*/
|
|
49
|
+
async fetch(route: Route, opts: FetchOptions = {}): Promise<[Response, Request]> {
|
|
50
|
+
const { query, data, timeout, retries, ...o } = merge({}, this.defaultOptions, opts);
|
|
51
|
+
if (data) {
|
|
52
|
+
o.headers = o.headers || {};
|
|
53
|
+
o.headers['content-type'] = o.headers['content-type'] || 'application/json';
|
|
54
|
+
o.method = o.method || 'POST';
|
|
55
|
+
o.body = JSON.stringify(data);
|
|
56
|
+
}
|
|
57
|
+
const url = this.makeUrl(route, query);
|
|
58
|
+
|
|
59
|
+
const attempts = retries + 1;
|
|
60
|
+
let attempted = 0;
|
|
61
|
+
while (attempted < attempts) {
|
|
62
|
+
attempted++;
|
|
63
|
+
const req = new Request(url, { ...o, signal: AbortSignal.timeout(timeout) });
|
|
64
|
+
const res = await fetch(req)
|
|
65
|
+
.then(r => {
|
|
66
|
+
if (!r.ok) throw new Error(r.statusText);
|
|
67
|
+
return r;
|
|
68
|
+
})
|
|
69
|
+
.catch(async error => {
|
|
70
|
+
if (attempted < attempts) {
|
|
71
|
+
const wait = attempted * 3000;
|
|
72
|
+
console.warn(`Fetcher (attempt ${attempted} of ${attempts})`, error);
|
|
73
|
+
await new Promise(resolve => setTimeout(resolve, wait));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
if (res) return [res, req];
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Failed to fetch ${url.href}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async fetchText(route: Route, opts: FetchOptions = {}): Promise<[string, Response, Request]> {
|
|
82
|
+
return this.fetch(route, opts).then(async ([res, req]) => {
|
|
83
|
+
const text = await res.text();
|
|
84
|
+
return [text, res, req];
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async fetchJson<T>(route: Route, opts: FetchOptions = {}): Promise<[T, Response, Request]> {
|
|
89
|
+
return this.fetchText(route, opts).then(([txt, res, req]) => [JSON.parse(txt) as T, res, req]);
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/_index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Fetcher, type Route, Query, FetchOptions } from './Fetcher.js';
|