UNPKG

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