typedrest 0.5.0 → 0.6.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.
Files changed (88) hide show
  1. package/README.md +41 -5
  2. package/endpoints/{CachingEndpoint.ts → CachingEndpoint.d.ts} +0 -1
  3. package/endpoints/Endpoint.d.ts +121 -0
  4. package/endpoints/Endpoint.js +1 -1
  5. package/endpoints/{EntryEndpoint.ts → EntryEndpoint.d.ts} +7 -27
  6. package/endpoints/generic/{CollectionEndpoint.ts → CollectionEndpoint.d.ts} +2 -5
  7. package/endpoints/generic/{ETagEndpointBase.ts → ETagEndpointBase.d.ts} +7 -38
  8. package/endpoints/generic/{ElementEndpoint.ts → ElementEndpoint.d.ts} +12 -70
  9. package/endpoints/generic/{GenericCollectionEndpoint.ts → GenericCollectionEndpoint.d.ts} +21 -106
  10. package/endpoints/generic/{IndexerEndpoint.ts → IndexerEndpoint.d.ts} +4 -11
  11. package/endpoints/generic/PartialResponse.d.ts +19 -0
  12. package/endpoints/{index.ts → index.d.ts} +1 -2
  13. package/endpoints/raw/{BlobEndpoint.ts → BlobEndpoint.d.ts} +9 -29
  14. package/endpoints/raw/{UploadEndpoint.ts → UploadEndpoint.d.ts} +4 -18
  15. package/endpoints/reactive/Delay.d.ts +7 -0
  16. package/endpoints/reactive/GenericStreamingCollectionEndpoint.d.ts +32 -0
  17. package/endpoints/reactive/{PollingEndpoint.ts → PollingEndpoint.d.ts} +7 -34
  18. package/endpoints/reactive/SseStreamingEndpoint.d.ts +50 -0
  19. package/endpoints/reactive/{StreamingCollectionEndpoint.ts → StreamingCollectionEndpoint.d.ts} +2 -5
  20. package/endpoints/reactive/{StreamingEndpoint.ts → StreamingEndpoint.d.ts} +5 -27
  21. package/endpoints/rpc/{ActionEndpoint.ts → ActionEndpoint.d.ts} +3 -10
  22. package/endpoints/rpc/{ConsumerEndpoint.ts → ConsumerEndpoint.d.ts} +3 -12
  23. package/endpoints/rpc/{FunctionEndpoint.ts → FunctionEndpoint.d.ts} +3 -14
  24. package/endpoints/rpc/{ProducerEndpoint.ts → ProducerEndpoint.d.ts} +3 -11
  25. package/endpoints/rpc/{RpcEndpointBase.ts → RpcEndpointBase.d.ts} +4 -12
  26. package/errors/DefaultErrorHandler.d.ts +21 -0
  27. package/errors/{Errors.ts → Errors.d.ts} +20 -21
  28. package/http/EntityStream.d.ts +10 -0
  29. package/http/FetchHttpClient.d.ts +15 -0
  30. package/http/FetchHttpClient.js +1 -1
  31. package/http/{HttpClient.ts → HttpClient.d.ts} +1 -3
  32. package/http/HttpContentRange.d.ts +23 -0
  33. package/http/{HttpHeader.ts → HttpHeader.d.ts} +1 -1
  34. package/http/{HttpMethod.ts → HttpMethod.d.ts} +1 -1
  35. package/http/{HttpStatusCode.ts → HttpStatusCode.d.ts} +1 -1
  36. package/http/ResponseCache.d.ts +19 -0
  37. package/http/RetryAfter.d.ts +6 -0
  38. package/http/ServerSentEvents.d.ts +50 -0
  39. package/links/AggregateLinkExtractor.d.ts +17 -0
  40. package/links/HalLinkExtractor.d.ts +13 -0
  41. package/links/HeaderLinkExtractor.d.ts +13 -0
  42. package/links/{Link.ts → Link.d.ts} +6 -2
  43. package/links/{LinkExtractor.ts → LinkExtractor.d.ts} +0 -1
  44. package/package.json +64 -14
  45. package/serializers/JsonSerializer.d.ts +18 -0
  46. package/serializers/{Serializer.ts → Serializer.d.ts} +0 -2
  47. package/endpoints/Endpoint.test.ts +0 -300
  48. package/endpoints/Endpoint.ts +0 -247
  49. package/endpoints/generic/CollectionEndpoint.test.ts +0 -215
  50. package/endpoints/generic/ElementEndpoint.test.ts +0 -262
  51. package/endpoints/generic/GenericCollectionEndpoint.test.ts +0 -123
  52. package/endpoints/generic/IndexerEndpoint.test.ts +0 -12
  53. package/endpoints/generic/PartialResponse.ts +0 -28
  54. package/endpoints/raw/BlobEndpoint.test.ts +0 -52
  55. package/endpoints/raw/UploadEndpoint.test.ts +0 -47
  56. package/endpoints/reactive/Delay.ts +0 -26
  57. package/endpoints/reactive/GenericStreamingCollectionEndpoint.ts +0 -73
  58. package/endpoints/reactive/PollingEndpoint.test.ts +0 -84
  59. package/endpoints/reactive/SseStreamingEndpoint.test.ts +0 -169
  60. package/endpoints/reactive/SseStreamingEndpoint.ts +0 -115
  61. package/endpoints/reactive/StreamingCollectionEndpoint.test.ts +0 -110
  62. package/endpoints/reactive/StreamingEndpoint.test.ts +0 -130
  63. package/endpoints/rpc/ActionEndpoint.test.ts +0 -36
  64. package/endpoints/rpc/ConsumerEndpoint.test.ts +0 -29
  65. package/endpoints/rpc/FunctionEndpoint.test.ts +0 -29
  66. package/endpoints/rpc/ProducerEndpoint.test.ts +0 -32
  67. package/errors/DefaultErrorHandler.ts +0 -65
  68. package/http/EntityStream.ts +0 -40
  69. package/http/FetchHttpClient.ts +0 -26
  70. package/http/HttpContentRange.ts +0 -41
  71. package/http/ResponseCache.ts +0 -25
  72. package/http/RetryAfter.ts +0 -19
  73. package/http/ServerSentEvents.ts +0 -145
  74. package/jest.config.js +0 -12
  75. package/links/AggregateLinkExtractor.ts +0 -28
  76. package/links/HalLinkExtractor.ts +0 -45
  77. package/links/HeaderLinkExtractor.ts +0 -56
  78. package/serializers/JsonSerializer.ts +0 -25
  79. /package/endpoints/generic/{index.ts → index.d.ts} +0 -0
  80. /package/endpoints/raw/{index.ts → index.d.ts} +0 -0
  81. /package/endpoints/reactive/{index.ts → index.d.ts} +0 -0
  82. /package/endpoints/rpc/{index.ts → index.d.ts} +0 -0
  83. /package/errors/{ErrorHandler.ts → ErrorHandler.d.ts} +0 -0
  84. /package/errors/{index.ts → index.d.ts} +0 -0
  85. /package/http/{index.ts → index.d.ts} +0 -0
  86. /package/{index.ts → index.d.ts} +0 -0
  87. /package/links/{index.ts → index.d.ts} +0 -0
  88. /package/serializers/{index.ts → index.d.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typedrest",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "author": "Bastian Eicher",
5
5
  "license": "MIT",
6
6
  "description": "TypedRest helps you build type-safe fluent-style REST API clients.",
@@ -15,28 +15,78 @@
15
15
  },
16
16
  "homepage": "https://typedrest.net",
17
17
  "main": "index.js",
18
- "types": "index.ts",
18
+ "types": "index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./index.d.ts",
22
+ "default": "./index.js"
23
+ },
24
+ "./endpoints": {
25
+ "types": "./endpoints/index.d.ts",
26
+ "default": "./endpoints/index.js"
27
+ },
28
+ "./endpoints/generic": {
29
+ "types": "./endpoints/generic/index.d.ts",
30
+ "default": "./endpoints/generic/index.js"
31
+ },
32
+ "./endpoints/rpc": {
33
+ "types": "./endpoints/rpc/index.d.ts",
34
+ "default": "./endpoints/rpc/index.js"
35
+ },
36
+ "./endpoints/raw": {
37
+ "types": "./endpoints/raw/index.d.ts",
38
+ "default": "./endpoints/raw/index.js"
39
+ },
40
+ "./endpoints/reactive": {
41
+ "types": "./endpoints/reactive/index.d.ts",
42
+ "default": "./endpoints/reactive/index.js"
43
+ },
44
+ "./http": {
45
+ "types": "./http/index.d.ts",
46
+ "default": "./http/index.js"
47
+ },
48
+ "./errors": {
49
+ "types": "./errors/index.d.ts",
50
+ "default": "./errors/index.js"
51
+ },
52
+ "./serializers": {
53
+ "types": "./serializers/index.d.ts",
54
+ "default": "./serializers/index.js"
55
+ },
56
+ "./links": {
57
+ "types": "./links/index.d.ts",
58
+ "default": "./links/index.js"
59
+ },
60
+ "./package.json": "./package.json"
61
+ },
19
62
  "files": [
20
- "**/*.js",
21
- "**/*.ts",
22
- "!_doc.*",
23
- "!docs/"
63
+ "index.js",
64
+ "index.d.ts",
65
+ "endpoints/**/*.js",
66
+ "endpoints/**/*.d.ts",
67
+ "errors/**/*.js",
68
+ "errors/**/*.d.ts",
69
+ "http/**/*.js",
70
+ "http/**/*.d.ts",
71
+ "links/**/*.js",
72
+ "links/**/*.d.ts",
73
+ "serializers/**/*.js",
74
+ "serializers/**/*.d.ts",
75
+ "!**/_doc.*",
76
+ "!**/*.test.*"
24
77
  ],
25
78
  "scripts": {
26
79
  "build": "tsc",
27
- "test": "jest",
80
+ "test": "vitest run",
28
81
  "docs": "typedoc"
29
82
  },
30
83
  "devDependencies": {
31
- "@types/node": "26.4.1",
32
- "@types/jest": "30.0.0",
84
+ "@types/node": "26.6.6",
33
85
  "@types/url-template": "2.0.31",
34
- "jest": "30.5.1",
35
- "jest-environment-jsdom": "30.5.1",
36
- "jest-fetch-mock": "4.2.0",
37
- "ts-jest": "29.4.12",
38
86
  "typedoc": "0.28.20",
39
- "typescript": "6.0.3"
87
+ "typescript": "6.0.3",
88
+ "vitest": "5.1.0",
89
+ "vitest-fetch-mock": "0.4.5"
40
90
  },
41
91
  "dependencies": {
42
92
  "url-template": "^2.0.8"
@@ -0,0 +1,18 @@
1
+ import { Serializer } from "./Serializer";
2
+ /**
3
+ * Serializes and deserializes entities as JSON.
4
+ */
5
+ export declare class JsonSerializer implements Serializer {
6
+ /**
7
+ * @inheritDoc
8
+ */
9
+ readonly supportedMediaTypes: string[];
10
+ /**
11
+ * @inheritDoc
12
+ */
13
+ serialize<T>(entity: T): string;
14
+ /**
15
+ * @inheritDoc
16
+ */
17
+ deserialize<T>(text: string): T;
18
+ }
@@ -6,14 +6,12 @@ export interface Serializer {
6
6
  * A list of MIME types this serializer supports.
7
7
  */
8
8
  readonly supportedMediaTypes: string[];
9
-
10
9
  /**
11
10
  * Serializes an entity.
12
11
  * @typeParam T The type of entity to serialize.
13
12
  * @param entity The entity to serialize.
14
13
  */
15
14
  serialize<T>(entity: T): string;
16
-
17
15
  /**
18
16
  * Deserializes an entity.
19
17
  * @typeParam T The type of entity to deserialize.
@@ -1,300 +0,0 @@
1
- import fetchMock from 'jest-fetch-mock';
2
- import { Endpoint, EntryEndpoint } from '.';
3
- import { HttpMethod, HttpHeader, HttpStatusCode } from '../http';
4
- import { ActionEndpoint } from './rpc';
5
- import { ConflictError } from '../errors';
6
-
7
- class CustomEndpoint extends Endpoint {
8
- async get() { await this.send(HttpMethod.Get); }
9
-
10
- isMethodAllowedPublic(method: HttpMethod) { return this.isMethodAllowed(method); }
11
- }
12
-
13
- fetchMock.enableMocks();
14
- let endpoint: CustomEndpoint;
15
-
16
- beforeEach(() => {
17
- fetchMock.resetMocks();
18
- endpoint = new CustomEndpoint(new EntryEndpoint('http://localhost/'), 'endpoint');
19
- });
20
-
21
- test('acceptHeader', async () => {
22
- fetchMock.mockOnceIf(
23
- req => req.url === 'http://localhost/endpoint',
24
- async req => {
25
- expect(req.headers.get(HttpHeader.Accept)).toBe('application/json');
26
- return {};
27
- }
28
- );
29
-
30
- await endpoint.get();
31
- });
32
-
33
- test('allowHeader', async () => {
34
- fetchMock.mockOnceIf('http://localhost/endpoint',
35
- '{}',
36
- {
37
- headers: {
38
- [HttpHeader.Allow]: HttpMethod.Put + ", " + HttpMethod.Post
39
- }
40
- });
41
- await endpoint.get();
42
-
43
- expect(endpoint.isMethodAllowedPublic(HttpMethod.Put)).toBe(true);
44
- expect(endpoint.isMethodAllowedPublic(HttpMethod.Post)).toBe(true);
45
- expect(endpoint.isMethodAllowedPublic(HttpMethod.Delete)).toBe(false);
46
- });
47
-
48
- test('link', async () => {
49
- fetchMock.mockOnceIf('http://localhost/endpoint',
50
- '{}',
51
- {
52
- headers: {
53
- [HttpHeader.Link]: '<a>; rel=target1, <b>; rel=target2'
54
- }
55
- });
56
- await endpoint.get();
57
-
58
- expect(endpoint.link('target1')).toEqual(new URL('http://localhost/a'));
59
- expect(endpoint.link('target2')).toEqual(new URL('http://localhost/b'));
60
- });
61
-
62
- test('linkAbsolute', async () => {
63
- fetchMock.mockOnceIf('http://localhost/endpoint',
64
- '{}',
65
- {
66
- headers: {
67
- [HttpHeader.Link]: '<http://localhost/b>; rel=target1'
68
- }
69
- });
70
- await endpoint.get();
71
-
72
- expect(endpoint.link('target1')).toEqual(new URL('http://localhost/b'));
73
- });
74
-
75
- test('linkError', async () => {
76
- fetchMock.mockOnceIf('http://localhost/endpoint',
77
- '{}',
78
- {
79
- headers: {
80
- [HttpHeader.Link]: '<http://localhost/a>; rel=target1'
81
- }
82
- });
83
- await endpoint.get();
84
-
85
- expect(() => endpoint.link('target2')).toThrow();
86
- });
87
-
88
- test('getLinks', async () => {
89
- fetchMock.mockOnceIf('http://localhost/endpoint',
90
- '{}',
91
- {
92
- headers: {
93
- [HttpHeader.Link]: '<target1>; rel=child; title=Title, <target2>; rel=child'
94
- }
95
- });
96
- await endpoint.get();
97
-
98
- expect(endpoint.getLinks('child')).toEqual([
99
- { uri: new URL('http://localhost/target1'), title: 'Title' },
100
- { uri: new URL('http://localhost/target2') }
101
- ]);
102
- });
103
-
104
- test('getLinksEscaping', async () => {
105
- fetchMock.mockOnceIf('http://localhost/endpoint',
106
- '{}',
107
- {
108
- headers: {
109
- [HttpHeader.Link]: '<target1>; rel=child; title="Title,= 1", <target2>; rel=child'
110
- }
111
- });
112
- await endpoint.get();
113
-
114
- expect(endpoint.getLinks('child')).toEqual([
115
- { uri: new URL('http://localhost/target1'), title: 'Title,= 1' },
116
- { uri: new URL('http://localhost/target2') }
117
- ]);
118
- });
119
-
120
- test('setDefaultLink', () => {
121
- endpoint.setDefaultLink('child', 'target');
122
- expect(endpoint.link('child')).toEqual(new URL('http://localhost/target'));
123
- });
124
-
125
- test('linkTemplate', async () => {
126
- fetchMock.mockOnceIf('http://localhost/endpoint',
127
- '{}',
128
- {
129
- headers: {
130
- [HttpHeader.Link]: '<a{?x}>; rel=child; templated=true'
131
- }
132
- });
133
- await endpoint.get();
134
-
135
- expect(endpoint.getLinkTemplate('child')).toBe('a{?x}');
136
- });
137
-
138
- test('linkTemplateResolve', async () => {
139
- fetchMock.mockOnceIf('http://localhost/endpoint',
140
- '{}',
141
- {
142
- headers: {
143
- [HttpHeader.Link]: '<a{?x}>; rel=child; templated=true'
144
- }
145
- });
146
- await endpoint.get();
147
-
148
- expect(endpoint.linkTemplate('child', { x: '1' }))
149
- .toEqual(new URL('http://localhost/a?x=1'));
150
- });
151
-
152
- test('linkTemplateResolveAbsolute', async () => {
153
- fetchMock.mockOnceIf('http://localhost/endpoint',
154
- '{}',
155
- {
156
- headers: {
157
- [HttpHeader.Link]: '<http://localhost/b{?x}>; rel=child; templated=true'
158
- }
159
- });
160
- await endpoint.get();
161
-
162
- expect(endpoint.linkTemplate('child', { x: '1' }))
163
- .toEqual(new URL('http://localhost/b?x=1'));
164
- });
165
-
166
- test('linkTemplateResolveQuery', async () => {
167
- fetchMock.mockOnceIf('http://localhost/endpoint',
168
- '{}',
169
- {
170
- headers: {
171
- [HttpHeader.Link]: '<http://localhost/b{?x,y}>; rel=search; templated=true'
172
- }
173
- });
174
- await endpoint.get();
175
-
176
- expect(endpoint.linkTemplate('search', { x: '1', y: '2' }))
177
- .toEqual(new URL('http://localhost/b?x=1&y=2'));
178
- });
179
-
180
- test('linkTemplateError', async () => {
181
- fetchMock.mockOnceIf('http://localhost/endpoint',
182
- '{}',
183
- {
184
- headers: {
185
- [HttpHeader.Link]: '<a>; rel=child; templated=true'
186
- }
187
- });
188
- await endpoint.get();
189
-
190
- expect(() => endpoint.getLinkTemplate('child2')).toThrow();
191
- });
192
-
193
- test('linkHal', async () => {
194
- fetchMock.mockOnceIf('http://localhost/endpoint',
195
- JSON.stringify({
196
- _links: {
197
- single: { href: 'a' },
198
- collection: [{ href: 'b', title: 'Title 1' }, { href: 'c' }],
199
- template: [{ href: '{id}', templated: true }]
200
- }
201
- }),
202
- {
203
- headers: {
204
- [HttpHeader.ContentType]: 'application/hal+json'
205
- }
206
- });
207
- await endpoint.get();
208
-
209
- expect(endpoint.link('single')).toEqual(new URL('http://localhost/a'));
210
- expect(endpoint.getLinks('collection')).toEqual([
211
- { uri: new URL('http://localhost/b'), title: 'Title 1' },
212
- { uri: new URL('http://localhost/c') }
213
- ]);
214
- expect(endpoint.getLinkTemplate('template')).toEqual('{id}');
215
- });
216
-
217
- test('setDefaultLinkTemplate', () => {
218
- endpoint.setDefaultLinkTemplate('child', 'a');
219
- expect(endpoint.getLinkTemplate('child')).toBe('a');
220
- });
221
-
222
- test('ensureTrailingSlashOnReferrerUri', () => {
223
- expect(new ActionEndpoint(endpoint, 'subresource').uri).toEqual(new URL('http://localhost/subresource'));
224
- expect(new ActionEndpoint(endpoint, './subresource').uri).toEqual(new URL('http://localhost/endpoint/subresource'));
225
- });
226
-
227
- test('errorHandlingWithNoContent', async () => {
228
- fetchMock.mockOnceIf('http://localhost/endpoint',
229
- '{}',
230
- {
231
- status: HttpStatusCode.Conflict
232
- });
233
-
234
- let errorThrown = false;
235
- try {
236
- await endpoint.get();
237
- } catch (err) {
238
- errorThrown = err instanceof ConflictError;
239
- }
240
- expect(errorThrown).toBe(true);
241
- });
242
-
243
- test('errorHandlingWithMessage', async () => {
244
- fetchMock.mockOnceIf('http://localhost/endpoint',
245
- '{"message":"my message","extra":"info"}',
246
- {
247
- status: HttpStatusCode.Conflict,
248
- headers: {
249
- [HttpHeader.ContentType]: 'application/json'
250
- }
251
- });
252
-
253
- let errorThrown = false;
254
- try {
255
- await endpoint.get();
256
- } catch (err) {
257
- errorThrown = err instanceof ConflictError
258
- && err.message === 'my message'
259
- && err.data.extra === 'info';
260
- }
261
- expect(errorThrown).toBe(true);
262
- });
263
-
264
- test('errorHandlingWithArray', async () => {
265
- fetchMock.mockOnceIf('http://localhost/endpoint',
266
- '[{"message":"my message"}]',
267
- {
268
- status: HttpStatusCode.Conflict,
269
- headers: {
270
- [HttpHeader.ContentType]: 'application/json'
271
- }
272
- });
273
-
274
- let errorThrown = false;
275
- try {
276
- await endpoint.get();
277
- } catch (err) {
278
- errorThrown = err instanceof ConflictError;
279
- }
280
- expect(errorThrown).toBe(true);
281
- });
282
-
283
- test('errorHandlingWithUnknownContentType', async () => {
284
- fetchMock.mockOnceIf('http://localhost/endpoint',
285
- '...',
286
- {
287
- status: HttpStatusCode.Conflict,
288
- headers: {
289
- [HttpHeader.ContentType]: 'dummy/type'
290
- }
291
- });
292
-
293
- let errorThrown = false;
294
- try {
295
- await endpoint.get();
296
- } catch (err) {
297
- errorThrown = err instanceof ConflictError;
298
- }
299
- expect(errorThrown).toBe(true);
300
- });
@@ -1,247 +0,0 @@
1
- import { HttpMethod, HttpHeader, HttpStatusCode, HttpClient } from "../http";
2
- import { Serializer } from "../serializers";
3
- import { ErrorHandler, NotFoundError } from "../errors";
4
- import { LinkExtractor, Link } from "../links";
5
- import template from 'url-template';
6
-
7
- export class Endpoint {
8
- /**
9
- * The HTTP URI of the remote resource.
10
- */
11
- readonly uri: URL;
12
-
13
- /**
14
- * The HTTP client used to communicate with the remote resource.
15
- */
16
- readonly httpClient: HttpClient;
17
-
18
- /**
19
- * Controls the serialization of entities sent to and received from the server.
20
- */
21
- readonly serializer: Serializer;
22
-
23
- /**
24
- * Handles errors in responses.
25
- */
26
- readonly errorHandler: ErrorHandler;
27
-
28
- /**
29
- * Extracts links from responses.
30
- */
31
- readonly linkExtractor: LinkExtractor;
32
-
33
- /**
34
- * Creates a new endpoint.
35
- * @param referrer The endpoint used to navigate to this one. Must be defined except for top-level endpoint.
36
- * @param uri The HTTP URI of the remote element. May be relative if `referrer` is defined.
37
- * @param serializer Controls the serialization of entities sent to and received from the server. Taken from `referrer` instead if it is defined.
38
- * @param errorHandler Handles errors in responses. Taken from `referrer` instead if it is defined.
39
- * @param linkExtractor Extracts links from responses. Taken from `referrer` instead if it is defined.
40
- * @param httpClient The HTTP client used to communicate with the remote resource. Taken from `referrer` instead if it is defined.
41
- */
42
- constructor(
43
- referrer: Endpoint | undefined,
44
- uri: URL | string,
45
- serializer?: Serializer,
46
- errorHandler?: ErrorHandler,
47
- linkExtractor?: LinkExtractor,
48
- httpClient?: HttpClient) {
49
- if (referrer) {
50
- this.uri = (typeof uri === "string") ? referrer.join(uri) : uri;
51
- if (serializer)
52
- throw new Error("serializer must not be specified if referrer is specified.");
53
- this.serializer = referrer.serializer;
54
- if (errorHandler)
55
- throw new Error("errorHandler must not be specified if referrer is specified.");
56
- this.errorHandler = referrer.errorHandler;
57
- if (linkExtractor)
58
- throw new Error("linkExtractor must not be specified if referrer is specified.");
59
- this.linkExtractor = referrer.linkExtractor;
60
- if (httpClient)
61
- throw new Error("httpClient must not be specified if referrer is specified.");
62
- this.httpClient = referrer.httpClient;
63
- } else {
64
- this.uri = (typeof uri === "string") ? new URL(uri) : uri;
65
- if (!serializer)
66
- throw new Error("serializer must be specified if referrer is not specified.");
67
- this.serializer = serializer;
68
- if (!errorHandler)
69
- throw new Error("errorHandler must be specified if referrer is not specified.");
70
- this.errorHandler = errorHandler;
71
- if (!linkExtractor)
72
- throw new Error("linkExtractor must be specified if referrer is not specified.");
73
- this.linkExtractor = linkExtractor;
74
- if (!httpClient)
75
- throw new Error("httpClient must be specified if referrer is not specified.");
76
- this.httpClient = httpClient;
77
- }
78
- }
79
-
80
- /**
81
- * Resolves a relative URI using this endpoint's URI as the base.
82
- * @param relativeUri The relative URI to resolve. Prepend `./` to imply a trailing slash in the base URI even if it is missing there.
83
- */
84
- protected join(relativeUri: string): URL {
85
- return new URL(
86
- relativeUri,
87
- relativeUri.startsWith("./") ? Endpoint.ensureTrailingSlash(this.uri) : this.uri);
88
- }
89
-
90
- /**
91
- * Adds a trailing slash to the URI if it does not already have one.
92
- */
93
- protected static ensureTrailingSlash(uri: URL | string): URL {
94
- let uriString = (typeof uri === "string") ? uri : uri.href;
95
- if (!uriString.endsWith("/"))
96
- uriString += "/";
97
- return new URL(uriString);
98
- }
99
-
100
- /**
101
- * Sends an HTTP request to this endpoint's URI.
102
- * Handles various cross-cutting concerns regarding a response message such as discovering links and handling errors.
103
- * @param method The HTTP method to use.
104
- * @param signal Used to cancel the request.
105
- * @param headers The HTTP headers to set.
106
- * @param body The body to send.
107
- * @throws {@link errors!HttpError}
108
- */
109
- protected async send(method: HttpMethod, signal?: AbortSignal, headers?: HeadersInit, body?: BodyInit): Promise<Response> {
110
- const response = await this.httpClient.send(this.uri, method, signal, headers, body);
111
- await this.handle(response);
112
- return response;
113
- }
114
-
115
- /**
116
- * Handles various cross-cutting concerns regarding a response message such as discovering links and handling errors.
117
- * @param response The response to process.
118
- * @throws {@link errors!HttpError}
119
- */
120
- protected async handle(response: Response) {
121
- this.links = await this.linkExtractor.getLinks(response);
122
- this.handleCapabilities(response);
123
- await this.errorHandler.handle(response);
124
- }
125
-
126
- // NOTE: Always replace entire array rather than modifying it to avoid async issues.
127
- private links: Link[] = [];
128
-
129
- // NOTE: Only modified during initial setup of the endpoint.
130
- private defaultLinks = new Map<string, URL>();
131
- private defaultLinkTemplates = new Map<string, string>();
132
-
133
- /**
134
- * Registers one or more default links for a specific relation type.
135
- * These links are used when no links with this relation type are provided by the server.
136
- * This should only be called during initial setup of the endpoint.
137
- * @param rel The relation type of the link to add.
138
- * @param href The href of the link relative to this endpoint's URI. Leave unspecified to remove any previous entries for the relation type.
139
- */
140
- setDefaultLink(rel: string, href?: string) {
141
- if (href) {
142
- this.defaultLinks.set(rel, this.join(href));
143
- } else {
144
- this.defaultLinks.delete(rel);
145
- }
146
- }
147
-
148
- /**
149
- * Registers a default link template for a specific relation type.
150
- * This template is used when no template with this relation type is provided by the server.
151
- * This should only be called during initial setup of the endpoint.
152
- * @param rel The relation type of the link to add.
153
- * @param href The templates href relative to this endpoint's URI. Leave unspecified to remove any previous entries for the relation type.
154
- */
155
- setDefaultLinkTemplate(rel: string, href?: string) {
156
- if (href) {
157
- this.defaultLinkTemplates.set(rel, href);
158
- } else {
159
- this.defaultLinkTemplates.delete(rel);
160
- }
161
- }
162
-
163
- /**
164
- * Resolves all links with a specific relation type. Uses cached data from last response.
165
- * @param rel The relation type of the links to look for.
166
- */
167
- getLinks(rel: string): { uri: URL; title?: string; }[] {
168
- const links: { uri: URL; title?: string; }[] = this.links
169
- .filter(x => !x.templated && x.rel === rel)
170
- .map(x => {
171
- return { uri: this.join(x.href), title: x.title };
172
- });
173
-
174
- const defaultLink = this.defaultLinks.get(rel);
175
- if (links.length === 0 && defaultLink) {
176
- links.push({ uri: defaultLink });
177
- }
178
-
179
- return links;
180
- }
181
-
182
- /**
183
- * Resolves a single link with a specific relation type. Uses cached data from last response.
184
- * @param rel The relation type of the link to look for.
185
- * @throws {@link errors!NotFoundError}: No link with the specified `rel` could be found.
186
- */
187
- link(rel: string): URL {
188
- const links = this.getLinks(rel);
189
-
190
- if (links.length === 0)
191
- throw new NotFoundError(`No link with rel=${rel} provided by endpoint ${this.uri}.`, HttpStatusCode.NotFound);
192
-
193
- return links[0].uri;
194
- }
195
-
196
- /**
197
- * Resolves a link template with a specific relation type. Uses cached data from last response.
198
- * @param rel The relation type of the link template to look for.
199
- * @param variables Variables for resolving the template.
200
- * @throws {@link errors!NotFoundError}: No link template with the specified `rel` could be found.
201
- */
202
- linkTemplate(rel: string, variables: { [key: string]: any; }): URL {
203
- const tmpl = template.parse(this.getLinkTemplate(rel))
204
- return new URL(this.join(tmpl.expand(variables)))
205
- }
206
-
207
- /**
208
- * Retrieves a link template with a specific relation type. Uses cached data from last response. Prefer {@link linkTemplate} when possible.
209
- * @param rel The relation type of the link template to look for.
210
- * @throws {@link errors!NotFoundError}: No link template with the specified `rel` could be found.
211
- */
212
- getLinkTemplate(rel: string) {
213
- const template = this.links.find(x => x.templated && x.rel === rel)?.href
214
- ?? this.defaultLinkTemplates.get(rel);
215
-
216
- if (!template)
217
- throw new NotFoundError(`No link template with rel=${rel} provided by endpoint ${this.uri}.`, HttpStatusCode.NotFound);
218
-
219
- return template;
220
- }
221
-
222
- // NOTE: Always replace entire array rather than modifying it to avoid async issues.
223
- private allowedMethods: HttpMethod[] = [];
224
-
225
- /**
226
- * Handles allowed HTTP methods and other capabilities reported by the server.
227
- */
228
- protected handleCapabilities(response: Response) {
229
- const header = response.headers.get(HttpHeader.Allow);
230
- if (header) {
231
- this.allowedMethods = header.split(", ") as HttpMethod[];
232
- }
233
- }
234
-
235
- /**
236
- * Shows whether the server has indicated that a specific HTTP method is currently allowed.
237
- * Uses cached data from last response.
238
- * @param method The HTTP methods (e.g. GET, POST, ...) to check.
239
- * @returns `true` if the method is allowed, `false` if the method is not allowed, `undefined` if no request has been sent yet or the server did not specify allowed methods.
240
- */
241
- protected isMethodAllowed(method: HttpMethod): boolean | undefined {
242
- if (this.allowedMethods.length === 0)
243
- return undefined;
244
-
245
- return this.allowedMethods.indexOf(method) !== -1;
246
- }
247
- }