1 | Ketting - A hypermedia client for javascript
|
2 | ============================================
|
3 |
|
4 | [![Greenkeeper badge](https://badges.greenkeeper.io/evert/ketting.svg)](https://greenkeeper.io/)
|
5 |
|
6 | Introduction
|
7 | ------------
|
8 |
|
9 | The Ketting library is an attempt at creating a 'generic' hypermedia client, it
|
10 | supports an opinionated set of modern features REST services might have.
|
11 |
|
12 | The library supports [HAL][hal], [Web Linking (HTTP Link Header)][1] and HTML5
|
13 | links. It uses the Fetch API and works both in the browsers and in node.js.
|
14 |
|
15 | ### Example
|
16 |
|
17 | ```js
|
18 | const ketting = new Ketting('https://api.example.org/');
|
19 |
|
20 | // Follow a link with rel="author". This could be a HTML5 `<link>`, a
|
21 | // HAL `_links` or a HTTP `Link:`.
|
22 | const author = await ketting.follow('author');
|
23 |
|
24 | // Grab the current state
|
25 | const authorState = await author.get();
|
26 |
|
27 | // Change the firstName property of the object. Note that this assumes JSON.
|
28 | authorState.firstName = 'Evert';
|
29 |
|
30 | // Save the new state
|
31 | await author.put(authorState);
|
32 | ```
|
33 |
|
34 | Installation
|
35 | ------------
|
36 |
|
37 | npm install ketting
|
38 |
|
39 | or:
|
40 |
|
41 | yarn add ketting
|
42 |
|
43 |
|
44 | Features overview
|
45 | -----------------
|
46 |
|
47 | Ketting is a library that sits on top of a [Fetch API][3] to provide a RESTful
|
48 | interface and make it easier to follow REST best practices more strictly.
|
49 |
|
50 | It provides some useful abstractions that make it easier to work with true
|
51 | hypermedia / HATEAOS servers. It currently parses [HAL][hal] and has a deep
|
52 | understanding of links and embedded resources. There's also support for parsing
|
53 | and following links from HTML documents, and it understands the HTTP `Link:`
|
54 | header.
|
55 |
|
56 | Using this library it becomes very easy to follow links from a single bookmark,
|
57 | and discover resources and features on the server.
|
58 |
|
59 | Supported formats:
|
60 |
|
61 | * [HAL][hal]
|
62 | * HTML - Can automatically follow `<link>` and `<a>` element with `rel=`
|
63 | attributes.
|
64 | * [HTTP Link header][1] - automatically registers as links regardless of format.
|
65 | * [JSON:API][jsonapi] - Understands the `links` object and registers collection
|
66 | members as `item` relationships.
|
67 | * [application/problem+json][problem] - Will extract useful information from
|
68 | the standard problem object and embed them in exception objects.
|
69 |
|
70 |
|
71 | ### Following links
|
72 |
|
73 | One core tenet of building a good REST service, is that URIs should be
|
74 | discovered, not hardcoded in an application. It's for this reason that the
|
75 | emphasis in this library is _not_ on URIs (like most libraries) but on
|
76 | relation-types (the `rel`) and links.
|
77 |
|
78 | Generally when interacting with a REST service, you'll want to only hardcode
|
79 | a single URI (a bookmark) and discover all the other APIs from there on on.
|
80 |
|
81 | For example, consider that there is a some API at `https://api.example.org/`.
|
82 | This API has a link to an API for news articles (`rel="articleCollection"`),
|
83 | which has a link for creating a new article (`rel="new"`). When `POST`ing on
|
84 | that uri, the api returns `201 Created` along with a `Location` header pointing
|
85 | to the new article. On this location, a new `rel="author"` appears
|
86 | automatically, pointing to the person that created the article.
|
87 |
|
88 | This is how that interaction might look like:
|
89 |
|
90 | ```js
|
91 | const ketting = new Ketting('https://api.example.org/');
|
92 | const createArticle = await ketting.follow('articleCollection').follow('new'); // chained follow
|
93 |
|
94 | const newArticle = await createArticle.post({ title: 'Hello world' });
|
95 | const author = await newArticle.follow('author');
|
96 |
|
97 | // Output author information
|
98 | console.log(await author.get());
|
99 | ```
|
100 |
|
101 | ### Embedded resources
|
102 |
|
103 | Embedded resources are a HAL feature. In situations when you are modeling a
|
104 | 'collection' of resources, in HAL you should generally just create links to
|
105 | all the items in the collection. However, if a client wants to fetch all these
|
106 | items, this can result in a lot of HTTP requests. HAL uses `_embedded` to work
|
107 | around this. Using `_embedded` a user can effectively tell the HAL client about
|
108 | the links in the collection and immediately send along the contents of those
|
109 | resources, thus avoiding the overhead.
|
110 |
|
111 | Ketting understands `_embedded` and completely abstracts them away. If you use
|
112 | Ketting with a HAL server, you can therefore completely ignore them.
|
113 |
|
114 | For example, given a collection resource with many resources that hal the
|
115 | relationshiptype `item`, you might use the following API:
|
116 |
|
117 | ```js
|
118 | const ketting = new Ketting('https://api.example.org/');
|
119 | const articleCollection = await ketting.follow('articleCollection');
|
120 |
|
121 | const items = await someCollection.followAll('item');
|
122 |
|
123 | for (const item of items) {
|
124 | console.log(await item.get());
|
125 | }
|
126 | ```
|
127 |
|
128 | Given the last example, if the server did _not_ use embedding, it will result
|
129 | in a HTTP GET request for every item in the collection.
|
130 |
|
131 | If the server _did_ use embedding, there will only be 1 GET request.
|
132 |
|
133 | A major advantage of this, is that it allows a server to be upgradable. Hot
|
134 | paths might be optimized using embedding, and the client seamlessly adjusts
|
135 | to the new information.
|
136 |
|
137 | Further reading:
|
138 |
|
139 | * [Further reading](https://evertpot.com/rest-embedding-hal-http2/).
|
140 | * [Hypertext Cache Pattern in HAL spec](https://tools.ietf.org/html/draft-kelly-json-hal-08#section-8.3).
|
141 |
|
142 |
|
143 | Automatically parsing problem+json
|
144 | ----------------------------------
|
145 |
|
146 | If your server emits application/problem+json documents ([RFC7807][problem])
|
147 | on HTTP errors, the library will automatically extract the information from
|
148 | that object, and also provide a better exception message (if the title
|
149 | property is provided).
|
150 |
|
151 |
|
152 | Node and Browser
|
153 | ----------------
|
154 |
|
155 | Ketting works on any stable node.js version and modern browsers. To run Ketting
|
156 | in a browser, the following must be supported by a browser:
|
157 |
|
158 | * The [Fetch API][3].
|
159 | * Promises (async/await is not required)
|
160 |
|
161 |
|
162 | API
|
163 | ---
|
164 |
|
165 | ### `Ketting`
|
166 |
|
167 | The 'Ketting' class is the main class you'll use to access anything else.
|
168 |
|
169 | #### Constructor
|
170 |
|
171 | ```js
|
172 | const options = {}; // options are optional
|
173 | const ketting = new Ketting('https://api.example.org/', options);
|
174 | ```
|
175 |
|
176 | 2 keys or `options` are currently supported: `auth` and `fetchInit`.
|
177 |
|
178 | `auth` can be used to specify authentication information. Supported
|
179 | authentication methods are:
|
180 |
|
181 | * HTTP Basic auth
|
182 | * OAuth2 Bearer tokens
|
183 | * OAuth2 Managed client
|
184 |
|
185 | Basic example:
|
186 |
|
187 | ```js
|
188 | const options = {
|
189 | auth: {
|
190 | type: 'basic',
|
191 | userName: 'foo',
|
192 | password: 'bar'
|
193 | }
|
194 | };
|
195 | ```
|
196 |
|
197 | OAuth2 [Resource Owner Password Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.3) example:
|
198 |
|
199 |
|
200 | ```js
|
201 | const options = {
|
202 | auth: {
|
203 | type: 'oauth2',
|
204 | grant_type: 'password',
|
205 | clientId: 'fooClient',
|
206 | clientSecret: 'barSecret',
|
207 | tokenEndpointUri: 'https://api.example.org/oauth/token',
|
208 | scopes: ['test']
|
209 | userName: 'fooOwner',
|
210 | password: 'barPassword'
|
211 | }
|
212 | };
|
213 | ```
|
214 |
|
215 | OAuth 2 [Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4) example:
|
216 |
|
217 |
|
218 | ```js
|
219 | const options = {
|
220 | auth: {
|
221 | type: 'oauth2',
|
222 | grant_type: 'client_credentials',
|
223 | clientId: 'fooClient',
|
224 | clientSecret: 'barSecret',
|
225 | tokenEndpointUri: 'https://api.example.org/oauth/token',
|
226 | scopes: ['test']
|
227 | }
|
228 | };
|
229 | ```
|
230 |
|
231 | OAuth 2 [Authorization Code Grant](https://tools.ietf.org/html/rfc6749#section-4.1) example:
|
232 |
|
233 | ```js
|
234 | const options = {
|
235 | auth: {
|
236 | type: 'oauth2',
|
237 | grant_type: 'authorization_code',
|
238 | clientId: 'fooClient',
|
239 | code: '...',
|
240 | tokenEndpointUri: 'https://api.example.org/oauth/token',
|
241 | }
|
242 | };
|
243 | ```
|
244 |
|
245 | It's also possible to just setup the client with just an OAuth2 access token
|
246 | (and optionally a refresh token):
|
247 |
|
248 | ```js
|
249 | const options = {
|
250 | auth: {
|
251 | type: 'oauth2',
|
252 | clientId: 'fooClient',
|
253 | clientSecret: '...', // Sometimes optional
|
254 | accessToken: '...',
|
255 | refreshToken: '...', // Optional.
|
256 | tokenEndpointUri: 'https://api.example.org/oauth/token',
|
257 | }
|
258 | };
|
259 | ```
|
260 |
|
261 | The `fetchInit` option is a default list of settings that's automatically
|
262 | passed to `fetch()`. This is especially useful in a browser, where there's a
|
263 | few more settings highly relevant to the security sandbox.
|
264 |
|
265 | For example, to ensure that the browser automatically passed relevant cookies
|
266 | to the endpoint, you would specify this as such:
|
267 |
|
268 | ```js
|
269 | const options = {
|
270 | fetchInit : {
|
271 | credentials: 'include'
|
272 | }
|
273 | };
|
274 | ```
|
275 |
|
276 | Other options that you may want to set might be `mode` or `cache`. See the
|
277 | documentation for the [Request constructor](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request)
|
278 | for the full list.
|
279 |
|
280 |
|
281 | #### `Ketting.getResource()`
|
282 |
|
283 | Return a 'resource' object, based on it's url. If the url is not supplied,
|
284 | a resource will be returned pointing to the bookmark.
|
285 |
|
286 | If a relative url is given, it will be resolved based on the bookmark uri.
|
287 |
|
288 | ```js
|
289 | const resource = client.getResource('http://example.org'); // absolute uri
|
290 | const resource = client.getResource('/foo'); // relative uri
|
291 | const resource = client.getResource(); // bookmark
|
292 | ```
|
293 |
|
294 | The resource is returned immediately, and not as a promise.
|
295 |
|
296 | #### `Ketting.follow()`
|
297 |
|
298 | The `follow` function on the `Ketting` follows a link based on it's relation
|
299 | type from the bookmark resource.
|
300 |
|
301 | ```js
|
302 | const someResource = await ketting.follow('author');
|
303 | ```
|
304 |
|
305 | This is just a shortcut to:
|
306 |
|
307 | ```js
|
308 | const someResource = await ketting.getResource().follow('author');
|
309 | ```
|
310 |
|
311 | #### `Ketting.fetch`
|
312 |
|
313 | The `fetch` function is a wrapper for the new [Fetch][3] web standard. This
|
314 | function takes the same arguments (`input` and `init`), but it decorates the
|
315 | HTTP request with Authentication headers.
|
316 |
|
317 | ```js
|
318 | const response = await ketting.fetch('https://example.org');
|
319 | ```
|
320 |
|
321 | ### Resource
|
322 |
|
323 | The `Resource` class is the most important object, and represents a REST
|
324 | resource. Functions such `follow` and `getResource` always return `Resource`
|
325 | objects.
|
326 |
|
327 | #### `Resource.uri`
|
328 |
|
329 | Returns the current uri of the resource. This is a property, not a function
|
330 | and is always available.
|
331 |
|
332 | #### `Resource.contentType`
|
333 |
|
334 | A property representing the `Content-Type` of the resource. This value will
|
335 | be used in `GET` requests (with the `Accept` header) and `PUT` requests (with
|
336 | the `Content-Type` header).
|
337 |
|
338 | The `contentType` might be available immediately if the current resource was
|
339 | followed from a link that had "type" information. If it's not available, it
|
340 | might be determined later, after the first `GET` request is done.
|
341 |
|
342 | #### `Resource.get()`
|
343 |
|
344 | Returns the result of a `GET` request. This function returns a `Promise`.
|
345 |
|
346 | ```js
|
347 | await resource.get();
|
348 | ```
|
349 |
|
350 | If the resource was fetched earlier, it will return a cached copy.
|
351 |
|
352 | #### `Resource.put()`
|
353 |
|
354 | Updates the resource with a new representation
|
355 |
|
356 | ```js
|
357 | await resource.put({ foo: 'bar' });
|
358 | ```
|
359 |
|
360 |
|
361 | #### `Resource.delete()`
|
362 |
|
363 | Deletes the resource.
|
364 |
|
365 | ```js
|
366 | await resource.delete();
|
367 | ````
|
368 |
|
369 | This function returns a Promise that resolves to `null`.
|
370 |
|
371 | #### `Resource.post()`
|
372 |
|
373 | This function is meant to be an easy way to create new resources. It's not
|
374 | necessarily for any type of `POST` request, but it is really meant as a
|
375 | convenience method APIs that follow the typical pattern of using `POST` for
|
376 | creation.
|
377 |
|
378 | If the HTTP response from the server was successful and contained a `Location`
|
379 | header, this method will resolve into a new Resource. For example, this might
|
380 | create a new resource and then get a list of links after creation:
|
381 |
|
382 | ```js
|
383 | const newResource = await parentResource.post({ foo: 'bar' });
|
384 | // Output a list of links on the newly created resource
|
385 | console.log(await newResource.links());
|
386 | ```
|
387 |
|
388 | #### `Resource.patch()`
|
389 |
|
390 | This function provides a really simply implementation of the `PATCH` method.
|
391 | All it does is encode the body to JSON and set the `Content-Type` to
|
392 | `application/json`. I'm curious to hear use-cases for this, so open a ticket
|
393 | if this doesn't cut it!
|
394 |
|
395 | ```js
|
396 | await resource.patch({
|
397 | foo: 'bar'
|
398 | });
|
399 | ```
|
400 |
|
401 | #### `Resource.refresh()`
|
402 |
|
403 | The `refresh` function behaves the same as the `get()` function, but it ignores
|
404 | the cache. It's equivalent to a user hitting the "refresh" button in a browser.
|
405 |
|
406 | This function is useful to ditching the cache of a specific resource if the
|
407 | server state has changed.
|
408 |
|
409 | ```js
|
410 | console.log(await resource.refresh());
|
411 | ```
|
412 |
|
413 | #### `Resource.links()`
|
414 |
|
415 | Returns a list of `Link` objects for the resource.
|
416 |
|
417 | ```js
|
418 | console.log(await resource.links());
|
419 | ```
|
420 |
|
421 | You can also request only the links for a relation-type you are interested in:
|
422 |
|
423 | ```js
|
424 | resource.links('author'); // Get links with rel=author
|
425 | ```
|
426 |
|
427 |
|
428 | #### `Resource.follow()`
|
429 |
|
430 | Follows a link, by it's relation-type and returns a new resource for the
|
431 | target.
|
432 |
|
433 | ```js
|
434 | const author = await resource.follow('author');
|
435 | console.log(await author.get());
|
436 | ```
|
437 |
|
438 | The `follow` function returns a special kind of Promise that has a `follow()`
|
439 | function itself.
|
440 |
|
441 | This makes it possible to chain follows:
|
442 |
|
443 | ```js
|
444 | resource
|
445 | .follow('author')
|
446 | .follow('homepage')
|
447 | .follow('icon');
|
448 | ```
|
449 |
|
450 | Lastly, it's possible to follow [RFC6570](https://tools.ietf.org/html/rfc6570)
|
451 | templated links (templated URI), using the second argument.
|
452 |
|
453 | For example, a link specified as:
|
454 |
|
455 | { href: "/foo{?a}", templated: true }
|
456 |
|
457 | May be followed using
|
458 |
|
459 | ```js
|
460 | resource
|
461 | .follow('some-templated-link', { a: 'bar' })
|
462 | ```
|
463 |
|
464 | This would result following a link to the `/foo?a=bar` uri.
|
465 |
|
466 |
|
467 | #### `Resource.followAll()`
|
468 |
|
469 | This method works like `follow()` but resolves into a list of resources.
|
470 | Multiple links with the same relation type can appear in resources; for
|
471 | example in collections.
|
472 |
|
473 | ```js
|
474 | var items = await resource.followAll('item');
|
475 | console.log(items);
|
476 | ```
|
477 |
|
478 | #### `resource.fetch()`
|
479 |
|
480 | The `fetch` function is a wrapper for the `Fetch API`. It takes very similar
|
481 | arguments to the regular fetch, but it does a few things special:
|
482 |
|
483 | 1. The uri can be omitted completely. If it's omitted, the uri of the
|
484 | resource is used.
|
485 | 2. If a uri is supplied and it's relative, it will be resolved with the
|
486 | uri of the resource.
|
487 |
|
488 | For example, this is how you might do a HTTP `PATCH` request:
|
489 |
|
490 | ```js
|
491 | const init = {
|
492 | method: 'PATCH',
|
493 | body: JSON.serialize(['some', 'patch', 'object'])
|
494 | };
|
495 | const response = await resource.fetch(init);
|
496 | console.log(response.statusCode);
|
497 | ```
|
498 |
|
499 | #### `resource.fetchAndThrow()`
|
500 |
|
501 | This function is identical to `fetch`, except that it will throw a (async)
|
502 | exception if the server responded with a HTTP error.
|
503 |
|
504 | #### `resource.go(uri: string)`
|
505 |
|
506 | This function returns a new Resource object, based on a relative uri.
|
507 | This is useful in case no link is available on the resource to follow.
|
508 |
|
509 | ```js
|
510 | const subResource = resource.go('?page=2');
|
511 | ```
|
512 |
|
513 | It doesn't do any HTTP requests.
|
514 |
|
515 | ### Link
|
516 |
|
517 | The link class represents any Link of any type of document. It has the
|
518 | following properties:
|
519 |
|
520 | * rel - relation type
|
521 | * href - The uri
|
522 | * baseHref - the uri of the parent document. Used for resolving relative uris.
|
523 | * type - A mimetype, if specified
|
524 | * templated - If it's a URI Template. Most of the time this is false.
|
525 | * title - Human readable label for the uri
|
526 | * name - Unique identifier for the link within the document (rarely used).
|
527 |
|
528 | #### `Link.resolve()`
|
529 |
|
530 | Returns the absolute uri to the link. For example:
|
531 |
|
532 | ```js
|
533 | const link = new Link({ href: '/foo', baseHref: "http://example.org/bar" });
|
534 |
|
535 | console.log(link.resolve());
|
536 | // output is http://example.org/foo
|
537 | ```
|
538 |
|
539 | #### `Link.expand()`
|
540 |
|
541 | Expands a templated link. Example:
|
542 |
|
543 | ```js
|
544 | const link = new Link({ href: 'http://example.org/foo{?q}', templated: true });
|
545 |
|
546 | console.log(link.expand({ q: 'bla bla' });
|
547 | // output is http://example.org/foo?q=bla+bla
|
548 | ```
|
549 |
|
550 | ### OAuth2 Managed Client
|
551 |
|
552 | The underlying OAuth2 client is implemented using [js-client-oauth2][5] is
|
553 | exposed via the 'Ketting' class.
|
554 |
|
555 | ```js
|
556 | const ketting = new Ketting('https://api.example.org/', {
|
557 | auth: {
|
558 | type: 'oauth2',
|
559 | client: {
|
560 | clientId: 'fooClient',
|
561 | clientSecret: 'barSecret',
|
562 | accessTokenUri: 'https://api.example.org/oauth/token',
|
563 | scopes: ['test']
|
564 | },
|
565 | owner: {
|
566 | userName: 'fooOwner',
|
567 | password: 'barPassword'
|
568 | }
|
569 | }
|
570 | });
|
571 |
|
572 | const oAuthClient = ketting.oauth2Helper.client;
|
573 | // Interact with the underlying OAuth2 client
|
574 | ```
|
575 |
|
576 | [1]: https://tools.ietf.org/html/rfc8288 "Web Linking"
|
577 | [3]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
|
578 |
|
579 | [5]: https://github.com/mulesoft/js-client-oauth2
|
580 | [6]: https://tools.ietf.org/html/rfc7240 "Prefer Header for HTTP"
|
581 |
|
582 | [hal]: http://stateless.co/hal_specification.html "HAL - Hypertext Application Language"
|
583 | [jsonapi]: https://jsonapi.org/
|
584 | [problem]: https://tools.ietf.org/html/rfc7807
|