@dungarees/rest 0.11.4
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/html.d.ts +4 -0
- package/html.js +22 -0
- package/html.test.d.ts +1 -0
- package/html.test.js +53 -0
- package/json.d.ts +5 -0
- package/json.js +16 -0
- package/json.test.d.ts +1 -0
- package/json.test.js +60 -0
- package/package.json +471 -0
- package/service.d.ts +3 -0
- package/service.js +38 -0
- package/service.test.d.ts +1 -0
- package/service.test.js +152 -0
- package/stub.d.ts +4 -0
- package/stub.js +19 -0
- package/stub.test.d.ts +1 -0
- package/stub.test.js +162 -0
- package/test-server.d.ts +19 -0
- package/test-server.js +42 -0
- package/type.d.ts +32 -0
- package/type.js +1 -0
package/html.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Fetcher, RestClient, RestEndpoint, RestEndpointRequest } from './type.ts';
|
|
2
|
+
export declare const htmlFetcher: Fetcher<string | Error>;
|
|
3
|
+
export declare const createHtmlRestClient: any;
|
|
4
|
+
export type HtmlRestClient<API extends RestEndpoint<RestEndpointRequest, string | Error>> = RestClient<Fetcher<string | Error>, API>;
|
package/html.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createRestClientCreator } from './service.js';
|
|
2
|
+
export const htmlFetcher = async (url, request) => {
|
|
3
|
+
const response = await fetch(url, {
|
|
4
|
+
...request,
|
|
5
|
+
headers: {
|
|
6
|
+
...request.headers,
|
|
7
|
+
...(request.body !== undefined && {
|
|
8
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
9
|
+
}),
|
|
10
|
+
},
|
|
11
|
+
body: request.body !== undefined ? formEncode(request.body) : null,
|
|
12
|
+
});
|
|
13
|
+
return await response.text();
|
|
14
|
+
};
|
|
15
|
+
// URLSearchParams rather than node:querystring, so this works in a browser as well as in node.
|
|
16
|
+
const formEncode = (body) => {
|
|
17
|
+
if (typeof body !== 'object' || body === null) {
|
|
18
|
+
throw new Error(`A form encoded body must be an object, got: ${String(body)}`);
|
|
19
|
+
}
|
|
20
|
+
return new URLSearchParams(Object.entries(body).map(([name, value]) => [name, String(value)])).toString();
|
|
21
|
+
};
|
|
22
|
+
export const createHtmlRestClient = createRestClientCreator(htmlFetcher);
|
package/html.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/html.test.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHtmlRestClient, htmlFetcher } from './html.js';
|
|
2
|
+
import { startTestServer } from './test-server.js';
|
|
3
|
+
import { firstValueFrom } from 'rxjs';
|
|
4
|
+
import { afterEach, expect, test } from 'vitest';
|
|
5
|
+
let server;
|
|
6
|
+
afterEach(async () => {
|
|
7
|
+
await server?.close();
|
|
8
|
+
server = undefined;
|
|
9
|
+
});
|
|
10
|
+
const HTML_RESPONSE = { body: '<h1>Hello</h1>', contentType: 'text/html' };
|
|
11
|
+
test('htmlFetcher hands back the response body as text', async () => {
|
|
12
|
+
server = await startTestServer({ respond: () => HTML_RESPONSE });
|
|
13
|
+
expect(await htmlFetcher(server.baseUrl, { method: 'GET', headers: {}, body: undefined })).toBe('<h1>Hello</h1>');
|
|
14
|
+
});
|
|
15
|
+
test('htmlFetcher form-encodes the request body', async () => {
|
|
16
|
+
server = await startTestServer({ respond: () => HTML_RESPONSE });
|
|
17
|
+
await htmlFetcher(server.baseUrl, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: {},
|
|
20
|
+
body: { a: '1', b: 'two words' },
|
|
21
|
+
});
|
|
22
|
+
expect(server.received[0]?.body).toBe('a=1&b=two+words');
|
|
23
|
+
});
|
|
24
|
+
test('htmlFetcher declares a form content type when it sends a body', async () => {
|
|
25
|
+
server = await startTestServer({ respond: () => HTML_RESPONSE });
|
|
26
|
+
await htmlFetcher(server.baseUrl, { method: 'POST', headers: {}, body: { a: '1' } });
|
|
27
|
+
expect(server.received[0]?.headers['content-type']).toBe('application/x-www-form-urlencoded');
|
|
28
|
+
});
|
|
29
|
+
test('htmlFetcher sends no content type when there is no body', async () => {
|
|
30
|
+
server = await startTestServer({ respond: () => HTML_RESPONSE });
|
|
31
|
+
await htmlFetcher(server.baseUrl, { method: 'GET', headers: {}, body: undefined });
|
|
32
|
+
expect(server.received[0]?.headers['content-type']).toBe(undefined);
|
|
33
|
+
});
|
|
34
|
+
test('htmlFetcher passes the headers it was given through', async () => {
|
|
35
|
+
server = await startTestServer({ respond: () => HTML_RESPONSE });
|
|
36
|
+
await htmlFetcher(server.baseUrl, {
|
|
37
|
+
method: 'GET',
|
|
38
|
+
headers: { 'header-1': 'value-1' },
|
|
39
|
+
body: undefined,
|
|
40
|
+
});
|
|
41
|
+
expect(server.received[0]?.headers['header-1']).toBe('value-1');
|
|
42
|
+
});
|
|
43
|
+
test('htmlFetcher hands back the body of an error response rather than throwing', async () => {
|
|
44
|
+
server = await startTestServer({
|
|
45
|
+
respond: () => ({ status: 404, contentType: 'text/html', body: '<h1>Not found</h1>' }),
|
|
46
|
+
});
|
|
47
|
+
expect(await htmlFetcher(server.baseUrl, { method: 'GET', headers: {}, body: undefined })).toBe('<h1>Not found</h1>');
|
|
48
|
+
});
|
|
49
|
+
test('an html rest client fetches through to a real server', async () => {
|
|
50
|
+
server = await startTestServer({ respond: () => HTML_RESPONSE });
|
|
51
|
+
const client = createHtmlRestClient(server.baseUrl);
|
|
52
|
+
expect(await firstValueFrom(client({ method: 'GET', pathname: '/page' }))).toBe('<h1>Hello</h1>');
|
|
53
|
+
});
|
package/json.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Fetcher, RestClient, RestEndpoint, RestEndpointRequest } from './type.ts';
|
|
2
|
+
import type { JsonType } from '@dungarees/core/type-util.ts';
|
|
3
|
+
export declare const jsonFetcher: Fetcher<JsonType | Error>;
|
|
4
|
+
export declare const createJsonRestClient: any;
|
|
5
|
+
export type JsonRestClient<API extends RestEndpoint<RestEndpointRequest, JsonType | Error>> = RestClient<Fetcher<JsonType | Error>, API>;
|
package/json.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createRestClientCreator } from './service.js';
|
|
2
|
+
export const jsonFetcher = async (url, request) => {
|
|
3
|
+
const response = await fetch(url, {
|
|
4
|
+
...request,
|
|
5
|
+
headers: {
|
|
6
|
+
...request.headers,
|
|
7
|
+
...(request.body !== undefined && { 'Content-Type': 'application/json' }),
|
|
8
|
+
},
|
|
9
|
+
body: request.body !== undefined ? JSON.stringify(request.body) : null,
|
|
10
|
+
});
|
|
11
|
+
const parsed = await response.json();
|
|
12
|
+
// response.json() is typed `any`. This is the boundary at which the API type declares the shape
|
|
13
|
+
// of the response, and nothing here can check it against that declaration.
|
|
14
|
+
return parsed;
|
|
15
|
+
};
|
|
16
|
+
export const createJsonRestClient = createRestClientCreator(jsonFetcher);
|
package/json.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/json.test.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createJsonRestClient, jsonFetcher } from './json.js';
|
|
2
|
+
import { startTestServer } from './test-server.js';
|
|
3
|
+
import { firstValueFrom } from 'rxjs';
|
|
4
|
+
import { afterEach, expect, test } from 'vitest';
|
|
5
|
+
let server;
|
|
6
|
+
afterEach(async () => {
|
|
7
|
+
await server?.close();
|
|
8
|
+
server = undefined;
|
|
9
|
+
});
|
|
10
|
+
test('jsonFetcher parses the JSON body the server sent back', async () => {
|
|
11
|
+
server = await startTestServer({ respond: () => ({ body: '{"a":3}' }) });
|
|
12
|
+
expect(await jsonFetcher(server.baseUrl, { method: 'GET', headers: {}, body: undefined })).toEqual({ a: 3 });
|
|
13
|
+
});
|
|
14
|
+
test('jsonFetcher sends the request body as JSON', async () => {
|
|
15
|
+
server = await startTestServer();
|
|
16
|
+
await jsonFetcher(server.baseUrl, { method: 'POST', headers: {}, body: { a: 1 } });
|
|
17
|
+
expect(server.received[0]?.body).toBe('{"a":1}');
|
|
18
|
+
});
|
|
19
|
+
test('jsonFetcher declares a JSON content type when it sends a body', async () => {
|
|
20
|
+
server = await startTestServer();
|
|
21
|
+
await jsonFetcher(server.baseUrl, { method: 'POST', headers: {}, body: { a: 1 } });
|
|
22
|
+
expect(server.received[0]?.headers['content-type']).toBe('application/json');
|
|
23
|
+
});
|
|
24
|
+
test('jsonFetcher sends no content type when there is no body', async () => {
|
|
25
|
+
server = await startTestServer();
|
|
26
|
+
await jsonFetcher(server.baseUrl, { method: 'GET', headers: {}, body: undefined });
|
|
27
|
+
expect(server.received[0]?.headers['content-type']).toBe(undefined);
|
|
28
|
+
});
|
|
29
|
+
test('jsonFetcher passes the headers it was given through', async () => {
|
|
30
|
+
server = await startTestServer();
|
|
31
|
+
await jsonFetcher(server.baseUrl, {
|
|
32
|
+
method: 'GET',
|
|
33
|
+
headers: { 'header-1': 'value-1' },
|
|
34
|
+
body: undefined,
|
|
35
|
+
});
|
|
36
|
+
expect(server.received[0]?.headers['header-1']).toBe('value-1');
|
|
37
|
+
});
|
|
38
|
+
test('jsonFetcher sends the method it was given', async () => {
|
|
39
|
+
server = await startTestServer();
|
|
40
|
+
await jsonFetcher(server.baseUrl, { method: 'DELETE', headers: {}, body: undefined });
|
|
41
|
+
expect(server.received[0]?.method).toBe('DELETE');
|
|
42
|
+
});
|
|
43
|
+
test('jsonFetcher parses the body of an error response rather than throwing', async () => {
|
|
44
|
+
server = await startTestServer({
|
|
45
|
+
respond: () => ({ status: 500, body: '{"error":"boom"}' }),
|
|
46
|
+
});
|
|
47
|
+
expect(await jsonFetcher(server.baseUrl, { method: 'GET', headers: {}, body: undefined })).toEqual({ error: 'boom' });
|
|
48
|
+
});
|
|
49
|
+
test('a json rest client fetches through to a real server', async () => {
|
|
50
|
+
server = await startTestServer({ respond: () => ({ body: '{"a":3}' }) });
|
|
51
|
+
const client = createJsonRestClient(server.baseUrl);
|
|
52
|
+
expect(await firstValueFrom(client({ method: 'GET', pathname: '/path' }))).toEqual({ a: 3 });
|
|
53
|
+
});
|
|
54
|
+
test('a json rest client puts the pathname and search on the wire', async () => {
|
|
55
|
+
server = await startTestServer();
|
|
56
|
+
const client = createJsonRestClient(server.baseUrl);
|
|
57
|
+
const search = { a: 1 };
|
|
58
|
+
await firstValueFrom(client({ method: 'GET', pathname: '/path', search }));
|
|
59
|
+
expect(server.received[0]?.url).toBe('/path?a=1');
|
|
60
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dungarees/rest",
|
|
3
|
+
"engines": {
|
|
4
|
+
"node": ">=25.0.0"
|
|
5
|
+
},
|
|
6
|
+
"scripts": {
|
|
7
|
+
"type-check": "tsc --noEmit"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@dungarees/core": "*",
|
|
12
|
+
"rxjs": "^7.8.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5.9.3",
|
|
16
|
+
"vitest": "^3.0.2"
|
|
17
|
+
},
|
|
18
|
+
"author": "info@productkind.com",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"version": "0.11.4",
|
|
21
|
+
"exports": {
|
|
22
|
+
"./html.test.ts": {
|
|
23
|
+
"import": "./html.test.js",
|
|
24
|
+
"types": "./html.test.d.ts"
|
|
25
|
+
},
|
|
26
|
+
"./html.ts": {
|
|
27
|
+
"import": "./html.js",
|
|
28
|
+
"types": "./html.d.ts"
|
|
29
|
+
},
|
|
30
|
+
"./json.test.ts": {
|
|
31
|
+
"import": "./json.test.js",
|
|
32
|
+
"types": "./json.test.d.ts"
|
|
33
|
+
},
|
|
34
|
+
"./json.ts": {
|
|
35
|
+
"import": "./json.js",
|
|
36
|
+
"types": "./json.d.ts"
|
|
37
|
+
},
|
|
38
|
+
"./node_modules/typescript/lib/lib.d.ts": {
|
|
39
|
+
"import": "./node_modules/typescript/lib/lib.d.js",
|
|
40
|
+
"types": "./node_modules/typescript/lib/lib.d.d.ts"
|
|
41
|
+
},
|
|
42
|
+
"./node_modules/typescript/lib/lib.decorators.d.ts": {
|
|
43
|
+
"import": "./node_modules/typescript/lib/lib.decorators.d.js",
|
|
44
|
+
"types": "./node_modules/typescript/lib/lib.decorators.d.d.ts"
|
|
45
|
+
},
|
|
46
|
+
"./node_modules/typescript/lib/lib.decorators.legacy.d.ts": {
|
|
47
|
+
"import": "./node_modules/typescript/lib/lib.decorators.legacy.d.js",
|
|
48
|
+
"types": "./node_modules/typescript/lib/lib.decorators.legacy.d.d.ts"
|
|
49
|
+
},
|
|
50
|
+
"./node_modules/typescript/lib/lib.dom.asynciterable.d.ts": {
|
|
51
|
+
"import": "./node_modules/typescript/lib/lib.dom.asynciterable.d.js",
|
|
52
|
+
"types": "./node_modules/typescript/lib/lib.dom.asynciterable.d.d.ts"
|
|
53
|
+
},
|
|
54
|
+
"./node_modules/typescript/lib/lib.dom.d.ts": {
|
|
55
|
+
"import": "./node_modules/typescript/lib/lib.dom.d.js",
|
|
56
|
+
"types": "./node_modules/typescript/lib/lib.dom.d.d.ts"
|
|
57
|
+
},
|
|
58
|
+
"./node_modules/typescript/lib/lib.dom.iterable.d.ts": {
|
|
59
|
+
"import": "./node_modules/typescript/lib/lib.dom.iterable.d.js",
|
|
60
|
+
"types": "./node_modules/typescript/lib/lib.dom.iterable.d.d.ts"
|
|
61
|
+
},
|
|
62
|
+
"./node_modules/typescript/lib/lib.es2015.collection.d.ts": {
|
|
63
|
+
"import": "./node_modules/typescript/lib/lib.es2015.collection.d.js",
|
|
64
|
+
"types": "./node_modules/typescript/lib/lib.es2015.collection.d.d.ts"
|
|
65
|
+
},
|
|
66
|
+
"./node_modules/typescript/lib/lib.es2015.core.d.ts": {
|
|
67
|
+
"import": "./node_modules/typescript/lib/lib.es2015.core.d.js",
|
|
68
|
+
"types": "./node_modules/typescript/lib/lib.es2015.core.d.d.ts"
|
|
69
|
+
},
|
|
70
|
+
"./node_modules/typescript/lib/lib.es2015.d.ts": {
|
|
71
|
+
"import": "./node_modules/typescript/lib/lib.es2015.d.js",
|
|
72
|
+
"types": "./node_modules/typescript/lib/lib.es2015.d.d.ts"
|
|
73
|
+
},
|
|
74
|
+
"./node_modules/typescript/lib/lib.es2015.generator.d.ts": {
|
|
75
|
+
"import": "./node_modules/typescript/lib/lib.es2015.generator.d.js",
|
|
76
|
+
"types": "./node_modules/typescript/lib/lib.es2015.generator.d.d.ts"
|
|
77
|
+
},
|
|
78
|
+
"./node_modules/typescript/lib/lib.es2015.iterable.d.ts": {
|
|
79
|
+
"import": "./node_modules/typescript/lib/lib.es2015.iterable.d.js",
|
|
80
|
+
"types": "./node_modules/typescript/lib/lib.es2015.iterable.d.d.ts"
|
|
81
|
+
},
|
|
82
|
+
"./node_modules/typescript/lib/lib.es2015.promise.d.ts": {
|
|
83
|
+
"import": "./node_modules/typescript/lib/lib.es2015.promise.d.js",
|
|
84
|
+
"types": "./node_modules/typescript/lib/lib.es2015.promise.d.d.ts"
|
|
85
|
+
},
|
|
86
|
+
"./node_modules/typescript/lib/lib.es2015.proxy.d.ts": {
|
|
87
|
+
"import": "./node_modules/typescript/lib/lib.es2015.proxy.d.js",
|
|
88
|
+
"types": "./node_modules/typescript/lib/lib.es2015.proxy.d.d.ts"
|
|
89
|
+
},
|
|
90
|
+
"./node_modules/typescript/lib/lib.es2015.reflect.d.ts": {
|
|
91
|
+
"import": "./node_modules/typescript/lib/lib.es2015.reflect.d.js",
|
|
92
|
+
"types": "./node_modules/typescript/lib/lib.es2015.reflect.d.d.ts"
|
|
93
|
+
},
|
|
94
|
+
"./node_modules/typescript/lib/lib.es2015.symbol.d.ts": {
|
|
95
|
+
"import": "./node_modules/typescript/lib/lib.es2015.symbol.d.js",
|
|
96
|
+
"types": "./node_modules/typescript/lib/lib.es2015.symbol.d.d.ts"
|
|
97
|
+
},
|
|
98
|
+
"./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts": {
|
|
99
|
+
"import": "./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.js",
|
|
100
|
+
"types": "./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.d.ts"
|
|
101
|
+
},
|
|
102
|
+
"./node_modules/typescript/lib/lib.es2016.array.include.d.ts": {
|
|
103
|
+
"import": "./node_modules/typescript/lib/lib.es2016.array.include.d.js",
|
|
104
|
+
"types": "./node_modules/typescript/lib/lib.es2016.array.include.d.d.ts"
|
|
105
|
+
},
|
|
106
|
+
"./node_modules/typescript/lib/lib.es2016.d.ts": {
|
|
107
|
+
"import": "./node_modules/typescript/lib/lib.es2016.d.js",
|
|
108
|
+
"types": "./node_modules/typescript/lib/lib.es2016.d.d.ts"
|
|
109
|
+
},
|
|
110
|
+
"./node_modules/typescript/lib/lib.es2016.full.d.ts": {
|
|
111
|
+
"import": "./node_modules/typescript/lib/lib.es2016.full.d.js",
|
|
112
|
+
"types": "./node_modules/typescript/lib/lib.es2016.full.d.d.ts"
|
|
113
|
+
},
|
|
114
|
+
"./node_modules/typescript/lib/lib.es2016.intl.d.ts": {
|
|
115
|
+
"import": "./node_modules/typescript/lib/lib.es2016.intl.d.js",
|
|
116
|
+
"types": "./node_modules/typescript/lib/lib.es2016.intl.d.d.ts"
|
|
117
|
+
},
|
|
118
|
+
"./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts": {
|
|
119
|
+
"import": "./node_modules/typescript/lib/lib.es2017.arraybuffer.d.js",
|
|
120
|
+
"types": "./node_modules/typescript/lib/lib.es2017.arraybuffer.d.d.ts"
|
|
121
|
+
},
|
|
122
|
+
"./node_modules/typescript/lib/lib.es2017.d.ts": {
|
|
123
|
+
"import": "./node_modules/typescript/lib/lib.es2017.d.js",
|
|
124
|
+
"types": "./node_modules/typescript/lib/lib.es2017.d.d.ts"
|
|
125
|
+
},
|
|
126
|
+
"./node_modules/typescript/lib/lib.es2017.date.d.ts": {
|
|
127
|
+
"import": "./node_modules/typescript/lib/lib.es2017.date.d.js",
|
|
128
|
+
"types": "./node_modules/typescript/lib/lib.es2017.date.d.d.ts"
|
|
129
|
+
},
|
|
130
|
+
"./node_modules/typescript/lib/lib.es2017.full.d.ts": {
|
|
131
|
+
"import": "./node_modules/typescript/lib/lib.es2017.full.d.js",
|
|
132
|
+
"types": "./node_modules/typescript/lib/lib.es2017.full.d.d.ts"
|
|
133
|
+
},
|
|
134
|
+
"./node_modules/typescript/lib/lib.es2017.intl.d.ts": {
|
|
135
|
+
"import": "./node_modules/typescript/lib/lib.es2017.intl.d.js",
|
|
136
|
+
"types": "./node_modules/typescript/lib/lib.es2017.intl.d.d.ts"
|
|
137
|
+
},
|
|
138
|
+
"./node_modules/typescript/lib/lib.es2017.object.d.ts": {
|
|
139
|
+
"import": "./node_modules/typescript/lib/lib.es2017.object.d.js",
|
|
140
|
+
"types": "./node_modules/typescript/lib/lib.es2017.object.d.d.ts"
|
|
141
|
+
},
|
|
142
|
+
"./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts": {
|
|
143
|
+
"import": "./node_modules/typescript/lib/lib.es2017.sharedmemory.d.js",
|
|
144
|
+
"types": "./node_modules/typescript/lib/lib.es2017.sharedmemory.d.d.ts"
|
|
145
|
+
},
|
|
146
|
+
"./node_modules/typescript/lib/lib.es2017.string.d.ts": {
|
|
147
|
+
"import": "./node_modules/typescript/lib/lib.es2017.string.d.js",
|
|
148
|
+
"types": "./node_modules/typescript/lib/lib.es2017.string.d.d.ts"
|
|
149
|
+
},
|
|
150
|
+
"./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts": {
|
|
151
|
+
"import": "./node_modules/typescript/lib/lib.es2017.typedarrays.d.js",
|
|
152
|
+
"types": "./node_modules/typescript/lib/lib.es2017.typedarrays.d.d.ts"
|
|
153
|
+
},
|
|
154
|
+
"./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts": {
|
|
155
|
+
"import": "./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.js",
|
|
156
|
+
"types": "./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.d.ts"
|
|
157
|
+
},
|
|
158
|
+
"./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts": {
|
|
159
|
+
"import": "./node_modules/typescript/lib/lib.es2018.asynciterable.d.js",
|
|
160
|
+
"types": "./node_modules/typescript/lib/lib.es2018.asynciterable.d.d.ts"
|
|
161
|
+
},
|
|
162
|
+
"./node_modules/typescript/lib/lib.es2018.d.ts": {
|
|
163
|
+
"import": "./node_modules/typescript/lib/lib.es2018.d.js",
|
|
164
|
+
"types": "./node_modules/typescript/lib/lib.es2018.d.d.ts"
|
|
165
|
+
},
|
|
166
|
+
"./node_modules/typescript/lib/lib.es2018.full.d.ts": {
|
|
167
|
+
"import": "./node_modules/typescript/lib/lib.es2018.full.d.js",
|
|
168
|
+
"types": "./node_modules/typescript/lib/lib.es2018.full.d.d.ts"
|
|
169
|
+
},
|
|
170
|
+
"./node_modules/typescript/lib/lib.es2018.intl.d.ts": {
|
|
171
|
+
"import": "./node_modules/typescript/lib/lib.es2018.intl.d.js",
|
|
172
|
+
"types": "./node_modules/typescript/lib/lib.es2018.intl.d.d.ts"
|
|
173
|
+
},
|
|
174
|
+
"./node_modules/typescript/lib/lib.es2018.promise.d.ts": {
|
|
175
|
+
"import": "./node_modules/typescript/lib/lib.es2018.promise.d.js",
|
|
176
|
+
"types": "./node_modules/typescript/lib/lib.es2018.promise.d.d.ts"
|
|
177
|
+
},
|
|
178
|
+
"./node_modules/typescript/lib/lib.es2018.regexp.d.ts": {
|
|
179
|
+
"import": "./node_modules/typescript/lib/lib.es2018.regexp.d.js",
|
|
180
|
+
"types": "./node_modules/typescript/lib/lib.es2018.regexp.d.d.ts"
|
|
181
|
+
},
|
|
182
|
+
"./node_modules/typescript/lib/lib.es2019.array.d.ts": {
|
|
183
|
+
"import": "./node_modules/typescript/lib/lib.es2019.array.d.js",
|
|
184
|
+
"types": "./node_modules/typescript/lib/lib.es2019.array.d.d.ts"
|
|
185
|
+
},
|
|
186
|
+
"./node_modules/typescript/lib/lib.es2019.d.ts": {
|
|
187
|
+
"import": "./node_modules/typescript/lib/lib.es2019.d.js",
|
|
188
|
+
"types": "./node_modules/typescript/lib/lib.es2019.d.d.ts"
|
|
189
|
+
},
|
|
190
|
+
"./node_modules/typescript/lib/lib.es2019.full.d.ts": {
|
|
191
|
+
"import": "./node_modules/typescript/lib/lib.es2019.full.d.js",
|
|
192
|
+
"types": "./node_modules/typescript/lib/lib.es2019.full.d.d.ts"
|
|
193
|
+
},
|
|
194
|
+
"./node_modules/typescript/lib/lib.es2019.intl.d.ts": {
|
|
195
|
+
"import": "./node_modules/typescript/lib/lib.es2019.intl.d.js",
|
|
196
|
+
"types": "./node_modules/typescript/lib/lib.es2019.intl.d.d.ts"
|
|
197
|
+
},
|
|
198
|
+
"./node_modules/typescript/lib/lib.es2019.object.d.ts": {
|
|
199
|
+
"import": "./node_modules/typescript/lib/lib.es2019.object.d.js",
|
|
200
|
+
"types": "./node_modules/typescript/lib/lib.es2019.object.d.d.ts"
|
|
201
|
+
},
|
|
202
|
+
"./node_modules/typescript/lib/lib.es2019.string.d.ts": {
|
|
203
|
+
"import": "./node_modules/typescript/lib/lib.es2019.string.d.js",
|
|
204
|
+
"types": "./node_modules/typescript/lib/lib.es2019.string.d.d.ts"
|
|
205
|
+
},
|
|
206
|
+
"./node_modules/typescript/lib/lib.es2019.symbol.d.ts": {
|
|
207
|
+
"import": "./node_modules/typescript/lib/lib.es2019.symbol.d.js",
|
|
208
|
+
"types": "./node_modules/typescript/lib/lib.es2019.symbol.d.d.ts"
|
|
209
|
+
},
|
|
210
|
+
"./node_modules/typescript/lib/lib.es2020.bigint.d.ts": {
|
|
211
|
+
"import": "./node_modules/typescript/lib/lib.es2020.bigint.d.js",
|
|
212
|
+
"types": "./node_modules/typescript/lib/lib.es2020.bigint.d.d.ts"
|
|
213
|
+
},
|
|
214
|
+
"./node_modules/typescript/lib/lib.es2020.d.ts": {
|
|
215
|
+
"import": "./node_modules/typescript/lib/lib.es2020.d.js",
|
|
216
|
+
"types": "./node_modules/typescript/lib/lib.es2020.d.d.ts"
|
|
217
|
+
},
|
|
218
|
+
"./node_modules/typescript/lib/lib.es2020.date.d.ts": {
|
|
219
|
+
"import": "./node_modules/typescript/lib/lib.es2020.date.d.js",
|
|
220
|
+
"types": "./node_modules/typescript/lib/lib.es2020.date.d.d.ts"
|
|
221
|
+
},
|
|
222
|
+
"./node_modules/typescript/lib/lib.es2020.full.d.ts": {
|
|
223
|
+
"import": "./node_modules/typescript/lib/lib.es2020.full.d.js",
|
|
224
|
+
"types": "./node_modules/typescript/lib/lib.es2020.full.d.d.ts"
|
|
225
|
+
},
|
|
226
|
+
"./node_modules/typescript/lib/lib.es2020.intl.d.ts": {
|
|
227
|
+
"import": "./node_modules/typescript/lib/lib.es2020.intl.d.js",
|
|
228
|
+
"types": "./node_modules/typescript/lib/lib.es2020.intl.d.d.ts"
|
|
229
|
+
},
|
|
230
|
+
"./node_modules/typescript/lib/lib.es2020.number.d.ts": {
|
|
231
|
+
"import": "./node_modules/typescript/lib/lib.es2020.number.d.js",
|
|
232
|
+
"types": "./node_modules/typescript/lib/lib.es2020.number.d.d.ts"
|
|
233
|
+
},
|
|
234
|
+
"./node_modules/typescript/lib/lib.es2020.promise.d.ts": {
|
|
235
|
+
"import": "./node_modules/typescript/lib/lib.es2020.promise.d.js",
|
|
236
|
+
"types": "./node_modules/typescript/lib/lib.es2020.promise.d.d.ts"
|
|
237
|
+
},
|
|
238
|
+
"./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts": {
|
|
239
|
+
"import": "./node_modules/typescript/lib/lib.es2020.sharedmemory.d.js",
|
|
240
|
+
"types": "./node_modules/typescript/lib/lib.es2020.sharedmemory.d.d.ts"
|
|
241
|
+
},
|
|
242
|
+
"./node_modules/typescript/lib/lib.es2020.string.d.ts": {
|
|
243
|
+
"import": "./node_modules/typescript/lib/lib.es2020.string.d.js",
|
|
244
|
+
"types": "./node_modules/typescript/lib/lib.es2020.string.d.d.ts"
|
|
245
|
+
},
|
|
246
|
+
"./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts": {
|
|
247
|
+
"import": "./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.js",
|
|
248
|
+
"types": "./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.d.ts"
|
|
249
|
+
},
|
|
250
|
+
"./node_modules/typescript/lib/lib.es2021.d.ts": {
|
|
251
|
+
"import": "./node_modules/typescript/lib/lib.es2021.d.js",
|
|
252
|
+
"types": "./node_modules/typescript/lib/lib.es2021.d.d.ts"
|
|
253
|
+
},
|
|
254
|
+
"./node_modules/typescript/lib/lib.es2021.full.d.ts": {
|
|
255
|
+
"import": "./node_modules/typescript/lib/lib.es2021.full.d.js",
|
|
256
|
+
"types": "./node_modules/typescript/lib/lib.es2021.full.d.d.ts"
|
|
257
|
+
},
|
|
258
|
+
"./node_modules/typescript/lib/lib.es2021.intl.d.ts": {
|
|
259
|
+
"import": "./node_modules/typescript/lib/lib.es2021.intl.d.js",
|
|
260
|
+
"types": "./node_modules/typescript/lib/lib.es2021.intl.d.d.ts"
|
|
261
|
+
},
|
|
262
|
+
"./node_modules/typescript/lib/lib.es2021.promise.d.ts": {
|
|
263
|
+
"import": "./node_modules/typescript/lib/lib.es2021.promise.d.js",
|
|
264
|
+
"types": "./node_modules/typescript/lib/lib.es2021.promise.d.d.ts"
|
|
265
|
+
},
|
|
266
|
+
"./node_modules/typescript/lib/lib.es2021.string.d.ts": {
|
|
267
|
+
"import": "./node_modules/typescript/lib/lib.es2021.string.d.js",
|
|
268
|
+
"types": "./node_modules/typescript/lib/lib.es2021.string.d.d.ts"
|
|
269
|
+
},
|
|
270
|
+
"./node_modules/typescript/lib/lib.es2021.weakref.d.ts": {
|
|
271
|
+
"import": "./node_modules/typescript/lib/lib.es2021.weakref.d.js",
|
|
272
|
+
"types": "./node_modules/typescript/lib/lib.es2021.weakref.d.d.ts"
|
|
273
|
+
},
|
|
274
|
+
"./node_modules/typescript/lib/lib.es2022.array.d.ts": {
|
|
275
|
+
"import": "./node_modules/typescript/lib/lib.es2022.array.d.js",
|
|
276
|
+
"types": "./node_modules/typescript/lib/lib.es2022.array.d.d.ts"
|
|
277
|
+
},
|
|
278
|
+
"./node_modules/typescript/lib/lib.es2022.d.ts": {
|
|
279
|
+
"import": "./node_modules/typescript/lib/lib.es2022.d.js",
|
|
280
|
+
"types": "./node_modules/typescript/lib/lib.es2022.d.d.ts"
|
|
281
|
+
},
|
|
282
|
+
"./node_modules/typescript/lib/lib.es2022.error.d.ts": {
|
|
283
|
+
"import": "./node_modules/typescript/lib/lib.es2022.error.d.js",
|
|
284
|
+
"types": "./node_modules/typescript/lib/lib.es2022.error.d.d.ts"
|
|
285
|
+
},
|
|
286
|
+
"./node_modules/typescript/lib/lib.es2022.full.d.ts": {
|
|
287
|
+
"import": "./node_modules/typescript/lib/lib.es2022.full.d.js",
|
|
288
|
+
"types": "./node_modules/typescript/lib/lib.es2022.full.d.d.ts"
|
|
289
|
+
},
|
|
290
|
+
"./node_modules/typescript/lib/lib.es2022.intl.d.ts": {
|
|
291
|
+
"import": "./node_modules/typescript/lib/lib.es2022.intl.d.js",
|
|
292
|
+
"types": "./node_modules/typescript/lib/lib.es2022.intl.d.d.ts"
|
|
293
|
+
},
|
|
294
|
+
"./node_modules/typescript/lib/lib.es2022.object.d.ts": {
|
|
295
|
+
"import": "./node_modules/typescript/lib/lib.es2022.object.d.js",
|
|
296
|
+
"types": "./node_modules/typescript/lib/lib.es2022.object.d.d.ts"
|
|
297
|
+
},
|
|
298
|
+
"./node_modules/typescript/lib/lib.es2022.regexp.d.ts": {
|
|
299
|
+
"import": "./node_modules/typescript/lib/lib.es2022.regexp.d.js",
|
|
300
|
+
"types": "./node_modules/typescript/lib/lib.es2022.regexp.d.d.ts"
|
|
301
|
+
},
|
|
302
|
+
"./node_modules/typescript/lib/lib.es2022.string.d.ts": {
|
|
303
|
+
"import": "./node_modules/typescript/lib/lib.es2022.string.d.js",
|
|
304
|
+
"types": "./node_modules/typescript/lib/lib.es2022.string.d.d.ts"
|
|
305
|
+
},
|
|
306
|
+
"./node_modules/typescript/lib/lib.es2023.array.d.ts": {
|
|
307
|
+
"import": "./node_modules/typescript/lib/lib.es2023.array.d.js",
|
|
308
|
+
"types": "./node_modules/typescript/lib/lib.es2023.array.d.d.ts"
|
|
309
|
+
},
|
|
310
|
+
"./node_modules/typescript/lib/lib.es2023.collection.d.ts": {
|
|
311
|
+
"import": "./node_modules/typescript/lib/lib.es2023.collection.d.js",
|
|
312
|
+
"types": "./node_modules/typescript/lib/lib.es2023.collection.d.d.ts"
|
|
313
|
+
},
|
|
314
|
+
"./node_modules/typescript/lib/lib.es2023.d.ts": {
|
|
315
|
+
"import": "./node_modules/typescript/lib/lib.es2023.d.js",
|
|
316
|
+
"types": "./node_modules/typescript/lib/lib.es2023.d.d.ts"
|
|
317
|
+
},
|
|
318
|
+
"./node_modules/typescript/lib/lib.es2023.full.d.ts": {
|
|
319
|
+
"import": "./node_modules/typescript/lib/lib.es2023.full.d.js",
|
|
320
|
+
"types": "./node_modules/typescript/lib/lib.es2023.full.d.d.ts"
|
|
321
|
+
},
|
|
322
|
+
"./node_modules/typescript/lib/lib.es2023.intl.d.ts": {
|
|
323
|
+
"import": "./node_modules/typescript/lib/lib.es2023.intl.d.js",
|
|
324
|
+
"types": "./node_modules/typescript/lib/lib.es2023.intl.d.d.ts"
|
|
325
|
+
},
|
|
326
|
+
"./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts": {
|
|
327
|
+
"import": "./node_modules/typescript/lib/lib.es2024.arraybuffer.d.js",
|
|
328
|
+
"types": "./node_modules/typescript/lib/lib.es2024.arraybuffer.d.d.ts"
|
|
329
|
+
},
|
|
330
|
+
"./node_modules/typescript/lib/lib.es2024.collection.d.ts": {
|
|
331
|
+
"import": "./node_modules/typescript/lib/lib.es2024.collection.d.js",
|
|
332
|
+
"types": "./node_modules/typescript/lib/lib.es2024.collection.d.d.ts"
|
|
333
|
+
},
|
|
334
|
+
"./node_modules/typescript/lib/lib.es2024.d.ts": {
|
|
335
|
+
"import": "./node_modules/typescript/lib/lib.es2024.d.js",
|
|
336
|
+
"types": "./node_modules/typescript/lib/lib.es2024.d.d.ts"
|
|
337
|
+
},
|
|
338
|
+
"./node_modules/typescript/lib/lib.es2024.full.d.ts": {
|
|
339
|
+
"import": "./node_modules/typescript/lib/lib.es2024.full.d.js",
|
|
340
|
+
"types": "./node_modules/typescript/lib/lib.es2024.full.d.d.ts"
|
|
341
|
+
},
|
|
342
|
+
"./node_modules/typescript/lib/lib.es2024.object.d.ts": {
|
|
343
|
+
"import": "./node_modules/typescript/lib/lib.es2024.object.d.js",
|
|
344
|
+
"types": "./node_modules/typescript/lib/lib.es2024.object.d.d.ts"
|
|
345
|
+
},
|
|
346
|
+
"./node_modules/typescript/lib/lib.es2024.promise.d.ts": {
|
|
347
|
+
"import": "./node_modules/typescript/lib/lib.es2024.promise.d.js",
|
|
348
|
+
"types": "./node_modules/typescript/lib/lib.es2024.promise.d.d.ts"
|
|
349
|
+
},
|
|
350
|
+
"./node_modules/typescript/lib/lib.es2024.regexp.d.ts": {
|
|
351
|
+
"import": "./node_modules/typescript/lib/lib.es2024.regexp.d.js",
|
|
352
|
+
"types": "./node_modules/typescript/lib/lib.es2024.regexp.d.d.ts"
|
|
353
|
+
},
|
|
354
|
+
"./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts": {
|
|
355
|
+
"import": "./node_modules/typescript/lib/lib.es2024.sharedmemory.d.js",
|
|
356
|
+
"types": "./node_modules/typescript/lib/lib.es2024.sharedmemory.d.d.ts"
|
|
357
|
+
},
|
|
358
|
+
"./node_modules/typescript/lib/lib.es2024.string.d.ts": {
|
|
359
|
+
"import": "./node_modules/typescript/lib/lib.es2024.string.d.js",
|
|
360
|
+
"types": "./node_modules/typescript/lib/lib.es2024.string.d.d.ts"
|
|
361
|
+
},
|
|
362
|
+
"./node_modules/typescript/lib/lib.es5.d.ts": {
|
|
363
|
+
"import": "./node_modules/typescript/lib/lib.es5.d.js",
|
|
364
|
+
"types": "./node_modules/typescript/lib/lib.es5.d.d.ts"
|
|
365
|
+
},
|
|
366
|
+
"./node_modules/typescript/lib/lib.es6.d.ts": {
|
|
367
|
+
"import": "./node_modules/typescript/lib/lib.es6.d.js",
|
|
368
|
+
"types": "./node_modules/typescript/lib/lib.es6.d.d.ts"
|
|
369
|
+
},
|
|
370
|
+
"./node_modules/typescript/lib/lib.esnext.array.d.ts": {
|
|
371
|
+
"import": "./node_modules/typescript/lib/lib.esnext.array.d.js",
|
|
372
|
+
"types": "./node_modules/typescript/lib/lib.esnext.array.d.d.ts"
|
|
373
|
+
},
|
|
374
|
+
"./node_modules/typescript/lib/lib.esnext.collection.d.ts": {
|
|
375
|
+
"import": "./node_modules/typescript/lib/lib.esnext.collection.d.js",
|
|
376
|
+
"types": "./node_modules/typescript/lib/lib.esnext.collection.d.d.ts"
|
|
377
|
+
},
|
|
378
|
+
"./node_modules/typescript/lib/lib.esnext.d.ts": {
|
|
379
|
+
"import": "./node_modules/typescript/lib/lib.esnext.d.js",
|
|
380
|
+
"types": "./node_modules/typescript/lib/lib.esnext.d.d.ts"
|
|
381
|
+
},
|
|
382
|
+
"./node_modules/typescript/lib/lib.esnext.decorators.d.ts": {
|
|
383
|
+
"import": "./node_modules/typescript/lib/lib.esnext.decorators.d.js",
|
|
384
|
+
"types": "./node_modules/typescript/lib/lib.esnext.decorators.d.d.ts"
|
|
385
|
+
},
|
|
386
|
+
"./node_modules/typescript/lib/lib.esnext.disposable.d.ts": {
|
|
387
|
+
"import": "./node_modules/typescript/lib/lib.esnext.disposable.d.js",
|
|
388
|
+
"types": "./node_modules/typescript/lib/lib.esnext.disposable.d.d.ts"
|
|
389
|
+
},
|
|
390
|
+
"./node_modules/typescript/lib/lib.esnext.error.d.ts": {
|
|
391
|
+
"import": "./node_modules/typescript/lib/lib.esnext.error.d.js",
|
|
392
|
+
"types": "./node_modules/typescript/lib/lib.esnext.error.d.d.ts"
|
|
393
|
+
},
|
|
394
|
+
"./node_modules/typescript/lib/lib.esnext.float16.d.ts": {
|
|
395
|
+
"import": "./node_modules/typescript/lib/lib.esnext.float16.d.js",
|
|
396
|
+
"types": "./node_modules/typescript/lib/lib.esnext.float16.d.d.ts"
|
|
397
|
+
},
|
|
398
|
+
"./node_modules/typescript/lib/lib.esnext.full.d.ts": {
|
|
399
|
+
"import": "./node_modules/typescript/lib/lib.esnext.full.d.js",
|
|
400
|
+
"types": "./node_modules/typescript/lib/lib.esnext.full.d.d.ts"
|
|
401
|
+
},
|
|
402
|
+
"./node_modules/typescript/lib/lib.esnext.intl.d.ts": {
|
|
403
|
+
"import": "./node_modules/typescript/lib/lib.esnext.intl.d.js",
|
|
404
|
+
"types": "./node_modules/typescript/lib/lib.esnext.intl.d.d.ts"
|
|
405
|
+
},
|
|
406
|
+
"./node_modules/typescript/lib/lib.esnext.iterator.d.ts": {
|
|
407
|
+
"import": "./node_modules/typescript/lib/lib.esnext.iterator.d.js",
|
|
408
|
+
"types": "./node_modules/typescript/lib/lib.esnext.iterator.d.d.ts"
|
|
409
|
+
},
|
|
410
|
+
"./node_modules/typescript/lib/lib.esnext.promise.d.ts": {
|
|
411
|
+
"import": "./node_modules/typescript/lib/lib.esnext.promise.d.js",
|
|
412
|
+
"types": "./node_modules/typescript/lib/lib.esnext.promise.d.d.ts"
|
|
413
|
+
},
|
|
414
|
+
"./node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts": {
|
|
415
|
+
"import": "./node_modules/typescript/lib/lib.esnext.sharedmemory.d.js",
|
|
416
|
+
"types": "./node_modules/typescript/lib/lib.esnext.sharedmemory.d.d.ts"
|
|
417
|
+
},
|
|
418
|
+
"./node_modules/typescript/lib/lib.scripthost.d.ts": {
|
|
419
|
+
"import": "./node_modules/typescript/lib/lib.scripthost.d.js",
|
|
420
|
+
"types": "./node_modules/typescript/lib/lib.scripthost.d.d.ts"
|
|
421
|
+
},
|
|
422
|
+
"./node_modules/typescript/lib/lib.webworker.asynciterable.d.ts": {
|
|
423
|
+
"import": "./node_modules/typescript/lib/lib.webworker.asynciterable.d.js",
|
|
424
|
+
"types": "./node_modules/typescript/lib/lib.webworker.asynciterable.d.d.ts"
|
|
425
|
+
},
|
|
426
|
+
"./node_modules/typescript/lib/lib.webworker.d.ts": {
|
|
427
|
+
"import": "./node_modules/typescript/lib/lib.webworker.d.js",
|
|
428
|
+
"types": "./node_modules/typescript/lib/lib.webworker.d.d.ts"
|
|
429
|
+
},
|
|
430
|
+
"./node_modules/typescript/lib/lib.webworker.importscripts.d.ts": {
|
|
431
|
+
"import": "./node_modules/typescript/lib/lib.webworker.importscripts.d.js",
|
|
432
|
+
"types": "./node_modules/typescript/lib/lib.webworker.importscripts.d.d.ts"
|
|
433
|
+
},
|
|
434
|
+
"./node_modules/typescript/lib/lib.webworker.iterable.d.ts": {
|
|
435
|
+
"import": "./node_modules/typescript/lib/lib.webworker.iterable.d.js",
|
|
436
|
+
"types": "./node_modules/typescript/lib/lib.webworker.iterable.d.d.ts"
|
|
437
|
+
},
|
|
438
|
+
"./node_modules/typescript/lib/tsserverlibrary.d.ts": {
|
|
439
|
+
"import": "./node_modules/typescript/lib/tsserverlibrary.d.js",
|
|
440
|
+
"types": "./node_modules/typescript/lib/tsserverlibrary.d.d.ts"
|
|
441
|
+
},
|
|
442
|
+
"./node_modules/typescript/lib/typescript.d.ts": {
|
|
443
|
+
"import": "./node_modules/typescript/lib/typescript.d.js",
|
|
444
|
+
"types": "./node_modules/typescript/lib/typescript.d.d.ts"
|
|
445
|
+
},
|
|
446
|
+
"./service.test.ts": {
|
|
447
|
+
"import": "./service.test.js",
|
|
448
|
+
"types": "./service.test.d.ts"
|
|
449
|
+
},
|
|
450
|
+
"./service.ts": {
|
|
451
|
+
"import": "./service.js",
|
|
452
|
+
"types": "./service.d.ts"
|
|
453
|
+
},
|
|
454
|
+
"./stub.test.ts": {
|
|
455
|
+
"import": "./stub.test.js",
|
|
456
|
+
"types": "./stub.test.d.ts"
|
|
457
|
+
},
|
|
458
|
+
"./stub.ts": {
|
|
459
|
+
"import": "./stub.js",
|
|
460
|
+
"types": "./stub.d.ts"
|
|
461
|
+
},
|
|
462
|
+
"./test-server.ts": {
|
|
463
|
+
"import": "./test-server.js",
|
|
464
|
+
"types": "./test-server.d.ts"
|
|
465
|
+
},
|
|
466
|
+
"./type.ts": {
|
|
467
|
+
"import": "./type.js",
|
|
468
|
+
"types": "./type.d.ts"
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
package/service.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Fetcher, GetParsedType, GetRequestType, GetResponseType, RestEndpoint, RestEndpointRequest } from './type.ts';
|
|
2
|
+
import { type Observable } from 'rxjs';
|
|
3
|
+
export declare const createRestClientCreator: <FETCHER extends Fetcher>(fetcher: FETCHER) => <API extends RestEndpoint<RestEndpointRequest, GetParsedType<FETCHER>>>(baseUrl: string) => <REQUEST extends GetRequestType<API>>(request: REQUEST) => Observable<GetResponseType<API, REQUEST>>;
|
package/service.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { stringifyUrl } from '@dungarees/core/url.ts';
|
|
2
|
+
import { defer } from 'rxjs';
|
|
3
|
+
export const createRestClientCreator = (fetcher) => (baseUrl) => {
|
|
4
|
+
const parsedBase = parseBaseUrl(baseUrl);
|
|
5
|
+
return (request) =>
|
|
6
|
+
// The fetcher hands back whatever the transport parsed, and this is the boundary at which the
|
|
7
|
+
// API type says what that is; nothing here can check it.
|
|
8
|
+
defer(async () => await fetcher(...getFetcherArg(parsedBase, request)));
|
|
9
|
+
};
|
|
10
|
+
// Rejected at client creation rather than per request: a search or hash on the base url would be
|
|
11
|
+
// silently dropped by the per-request url building below.
|
|
12
|
+
const parseBaseUrl = (baseUrl) => {
|
|
13
|
+
const url = new URL(baseUrl);
|
|
14
|
+
if (url.search !== '') {
|
|
15
|
+
throw new Error(`baseUrl must not contain a query string: ${baseUrl}`);
|
|
16
|
+
}
|
|
17
|
+
if (url.hash !== '') {
|
|
18
|
+
throw new Error(`baseUrl must not contain a hash fragment: ${baseUrl}`);
|
|
19
|
+
}
|
|
20
|
+
const rawPath = url.pathname;
|
|
21
|
+
return {
|
|
22
|
+
protocol: url.protocol.replace(/:$/, ''),
|
|
23
|
+
hostname: url.hostname,
|
|
24
|
+
port: url.port !== '' ? Number(url.port) : undefined,
|
|
25
|
+
basePath: rawPath.endsWith('/') ? rawPath.slice(0, -1) : rawPath,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
const joinPath = (basePath, requestPath) => `${basePath}${requestPath.startsWith('/') ? requestPath : `/${requestPath}`}`;
|
|
29
|
+
const getFetcherArg = (parsedBase, { pathname, method, search, headers = {}, body = undefined }) => [
|
|
30
|
+
stringifyUrl({
|
|
31
|
+
protocol: parsedBase.protocol,
|
|
32
|
+
hostname: parsedBase.hostname,
|
|
33
|
+
...(parsedBase.port !== undefined && { port: parsedBase.port }),
|
|
34
|
+
pathname: joinPath(parsedBase.basePath, pathname),
|
|
35
|
+
search,
|
|
36
|
+
}),
|
|
37
|
+
{ method, headers, body },
|
|
38
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/service.test.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createRestClientCreator } from './service.js';
|
|
2
|
+
import { firstValueFrom } from 'rxjs';
|
|
3
|
+
import { expect, test } from 'vitest';
|
|
4
|
+
const TEST_RESPONSE = { a: 3 };
|
|
5
|
+
const JSON_FETCHER = async () => await Promise.resolve(TEST_RESPONSE);
|
|
6
|
+
const PROBE_FETCHER = async (url, { method, headers = {} }) => await Promise.resolve({ method, url, headers });
|
|
7
|
+
const probeUrl = async (baseUrl) => {
|
|
8
|
+
const client = createRestClientCreator(PROBE_FETCHER)(baseUrl);
|
|
9
|
+
const { url } = await firstValueFrom(client({ method: 'GET', pathname: '/path' }));
|
|
10
|
+
return url;
|
|
11
|
+
};
|
|
12
|
+
test('the client resolves to the response the fetcher produced', async () => {
|
|
13
|
+
const client = createRestClientCreator(JSON_FETCHER)('https://host');
|
|
14
|
+
expect(await firstValueFrom(client({ method: 'GET', pathname: '/path' }))).toEqual(TEST_RESPONSE);
|
|
15
|
+
});
|
|
16
|
+
test('the client narrows the response type from the request it was given', () => {
|
|
17
|
+
const client = createRestClientCreator(JSON_FETCHER)('https://host');
|
|
18
|
+
client({ method: 'GET', pathname: '/path' });
|
|
19
|
+
client({
|
|
20
|
+
method: 'GET',
|
|
21
|
+
pathname: '/path',
|
|
22
|
+
// @ts-expect-error the '/path' endpoint responds with TestResponse, not OtherResponse
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
test('the client narrows the response type from the method', () => {
|
|
26
|
+
const client = createRestClientCreator(JSON_FETCHER)('https://host');
|
|
27
|
+
client({ method: 'POST', pathname: '/path' });
|
|
28
|
+
});
|
|
29
|
+
test('the awaited value is narrowed the same way the observable is', async () => {
|
|
30
|
+
const client = createRestClientCreator(JSON_FETCHER)('https://host');
|
|
31
|
+
const response = await firstValueFrom(client({ method: 'GET', pathname: '/path' }));
|
|
32
|
+
response;
|
|
33
|
+
// @ts-expect-error the '/path' endpoint responds with TestResponse, not OtherResponse
|
|
34
|
+
response;
|
|
35
|
+
});
|
|
36
|
+
test('the client narrows on search and headers as well as path and method', () => {
|
|
37
|
+
const client = createRestClientCreator(JSON_FETCHER)('https://host');
|
|
38
|
+
const search = { a: '1' };
|
|
39
|
+
const headers = { 'header-1': 'value-1' };
|
|
40
|
+
client({
|
|
41
|
+
method: 'GET',
|
|
42
|
+
pathname: '/path',
|
|
43
|
+
search,
|
|
44
|
+
headers,
|
|
45
|
+
});
|
|
46
|
+
client({
|
|
47
|
+
method: 'GET',
|
|
48
|
+
pathname: '/path',
|
|
49
|
+
search,
|
|
50
|
+
headers,
|
|
51
|
+
// @ts-expect-error the {a: string} search selects TestResponse, not OtherResponse
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
test('the client rejects a request the api does not declare', () => {
|
|
55
|
+
const client = createRestClientCreator(JSON_FETCHER)('https://host');
|
|
56
|
+
const search = { a: '1' };
|
|
57
|
+
const headers = { 'header-1': 'value-1' };
|
|
58
|
+
client({
|
|
59
|
+
method: 'GET',
|
|
60
|
+
// @ts-expect-error '/path2' is not a pathname the api declares
|
|
61
|
+
pathname: '/path2',
|
|
62
|
+
search,
|
|
63
|
+
headers,
|
|
64
|
+
});
|
|
65
|
+
client({
|
|
66
|
+
// @ts-expect-error 'POST' is not a method the api declares
|
|
67
|
+
method: 'POST',
|
|
68
|
+
pathname: '/path',
|
|
69
|
+
search,
|
|
70
|
+
headers,
|
|
71
|
+
});
|
|
72
|
+
// @ts-expect-error the api requires headers, which this request omits
|
|
73
|
+
client({ method: 'GET', pathname: '/path', search });
|
|
74
|
+
// @ts-expect-error the api requires a search, which this request omits
|
|
75
|
+
client({ method: 'GET', pathname: '/path', headers });
|
|
76
|
+
});
|
|
77
|
+
test('the creator rejects a type that is not a RestEndpoint', () => {
|
|
78
|
+
// @ts-expect-error a string is not a RestEndpoint
|
|
79
|
+
createRestClientCreator(JSON_FETCHER)('https://host');
|
|
80
|
+
});
|
|
81
|
+
test('the creator rejects an api whose response the fetcher cannot produce', () => {
|
|
82
|
+
// @ts-expect-error a JSON fetcher cannot produce a function
|
|
83
|
+
createRestClientCreator(JSON_FETCHER)('https://host');
|
|
84
|
+
});
|
|
85
|
+
test('every part of the request reaches the fetcher', async () => {
|
|
86
|
+
const client = createRestClientCreator(PROBE_FETCHER)('https://host');
|
|
87
|
+
const search = { a: 1 };
|
|
88
|
+
const headers = { 'header-1': 'value-1' };
|
|
89
|
+
expect(await firstValueFrom(client({ method: 'POST', pathname: '/path', search, headers }))).toEqual({
|
|
90
|
+
method: 'POST',
|
|
91
|
+
url: 'https://host/path?a=1',
|
|
92
|
+
headers: { 'header-1': 'value-1' },
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
test('a baseUrl with no path just gets the pathname appended', async () => {
|
|
96
|
+
expect(await probeUrl('https://host')).toBe('https://host/path');
|
|
97
|
+
});
|
|
98
|
+
test('a baseUrl that is only a trailing slash does not double up', async () => {
|
|
99
|
+
expect(await probeUrl('https://host/')).toBe('https://host/path');
|
|
100
|
+
});
|
|
101
|
+
test('a baseUrl path prefix is kept in front of the pathname', async () => {
|
|
102
|
+
expect(await probeUrl('https://host/api/v2')).toBe('https://host/api/v2/path');
|
|
103
|
+
});
|
|
104
|
+
test('a baseUrl path prefix with a trailing slash does not double up', async () => {
|
|
105
|
+
expect(await probeUrl('https://host/api/v2/')).toBe('https://host/api/v2/path');
|
|
106
|
+
});
|
|
107
|
+
test('a baseUrl port is kept', async () => {
|
|
108
|
+
expect(await probeUrl('https://host:8080/api')).toBe('https://host:8080/api/path');
|
|
109
|
+
});
|
|
110
|
+
test('a request pathname without a leading slash still gets a separator', async () => {
|
|
111
|
+
const client = createRestClientCreator(PROBE_FETCHER)('https://host/api');
|
|
112
|
+
const { url } = await firstValueFrom(client({ method: 'GET', pathname: 'a/b' }));
|
|
113
|
+
expect(url).toBe('https://host/api/a/b');
|
|
114
|
+
});
|
|
115
|
+
test('a baseUrl carrying a query string is refused when the client is created', () => {
|
|
116
|
+
expect(() => createRestClientCreator(PROBE_FETCHER)('https://host?key=abc')).toThrow('baseUrl must not contain a query string');
|
|
117
|
+
});
|
|
118
|
+
test('a baseUrl carrying a hash fragment is refused when the client is created', () => {
|
|
119
|
+
expect(() => createRestClientCreator(PROBE_FETCHER)('https://host#section')).toThrow('baseUrl must not contain a hash fragment');
|
|
120
|
+
});
|
|
121
|
+
test('a baseUrl that is not a url at all is refused when the client is created', () => {
|
|
122
|
+
expect(() => createRestClientCreator(PROBE_FETCHER)('not-a-url')).toThrow();
|
|
123
|
+
});
|
|
124
|
+
test('the fetcher is not called until the observable is subscribed to', () => {
|
|
125
|
+
let calls = 0;
|
|
126
|
+
const countingFetcher = async () => {
|
|
127
|
+
calls += 1;
|
|
128
|
+
return await Promise.resolve(TEST_RESPONSE);
|
|
129
|
+
};
|
|
130
|
+
const client = createRestClientCreator(countingFetcher)('https://host');
|
|
131
|
+
client({ method: 'GET', pathname: '/path' });
|
|
132
|
+
expect(calls).toBe(0);
|
|
133
|
+
});
|
|
134
|
+
test('each subscription runs the request again', async () => {
|
|
135
|
+
let calls = 0;
|
|
136
|
+
const countingFetcher = async () => {
|
|
137
|
+
calls += 1;
|
|
138
|
+
return await Promise.resolve(TEST_RESPONSE);
|
|
139
|
+
};
|
|
140
|
+
const client = createRestClientCreator(countingFetcher)('https://host');
|
|
141
|
+
const response$ = client({ method: 'GET', pathname: '/path' });
|
|
142
|
+
await firstValueFrom(response$);
|
|
143
|
+
await firstValueFrom(response$);
|
|
144
|
+
expect(calls).toBe(2);
|
|
145
|
+
});
|
|
146
|
+
test('a rejecting fetcher surfaces as an observable error', async () => {
|
|
147
|
+
const failingFetcher = async () => {
|
|
148
|
+
throw new Error('network down');
|
|
149
|
+
};
|
|
150
|
+
const client = createRestClientCreator(failingFetcher)('https://host');
|
|
151
|
+
await expect(firstValueFrom(client({ method: 'GET', pathname: '/path' }))).rejects.toThrow('network down');
|
|
152
|
+
});
|
package/stub.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { GetResponseType, RestEndpoint, RestEndpointRequest, StubEndpoint } from './type.ts';
|
|
2
|
+
import { type Observable } from 'rxjs';
|
|
3
|
+
export declare const DEFAULT_STUB_DELAY = 1;
|
|
4
|
+
export declare const createStubRestClient: <API extends RestEndpoint, ENDPOINTS extends ReadonlyArray<StubEndpoint<API>>>(endpoints: ENDPOINTS) => <REQUEST extends RestEndpointRequest>(request: REQUEST) => Observable<GetResponseType<API, REQUEST>>;
|
package/stub.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isDeepEqual } from '@dungarees/core/util.ts';
|
|
2
|
+
import { delay, mergeMap, of, throwError, timer } from 'rxjs';
|
|
3
|
+
export const DEFAULT_STUB_DELAY = 1;
|
|
4
|
+
export const createStubRestClient = (endpoints) => (request) => {
|
|
5
|
+
const endpoint = endpoints.find((candidate) => isDeepEqual(candidate.request, request));
|
|
6
|
+
// Reported as a failure rather than answered with undefined, so that a stub which is simply
|
|
7
|
+
// missing an endpoint is distinguishable from one that answers with undefined on purpose.
|
|
8
|
+
if (endpoint === undefined) {
|
|
9
|
+
return throwError(() => new Error(`No stubbed endpoint matches the request: ${JSON.stringify(request)}`));
|
|
10
|
+
}
|
|
11
|
+
// The matching endpoint declares one of the api's responses; tying that union back to the
|
|
12
|
+
// response for this particular request is what the API type expresses and cannot be checked.
|
|
13
|
+
return getStubbedValue(endpoint.response, endpoint.delay ?? DEFAULT_STUB_DELAY);
|
|
14
|
+
};
|
|
15
|
+
// An Error as the stubbed response means the endpoint fails, which is how a test states that the
|
|
16
|
+
// transport itself went wrong rather than that it answered.
|
|
17
|
+
const getStubbedValue = (value, delayTime) => value instanceof Error
|
|
18
|
+
? timer(delayTime).pipe(mergeMap(() => throwError(() => value)))
|
|
19
|
+
: of(value).pipe(delay(delayTime));
|
package/stub.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/stub.test.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createStubRestClient } from './stub.js';
|
|
2
|
+
import { mtest } from '@dungarees/core/marbles-vitest.ts';
|
|
3
|
+
import { expect, test } from 'vitest';
|
|
4
|
+
const endpointAHeader1 = {
|
|
5
|
+
request: {
|
|
6
|
+
method: 'GET',
|
|
7
|
+
pathname: '/path',
|
|
8
|
+
search: { a: '1' },
|
|
9
|
+
headers: { 'header-1': 'value-1' },
|
|
10
|
+
},
|
|
11
|
+
response: { a: 1 },
|
|
12
|
+
};
|
|
13
|
+
const endpointBHeader2 = {
|
|
14
|
+
request: {
|
|
15
|
+
method: 'GET',
|
|
16
|
+
pathname: '/path',
|
|
17
|
+
search: { b: '1' },
|
|
18
|
+
headers: { 'header-1': 'value-2' },
|
|
19
|
+
},
|
|
20
|
+
response: { b: 1 },
|
|
21
|
+
};
|
|
22
|
+
const endpointBHeader1 = {
|
|
23
|
+
request: {
|
|
24
|
+
method: 'GET',
|
|
25
|
+
pathname: '/path',
|
|
26
|
+
search: { b: '1' },
|
|
27
|
+
headers: { 'header-1': 'value-1' },
|
|
28
|
+
},
|
|
29
|
+
response: { c: 1 },
|
|
30
|
+
};
|
|
31
|
+
const endpointAHeader2 = {
|
|
32
|
+
request: {
|
|
33
|
+
method: 'GET',
|
|
34
|
+
pathname: '/path',
|
|
35
|
+
search: { a: '1' },
|
|
36
|
+
headers: { 'header-1': 'value-2' },
|
|
37
|
+
},
|
|
38
|
+
response: { d: 1 },
|
|
39
|
+
};
|
|
40
|
+
const ALL_ENDPOINTS = [
|
|
41
|
+
endpointAHeader1,
|
|
42
|
+
endpointBHeader2,
|
|
43
|
+
endpointBHeader1,
|
|
44
|
+
endpointAHeader2,
|
|
45
|
+
];
|
|
46
|
+
const SEARCH_A = { a: '1' };
|
|
47
|
+
const SEARCH_B = { b: '1' };
|
|
48
|
+
const HEADERS_1 = { 'header-1': 'value-1' };
|
|
49
|
+
const HEADERS_2 = { 'header-1': 'value-2' };
|
|
50
|
+
mtest('the stub stands in for a client and answers the matching endpoint', ({ expect }) => {
|
|
51
|
+
const consume = (client) => client({
|
|
52
|
+
method: 'GET',
|
|
53
|
+
pathname: '/path',
|
|
54
|
+
search: SEARCH_A,
|
|
55
|
+
headers: HEADERS_1,
|
|
56
|
+
});
|
|
57
|
+
expect(consume(createStubRestClient([endpointAHeader1]))).toBeObservable('-(a|)', { a: { a: 1 } });
|
|
58
|
+
});
|
|
59
|
+
mtest('the stub picks the endpoint whose headers match', ({ expect }) => {
|
|
60
|
+
const client = createStubRestClient(ALL_ENDPOINTS);
|
|
61
|
+
const response = client({
|
|
62
|
+
method: 'GET',
|
|
63
|
+
pathname: '/path',
|
|
64
|
+
search: SEARCH_A,
|
|
65
|
+
headers: HEADERS_2,
|
|
66
|
+
});
|
|
67
|
+
response;
|
|
68
|
+
// @ts-expect-error the value-2 header selects ResponseD, not ResponseA
|
|
69
|
+
response;
|
|
70
|
+
expect(response).toBeObservable('-(d|)', { d: { d: 1 } });
|
|
71
|
+
});
|
|
72
|
+
mtest('the stub picks the endpoint whose search matches', ({ expect }) => {
|
|
73
|
+
const client = createStubRestClient(ALL_ENDPOINTS);
|
|
74
|
+
const response = client({
|
|
75
|
+
method: 'GET',
|
|
76
|
+
pathname: '/path',
|
|
77
|
+
search: SEARCH_B,
|
|
78
|
+
headers: HEADERS_1,
|
|
79
|
+
});
|
|
80
|
+
response;
|
|
81
|
+
// @ts-expect-error the {b} search selects ResponseC, not ResponseA
|
|
82
|
+
response;
|
|
83
|
+
expect(response).toBeObservable('-(c|)', { c: { c: 1 } });
|
|
84
|
+
});
|
|
85
|
+
mtest('the stub picks the endpoint whose method matches', ({ expect }) => {
|
|
86
|
+
const getEndpoint = {
|
|
87
|
+
request: { method: 'GET', pathname: '/path' },
|
|
88
|
+
response: { a: 1 },
|
|
89
|
+
};
|
|
90
|
+
const postEndpoint = {
|
|
91
|
+
request: { method: 'POST', pathname: '/path' },
|
|
92
|
+
response: { b: 1 },
|
|
93
|
+
};
|
|
94
|
+
const endpoints = [getEndpoint, postEndpoint];
|
|
95
|
+
const client = createStubRestClient(endpoints);
|
|
96
|
+
const response = client({ method: 'POST', pathname: '/path' });
|
|
97
|
+
response;
|
|
98
|
+
// @ts-expect-error POST selects ResponseB, not ResponseA
|
|
99
|
+
response;
|
|
100
|
+
expect(response).toBeObservable('-(b|)', { b: { b: 1 } });
|
|
101
|
+
});
|
|
102
|
+
mtest('the stub picks the endpoint whose pathname matches', ({ expect }) => {
|
|
103
|
+
const first = {
|
|
104
|
+
request: { method: 'GET', pathname: '/path' },
|
|
105
|
+
response: { a: 1 },
|
|
106
|
+
};
|
|
107
|
+
const second = {
|
|
108
|
+
request: { method: 'GET', pathname: '/path2' },
|
|
109
|
+
response: { b: 1 },
|
|
110
|
+
};
|
|
111
|
+
const endpoints = [first, second];
|
|
112
|
+
const client = createStubRestClient(endpoints);
|
|
113
|
+
const response = client({ method: 'GET', pathname: '/path2' });
|
|
114
|
+
response;
|
|
115
|
+
// @ts-expect-error '/path2' selects ResponseB, not ResponseA
|
|
116
|
+
response;
|
|
117
|
+
expect(response).toBeObservable('-(b|)', { b: { b: 1 } });
|
|
118
|
+
});
|
|
119
|
+
mtest('the stub waits the delay the endpoint asked for', ({ expect }) => {
|
|
120
|
+
const delayed = {
|
|
121
|
+
request: { method: 'GET', pathname: '/path' },
|
|
122
|
+
response: { a: 1 },
|
|
123
|
+
delay: 3,
|
|
124
|
+
};
|
|
125
|
+
const endpoints = [delayed];
|
|
126
|
+
const client = createStubRestClient(endpoints);
|
|
127
|
+
expect(client({ method: 'GET', pathname: '/path' })).toBeObservable('---(a|)', {
|
|
128
|
+
a: { a: 1 },
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
mtest('the stub reports an endpoint whose response is an Error as a failure', ({ expect }) => {
|
|
132
|
+
const failure = new Error('gateway timeout');
|
|
133
|
+
const failing = {
|
|
134
|
+
request: { method: 'GET', pathname: '/path' },
|
|
135
|
+
response: failure,
|
|
136
|
+
};
|
|
137
|
+
const endpoints = [failing];
|
|
138
|
+
const client = createStubRestClient(endpoints);
|
|
139
|
+
expect(client({ method: 'GET', pathname: '/path' })).toBeObservable('-#', {}, failure);
|
|
140
|
+
});
|
|
141
|
+
mtest('the stub answers an endpoint that was stubbed with undefined', ({ expect }) => {
|
|
142
|
+
const undefinedResponse = {
|
|
143
|
+
request: { method: 'GET', pathname: '/path' },
|
|
144
|
+
response: undefined,
|
|
145
|
+
};
|
|
146
|
+
const endpoints = [undefinedResponse];
|
|
147
|
+
const client = createStubRestClient(endpoints);
|
|
148
|
+
expect(client({ method: 'GET', pathname: '/path' })).toBeObservable('-(u|)', {
|
|
149
|
+
u: undefined,
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
test('the stub fails loudly for a request none of its endpoints match', async () => {
|
|
153
|
+
const stubbed = {
|
|
154
|
+
request: { method: 'GET', pathname: '/path' },
|
|
155
|
+
response: { a: 1 },
|
|
156
|
+
};
|
|
157
|
+
const endpoints = [stubbed];
|
|
158
|
+
const client = createStubRestClient(endpoints);
|
|
159
|
+
await expect(new Promise((resolve, reject) => {
|
|
160
|
+
client({ method: 'GET', pathname: '/missing' }).subscribe({ error: reject, next: resolve });
|
|
161
|
+
})).rejects.toThrow('No stubbed endpoint matches the request');
|
|
162
|
+
});
|
package/test-server.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type ReceivedRequest = {
|
|
2
|
+
method: string;
|
|
3
|
+
url: string;
|
|
4
|
+
headers: Record<string, string>;
|
|
5
|
+
body: string;
|
|
6
|
+
};
|
|
7
|
+
export type TestServerResponse = {
|
|
8
|
+
status?: number;
|
|
9
|
+
contentType?: string;
|
|
10
|
+
body: string;
|
|
11
|
+
};
|
|
12
|
+
export type TestServer = {
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
received: ReceivedRequest[];
|
|
15
|
+
close: () => Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
export declare const startTestServer: ({ respond, }?: {
|
|
18
|
+
respond?: (request: ReceivedRequest) => TestServerResponse;
|
|
19
|
+
}) => Promise<TestServer>;
|
package/test-server.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
// A real server rather than a stand-in for fetch: content types, body encoding and response
|
|
3
|
+
// parsing are the whole of what the fetchers do, and none of it survives stubbing fetch.
|
|
4
|
+
export const startTestServer = async ({ respond = () => ({ body: '{}', contentType: 'application/json' }), } = {}) => {
|
|
5
|
+
const received = [];
|
|
6
|
+
const server = createServer((request, response) => {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
request.on('data', (chunk) => chunks.push(chunk));
|
|
9
|
+
request.on('end', () => {
|
|
10
|
+
const headers = Object.fromEntries(Object.entries(request.headers).map(([name, value]) => [
|
|
11
|
+
name,
|
|
12
|
+
Array.isArray(value) ? value.join(', ') : (value ?? ''),
|
|
13
|
+
]));
|
|
14
|
+
const receivedRequest = {
|
|
15
|
+
method: request.method ?? '',
|
|
16
|
+
url: request.url ?? '',
|
|
17
|
+
headers,
|
|
18
|
+
body: Buffer.concat(chunks).toString('utf-8'),
|
|
19
|
+
};
|
|
20
|
+
received.push(receivedRequest);
|
|
21
|
+
const { status = 200, contentType = 'application/json', body } = respond(receivedRequest);
|
|
22
|
+
response.writeHead(status, { 'Content-Type': contentType });
|
|
23
|
+
response.end(body);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
await new Promise((resolve) => {
|
|
27
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
28
|
+
});
|
|
29
|
+
const address = server.address();
|
|
30
|
+
if (address === null || typeof address === 'string') {
|
|
31
|
+
throw new Error('Test server was expected to be listening on a port');
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
baseUrl: `http://127.0.0.1:${address.port}`,
|
|
35
|
+
received,
|
|
36
|
+
close: async () => {
|
|
37
|
+
await new Promise((resolve, reject) => {
|
|
38
|
+
server.close((error) => (error === undefined ? resolve() : reject(error)));
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
};
|
package/type.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { UrlSearchConfig } from '@dungarees/core/url.ts';
|
|
2
|
+
import type { Observable } from 'rxjs';
|
|
3
|
+
export type AUTH_HEADER = {
|
|
4
|
+
Authorization: `Token ${string}`;
|
|
5
|
+
};
|
|
6
|
+
export type RestMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH';
|
|
7
|
+
export type RestEndpointRequest = {
|
|
8
|
+
readonly method: RestMethod;
|
|
9
|
+
readonly pathname: string;
|
|
10
|
+
readonly search?: UrlSearchConfig;
|
|
11
|
+
readonly headers?: Record<string, string>;
|
|
12
|
+
readonly body?: unknown;
|
|
13
|
+
};
|
|
14
|
+
export type RestEndpoint<REQUEST extends RestEndpointRequest = RestEndpointRequest, RESPONSE = unknown> = {
|
|
15
|
+
request: REQUEST;
|
|
16
|
+
response: RESPONSE;
|
|
17
|
+
};
|
|
18
|
+
export type GetResponseType<API, REQUEST extends RestEndpointRequest> = API extends RestEndpoint<REQUEST, infer RESPONSE> ? RESPONSE : never;
|
|
19
|
+
export type GetRequestType<API> = API extends RestEndpoint<infer REQUEST, unknown> ? REQUEST : never;
|
|
20
|
+
export type FetcherConfigArg = {
|
|
21
|
+
method: string;
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
body: unknown;
|
|
24
|
+
};
|
|
25
|
+
export type Fetcher<PARSED_TYPE = unknown> = (url: string, request: FetcherConfigArg) => Promise<PARSED_TYPE>;
|
|
26
|
+
export type GetParsedType<FETCHER extends Fetcher> = FETCHER extends Fetcher<infer PARSED_TYPE> ? PARSED_TYPE : never;
|
|
27
|
+
export type RestClient<FETCHER extends Fetcher, API extends RestEndpoint<RestEndpointRequest, GetParsedType<FETCHER>>> = <REQUEST extends RestEndpointRequest>(request: REQUEST) => Observable<GetResponseType<API, REQUEST>>;
|
|
28
|
+
export type StubEndpoint<API extends RestEndpoint> = API extends RestEndpoint<infer REQUEST, infer RESPONSE> ? {
|
|
29
|
+
request: REQUEST;
|
|
30
|
+
response: RESPONSE;
|
|
31
|
+
delay?: number;
|
|
32
|
+
} : never;
|
package/type.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|