Comparing version 0.0.2 to 0.0.3
ChangeLog | ||
========= | ||
0.0.3 (2017-02-06) | ||
------------------ | ||
* Now using Bluebird for promises, so we can extend them. | ||
* #2: Custom requests are now possible on `Resource` objects. | ||
* #3: Promises returned from `follow()` now have a `follow()` function | ||
themselves, making it extremely easy to hop from link to link. | ||
* Added a `post()` method for making new resources. This function returns a | ||
`Resource` object again if the response contained a `Location` header. | ||
* #4: Things in the `_embedded` property are now also treated as links and can | ||
be followed. | ||
* The `links()` method on Resource now have a `rel` argument for easy filtering. | ||
* Added a `followAll()` function for getting collections. | ||
0.0.2 (2017-01-03) | ||
@@ -5,0 +20,0 @@ ------------------ |
var Link = require('./link'); | ||
/** | ||
* The Representation object is the Resource Representation. This might be | ||
* response to a GET request. | ||
* | ||
* It typically includes the request body + a number of relevant http headers. | ||
*/ | ||
var Representation = function(contentType, body) { | ||
@@ -12,8 +18,14 @@ | ||
} | ||
if (typeof this.body._embedded !== 'undefined') { | ||
parseHALEmbedded(this); | ||
} | ||
} | ||
/** | ||
* Parse the HAL _links object and populate the 'links' property. | ||
*/ | ||
var parseHALLinks = function(representation) { | ||
for(relType in representation.body._links) { | ||
for(var relType in representation.body._links) { | ||
var links = representation.body._links[relType]; | ||
@@ -23,11 +35,11 @@ if (!Array.isArray(links)) { | ||
} | ||
for(link in links) { | ||
for(var ii in links) { | ||
representation.links.push( | ||
new Link( | ||
relType, | ||
links[link].href, | ||
links[link].type | ||
links[ii].href, | ||
links[ii].type | ||
) | ||
); | ||
} | ||
} | ||
} | ||
@@ -37,4 +49,23 @@ | ||
/** | ||
* Parse the HAL _embedded object. Right now we're just grabbing the | ||
* information from _embedded and turn it into links. | ||
*/ | ||
var parseHALEmbedded = function(representation) { | ||
for(var relType in representation.body._embedded) { | ||
var embedded = representation.body._embedded[relType]; | ||
if (!Array.isArray(embedded)) { | ||
embedded = [embedded]; | ||
} | ||
for(var ii in embedded) { | ||
representation.links.push( | ||
new Link( | ||
relType, | ||
embedded[link]._links.self.href; | ||
) | ||
); | ||
} | ||
} | ||
module.exports = Representation; |
@@ -5,2 +5,3 @@ "use strict"; | ||
var url = require('url'); | ||
var FollowablePromise = require('./followable-promise'); | ||
@@ -34,3 +35,4 @@ var Resource = function(client, uri) { | ||
return this.client.request.put({ | ||
return this.request({ | ||
method: 'PUT', | ||
uri: this.uri, | ||
@@ -49,3 +51,4 @@ body: body | ||
return this.client.request.delete({ | ||
return this.request({ | ||
method: 'DELETE', | ||
uri: this.uri, | ||
@@ -60,2 +63,31 @@ body: body | ||
/** | ||
* Sends a POST request to the resource. | ||
* | ||
* This function assumes that POST is used to create new resources, and | ||
* that the response will be a 201 Created along with a Location header that | ||
* identifies the new resource location. | ||
* | ||
* This function returns a Promise that resolves into the newly created | ||
* Resource. | ||
* | ||
* If no Location header was given, it will resolve still, but with an empty | ||
* value. | ||
*/ | ||
post: function(body) { | ||
return this.request({ | ||
method: 'POST', | ||
uri: this.uri, | ||
body: body | ||
}).then(function(response) { | ||
if (response.headers.location) { | ||
return new Resource( | ||
this.client, | ||
response.headers.uri | ||
); | ||
} | ||
}); | ||
}, | ||
/** | ||
* Refreshes the representation for this resource. | ||
@@ -66,12 +98,12 @@ * Returns an empty Promise. | ||
return this.client.request.get(this.uri) | ||
.then(function(response) { | ||
return this.request({ | ||
method: 'GET', | ||
uri: this.uri | ||
}).then(function(response) { | ||
this.repr = new Representation( | ||
response.headers['content-type'], | ||
response.body | ||
); | ||
}.bind(this)); | ||
this.repr = new Representation( | ||
response.headers['content-type'], | ||
response.body | ||
); | ||
}.bind(this)); | ||
}, | ||
@@ -81,7 +113,11 @@ | ||
* Returns the links for this resource, as a promise. | ||
* | ||
* The rel argument is optional. If it's given, we will only return links | ||
* from that relationship type. | ||
*/ | ||
links: function() { | ||
links: function(rel) { | ||
return this.representation().then(function(r) { | ||
return r.links; | ||
if (!rel) return r.links; | ||
return r.links.filter( function(item) { return item.rel === rel; } ); | ||
}); | ||
@@ -99,15 +135,19 @@ | ||
return this.links().then(function(links) { | ||
return new FollowablePromise(function(res, rej) { | ||
var link = links.find(function(link) { | ||
return link.rel === rel; | ||
}); | ||
if (!link) { | ||
throw new Error('Relation with type ' + rel + ' not found on resource ' + this.uri); | ||
} | ||
return new Resource( | ||
this.client, | ||
url.resolve(this.uri, link.href) | ||
); | ||
this.links(rel) | ||
.then(function(links) { | ||
if (links.length === 0) { | ||
throw new Error('Relation with type ' + rel + ' not found on resource ' + this.uri); | ||
} | ||
res(new Resource( | ||
this.client, | ||
url.resolve(this.uri, links[0].href) | ||
)); | ||
}.bind(this)) | ||
.catch(function(reason) { | ||
rej(reason); | ||
}); | ||
}.bind(this)); | ||
@@ -118,2 +158,21 @@ | ||
/** | ||
* Follows a relationship based on its reltype. This function returns a | ||
* Promise that resolves to an array of Resource objects. | ||
* | ||
* If no resources were found, the array will be empty. | ||
*/ | ||
followAll: function(rel) { | ||
return this.links(rel).then(function(links) { | ||
return links.map(function(link) { | ||
return new Resource( | ||
this.client, | ||
url.resolve(this.uri, link.href) | ||
); | ||
}); | ||
}); | ||
}, | ||
/** | ||
* Returns the representation for the object. | ||
@@ -133,2 +192,12 @@ * | ||
}, | ||
/** | ||
* Does an arbitrary HTTP request on the resource, and returns the HTTP | ||
* response object from the Request library, wrapped in a Promise. | ||
*/ | ||
request: function(options) { | ||
return this.client.request(options); | ||
} | ||
@@ -135,0 +204,0 @@ |
{ | ||
"name": "restl", | ||
"version": "0.0.2", | ||
"version": "0.0.3", | ||
"description": "Generic hypermedia client.", | ||
@@ -28,2 +28,3 @@ "main": "lib/index.js", | ||
"dependencies": { | ||
"bluebird": "^3.4.7", | ||
"request": "^2.79.0", | ||
@@ -30,0 +31,0 @@ "request-promise-any": "^1.0.3" |
155
readme.md
@@ -33,3 +33,3 @@ Restl - A hypermedia client for nodejs | ||
* Resolve every URI to an absolute URI. | ||
* Figuring out `_embedded`. | ||
* Figuring caching resources from `_embedded`. | ||
* Support HTTP `Link` header. | ||
@@ -39,3 +39,5 @@ * Support non-JSON resources, including things like images. | ||
* Parse [Atom][5]. | ||
* Built-in OAuth2. | ||
### Post 1.0 | ||
@@ -46,2 +48,5 @@ | ||
* Support [`Prefer: return=representation`][6]. | ||
* Browser support (nodejs only at the moment, but only because of the Request | ||
library.) | ||
* Unittests | ||
@@ -52,5 +57,8 @@ ### Already done: | ||
* Basic HAL parsing. | ||
* Normalizing `_links` and `_embedded`. | ||
* `PUT` request. | ||
* `DELETE` request. | ||
* `POST` request | ||
Usage | ||
@@ -91,2 +99,33 @@ ----- | ||
### Following a chain of links | ||
It's possible to follow a chain of links with follow: | ||
```js | ||
client.follow('rel1') | ||
.then(function(resource1) { | ||
return resource1.follow('rel2'); | ||
}) | ||
.then(function(resource2) { | ||
return resource2.follow('rel3'); | ||
}) | ||
.then(function(resource3) { | ||
console.log(resource3.getLinks()); | ||
}); | ||
``` | ||
As you can see, `follow()` returns a Promise. However, the returned promise | ||
has an additional `follow()` function itself, which makes it possible to | ||
shorten this to: | ||
```js | ||
client | ||
.follow('rel1') | ||
.follow('rel2') | ||
.follow('rel3') | ||
.then(function(resource3) { | ||
console.log(resource3.getLinks()); | ||
}); | ||
``` | ||
### Providing custom options | ||
@@ -153,4 +192,46 @@ | ||
### `Resource.refresh()` | ||
#### `Resource.put()` | ||
Updates the resource with a new respresentation | ||
```js | ||
resource.put({ 'foo' : 'bar' }); | ||
``` | ||
This function returns a Promise that resolves to `null`. | ||
#### `Resource.delete()` | ||
Deletes the resource. | ||
```js | ||
resource.delete(); | ||
```` | ||
This function returns a Promise that resolves to `null`. | ||
#### `Resource.post()` | ||
This function is meant to be an easy way to create new resources. It's not | ||
necessarily for any type of `POST` request, but it is really meant as a | ||
convenience method APIs that follow the typical pattern of using `POST` for | ||
creation. | ||
If the HTTP response from the server was successful and contained a `Location` | ||
header, this method will resolve into a new Resource. For example, this might | ||
create a new resource and then get a list of links after creation: | ||
```js | ||
resource.post({ property: 'value'}) | ||
.then(function(newResource) { | ||
return newResource.links(); | ||
}) | ||
.then(function(links) { | ||
console.log(links); | ||
}); | ||
``` | ||
#### `Resource.refresh()` | ||
Refreshes the internal cache for a resource and does a `GET` request again. | ||
@@ -168,3 +249,3 @@ This function returns a `Promise` that resolves when the operation is complete, | ||
### `Resource.links()` | ||
#### `Resource.links()` | ||
@@ -179,4 +260,13 @@ Returns a list of `Link` objects for the resource. | ||
### `Resource.follow()` | ||
You can also request only the links for a relation-type you are interested in: | ||
```js | ||
resource.links('item').then(function(links) { | ||
}); | ||
``` | ||
#### `Resource.follow()` | ||
Follows a link, by it's relation-type and returns a new resource for the | ||
@@ -193,3 +283,60 @@ target. | ||
The follow function returns a special kind of Promise that has a `follow()` | ||
function itself. | ||
This makes it possible to chain follows: | ||
```js | ||
resource | ||
.follow('author') | ||
.follow('homepage') | ||
.follow('icon'); | ||
``` | ||
#### `Resource.followAll()` | ||
This method works like `follow()` but resolves into a list of resources. | ||
Multiple links with the same relation type can appear in resources; for | ||
example in collections. | ||
```js | ||
resource.followAll('item') | ||
.then(function(items) { | ||
console.log(items); | ||
}); | ||
``` | ||
#### `Resource.representation()` | ||
This function is similar to `GET`, but instead of just returning a response | ||
body, it returns a `Representation` object. | ||
### `Representation` | ||
The Representation is typically the 'body' of a resource in REST terminology. | ||
It's the R in REST. | ||
The Representation is what gets sent by a HTTP server in response to a `GET` | ||
request, and it's what gets sent by a HTTP client in a `POST` request. | ||
The Representation provides access to the body, a list of links and HTTP | ||
headers that represent real meta-data of the resource. Currently this is only | ||
`Content-Type` but this might be extended to include encoding, language and | ||
cache-related information. | ||
#### `Representation.body` | ||
The `body` property has the body contents of a `PUT` request or a `GET` response. | ||
#### `Representation.links` | ||
The `links` property has the list of links for a resource. | ||
#### `Representation.contentType` | ||
The `contentType` property has the value of the `Content-Type` header for both | ||
requests and responses. | ||
[1]: https://tools.ietf.org/html/rfc5988 "Web Linking" | ||
@@ -196,0 +343,0 @@ [2]: http://stateless.co/hal_specification.html "HAL - Hypertext Application Language" |
31
test.js
@@ -10,5 +10,34 @@ var restl = require('.'); | ||
var client = restl('http://192.168.0.26:3009', options); | ||
var client = restl('http://192.168.0.26:3009/', options); | ||
var home = client.getResource(); | ||
describe('creating a new location', () => async { | ||
const accounts = await home | ||
.follow('currentUser') | ||
.follow('accounts') | ||
.followAll('item'); | ||
const account = accounts[0]; | ||
const locations = await account.follow('locationCollection'); | ||
const newLocation = await locations.post({ | ||
title: 'Mac Donalds' | ||
}); | ||
it('should return 201 Created'); | ||
it('should have created the new location', () => async { | ||
const hal = await newLocation.get(); | ||
expect(hal).to.equal(...); | ||
}); | ||
}); | ||
home.follow('currentUser') | ||
@@ -15,0 +44,0 @@ .then(function(currentUser) { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
18589
12
310
339
3
+ Addedbluebird@^3.4.7
+ Addedbluebird@3.7.2(transitive)