Skip to content

Commit

Permalink
Support changes to URL and authorize across versions
Browse files Browse the repository at this point in the history
  • Loading branch information
arobson committed Jul 6, 2016
1 parent 5b12663 commit 33822a6
Show file tree
Hide file tree
Showing 14 changed files with 272 additions and 19 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.7.x

### 0.7.0
* Support use of `latest` as a version specifier
* Support URL changes across versions (includes OPTIONS)
* Support authorize changes across versions

## 0.6.x

### 0.6.3
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ The default versioning strategies parses the version specifier out of the `Accep
* `application/json; version=2`
* `application/hal+json; version=3`

> Note: hyped also supports the use of the keyword `latest` in place of a numeric indicator.
#### Versioning handlers
Handlers behave a bit differently. When providing a new handle implementation for a version, that handle implementation will apply for the current version _and_ any consecutive versions after that don't provide their own handle implementation.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hyped",
"version": "0.6.3",
"version": "0.7.0",
"description": "Hypermedia response generation engine",
"main": "src/index.js",
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions spec/ah/board/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ module.exports = function( host ) {
},
10: {
self: {
url: "/train/:id",
handle: function( envelope ) {
return {
status: 200,
Expand Down
29 changes: 29 additions & 0 deletions spec/ah/secure/resource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module.exports = function() {
return {
name: "secure",
actions: {
self: {
method: "get",
url: "/",
authorize: function( envelope ) {
return envelope.data.level ? envelope.data.level > 1 : false;
},
handle: function( envelope ) {
return { data: { result: "level 1" } };
}
}
},
versions: {
2: {
self: {
authorize: function( envelope ) {
return envelope.data.level ? envelope.data.level > 2 : false;
},
handle: function( envelope ) {
return { data: { result: "level 2" } };
}
}
}
}
};
};
19 changes: 19 additions & 0 deletions spec/behavior/actions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,4 +272,23 @@ describe( "Action links", function() {
} );
} );
} );

describe( "with action rendering with non-default version", function() {
describe( "when rendering an \"plain\" action", function() {
var expected = { self: { href: "/parent/2/1", method: "GET" } };
var createLinks;
var model;
before( function() {
model = {
id: 1
};
createLinks = links.getLinkGenerator( resources, "", 2 );
} );

it( "should produce expected links", function() {
return createLinks( "parent", "self", {}, model )
.should.eventually.eql( expected );
} );
} );
} );
} );
33 changes: 31 additions & 2 deletions spec/behavior/hyperResource.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ describe( "Hyper Resource", function() {
var expected = {
id: 1,
title: "test",
_origin: { href: "/parent/1", method: "GET" },
_origin: { href: "/parent/2/1", method: "GET" },
_resource: "parent",
_action: "self",
_version: 2,
_links: {
self: { href: "/parent/1", method: "GET" },
self: { href: "/parent/2/1", method: "GET" },
children: { href: "/parent/1/child", method: "GET", parameters: parameters },
"next-child-page": { href: "/parent/1/child?page=2&size=5", method: "GET", parameters: parameters },
"create-child": { href: "/parent/1/child", method: "POST" }
Expand Down Expand Up @@ -240,6 +240,35 @@ describe( "Hyper Resource", function() {
} );
} );

describe( "when rendering options (version 10) excluding children as generic user", function() {
var expected = {
_mediaTypes: [],
_versions: [ "1", "2", "10" ],
_links: {
"parent:self": { href: "/parent/10/{id}", method: "GET", templated: true },
"parent:list": { href: "/parent", method: "GET" },
"parent:children": { href: "/parent/{id}/child", method: "GET", templated: true, parameters: {
size: { range: [ 1, 100 ] }
} }
}
};
var options;
var envelope = { user: {} };

before( function() {
var fn = HyperResource.optionsGenerator( resources, "", 10, true, envelope );
options = fn();
} );

it( "should render options correctly", function() {
return options.should.eventually.eql( expected );
} );

it( "should not have called authorize on hidden action", function() {
return should.not.exist( envelope.hiddenWasAuthorized );
} );
} );

describe( "when rendering options excluding children as admin", function() {
var expected = {
_mediaTypes: [],
Expand Down
2 changes: 2 additions & 0 deletions spec/behavior/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ var resources = {
versions: {
2: {
self: {
url: "/parent/2/:id",
include: [ "id", "title" ],
handle: function() {
return "two";
Expand All @@ -88,6 +89,7 @@ var resources = {
},
10: {
self: {
url: "/parent/10/:id",
include: [ "id" ],
handle: function() {
return "three";
Expand Down
1 change: 1 addition & 0 deletions spec/behavior/versions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe( "Versioning", function() {

it( "should merge distinct versions correctly", function() {
should.not.exist( parentVersion1.actions.self.include );
parentVersion2.actions.self.url.should.eql( "/parent/2/:id" );
parentVersion2.actions.self.include.should.eql( [ "id", "title" ] );
parentVersion3.actions.self.include.should.eql( [ "id" ] );
} );
Expand Down
105 changes: 103 additions & 2 deletions spec/integration/autohost.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,9 @@ describe( "Autohost Integration", function() {
var body, contentType;

before( function( done ) {
request( "http://localhost:8800/api/board/100", function( err, res ) {
request( "http://localhost:8800/api/board/train/100", function( err, res ) {
body = JSON.parse( res.body );

contentType = res.headers[ "content-type" ].split( ";" )[ 0 ];
done();
} );
Expand Down Expand Up @@ -464,7 +465,107 @@ describe( "Autohost Integration", function() {
} );
} );

describe( "with undefined api prefix", function() {
describe( "with versioned authorization calls", function() {
var hyped, host;
before( function( done ) {
hyped = require( "../../src/index.js" )( {
defaultContentType: "application/hal+json"
} );
host = hyped.createHost( autohost, {
urlPrefix: "/test",
resources: "./spec/ah"
}, function() {
host.start();
done();
} );
} );

describe( "when requesting with version 1 and adequate authorization", function() {
var body, contentType;

before( function( done ) {
request( "http://localhost:8800/test/api/secure?level=2",
{ headers: { accept: "*/*" } },
function( err, res ) {
body = JSON.parse( res.body );
contentType = res.headers[ "content-type" ].split( ";" )[ 0 ];
done();
} );
}
);

it( "should get HAL version 1 (default content type and version)", function() {
contentType.should.equal( "application/hal+json" );
body.result.should.eql( "level 1" );
} );
} );

describe( "when requesting with version 1 and inadequate authorization", function() {
var body, contentType;

before( function( done ) {
request( "http://localhost:8800/test/api/secure?level=1",
{ headers: { accept: "*/*" } },
function( err, res ) {
body = JSON.parse( res.body );
contentType = res.headers[ "content-type" ].split( ";" )[ 0 ];
done();
} );
}
);

it( "should get HAL version 1 (default content type and version)", function() {
contentType.should.equal( "application/hal+json" );
body.message.should.eql( "User lacks sufficient permissions" );
} );
} );

describe( "when requesting with version 2 and adequate authorization", function() {
var body, contentType;

before( function( done ) {
request( "http://localhost:8800/test/api/secure?level=3",
{ headers: { accept: "application/hal.v2+json" } },
function( err, res ) {
body = JSON.parse( res.body );
contentType = res.headers[ "content-type" ].split( ";" )[ 0 ];
done();
} );
}
);

it( "should get HAL version 2 (default content type and version)", function() {
contentType.should.equal( "application/hal.v2+json" );
body.result.should.eql( "level 2" );
} );
} );

describe( "when requesting with version 2 and inadequate authorization", function() {
var body, contentType;

before( function( done ) {
request( "http://localhost:8800/test/api/secure?level=2",
{ headers: { accept: "application/hal.v2+json" } },
function( err, res ) {
body = JSON.parse( res.body );
contentType = res.headers[ "content-type" ].split( ";" )[ 0 ];
done();
} );
}
);

it( "should get HAL version 2 (default content type and version)", function() {
contentType.should.equal( "application/hal.v2+json" );
body.message.should.eql( "User lacks sufficient permissions" );
} );
} );

after( function() {
host.stop();
} );
} );

describe( "with undefined api prefix and latest version as default", function() {
var hyped, host;
before( function( done ) {
hyped = require( "../../src/index.js" )( true, true );
Expand Down
2 changes: 1 addition & 1 deletion spec/integration/halOptionsNoPrefix.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_links":
{
"board:cards": { "href": "/board/{id}/card", "method": "GET", "templated": true },
"board:self": { "href": "/board/{id}", "method": "GET", "templated": true },
"board:self": { "href": "/board/train/{id}", "method": "GET", "templated": true },
"card:self": { "href": "/card/{id}", "method": "GET", "templated": true },
"card:move": { "href": "/card/{id}/board/{targetBoardId}/lane/{targetLaneId}", "method": "PUT", "templated": true },
"card:block": { "href": "/card/{id}/block", "method": "PUT", "templated": true },
Expand Down
42 changes: 31 additions & 11 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ function addResourceMiddleware( state, host ) {

function addResource( state, resource, resourceName ) { // jshint ignore:line
versions.processHandles( resource );
versions.processAuthorize( resource );
state.resources[ resourceName ] = resource;
}

Expand Down Expand Up @@ -108,7 +109,7 @@ function getContentType( req ) { // jshint ignore:line
}

function getEngine( state, mediaType ) { // jshint ignore:line
var filtered = mediaType.replace( /[.]v[0-9]*/, "" ).split( ";" )[ 0 ];
var filtered = mediaType.replace( /[.]v([0-9]+|latest)/, "" ).split( ";" )[ 0 ];
return state.engines[ filtered ];
}

Expand Down Expand Up @@ -136,23 +137,25 @@ function getOptionModel( state, envelope ) {
}

function getFullOptionModel( state ) {
var version = 1;
if ( !state.fullOptionModels[ version ] ) {
state.fullOptionModels[ version ] = HyperResource.routesGenerator( state.resources, state.prefix, version )( state.engines );
var maxVersion = getMaxVersion( state );
for ( var i = 1; i <= maxVersion; i++ ) {
if ( !state.fullOptionModels[ i ] ) {
state.fullOptionModels[ i ] = HyperResource.routesGenerator( state.resources, state.prefix, i )( state.engines );
}
}
return state.fullOptionModels[ version ];
return state.fullOptionModels;
}

function getMaxVersion( state ) { // jshint ignore:line
return _.reduce( state.resources, function( version, resource ) {
var max = _.maxBy( _.keys( resource.versions ), parseInt );
var max = parseInt( _.maxBy( _.keys( resource.versions ), parseInt ) ) || 0;
return max > version ? max : version;
}, 1 );
}

function getMimeVersion( header ) {
var version;
var match = /[.]v([0-9]*)/.exec( header );
var match = /[.]v([0-9]+|latest)/.exec( header );
if ( match && match.length > 0 ) {
version = match[ 1 ];
}
Expand All @@ -161,7 +164,7 @@ function getMimeVersion( header ) {

function getParameterVersion( header ) {
var version;
var match = /version\s*[=]\s*([0-9]+)[;]?/g.exec( header );
var match = /version\s*[=]\s*([0-9]+|latest)[;]?/g.exec( header );
if ( match && match.length > 0 ) {
version = match[ 1 ];
}
Expand All @@ -178,6 +181,8 @@ function getVersion( state, envelope ) {
var accept = envelope.headers.accept || "";
var mimeVersion = getMimeVersion( accept );
var parameterVersion = getParameterVersion( accept );
parameterVersion = parameterVersion === "latest" ? state.maxVersion : parameterVersion;
mimeVersion = mimeVersion === "latest" ? state.maxVersion : mimeVersion;
var version = state.preferLatest ? state.maxVersion : 1;
var final = parseInt( mimeVersion || parameterVersion || version );
envelope.version = final;
Expand Down Expand Up @@ -230,10 +235,25 @@ function urlStrategy( state, resourceName, actionName, action, resourceList ) {
if ( _.isEmpty( state.resources ) ) {
state.resources = resourceList;
}
// will need to do this for all available versions at some point ...

var options = getFullOptionModel( state );
var link = options._links[ [ resourceName, actionName ].join( ":" ) ];
return link ? url.forExpress( link.href ) : "";
var versions = options[ 1 ]._versions;
var key = [ resourceName, actionName ].join( ":" );
var links = _.uniq( _.reduce( versions, function( acc, version ) {
var versionSet = options[ version ];
if ( versionSet ) {
var link = url.forExpress( versionSet._links[ key ].href );
acc.push( link || "" );
}
return acc;
}, [] ) );
if ( links.length === 0 ) {
return "";
} else if ( links.length === 1 ) {
return links[ 0 ];
} else {
return links;
}
}

module.exports = function( resourceList, defaultToNewest, includeChildrenInOptions ) {
Expand Down
Loading

0 comments on commit 33822a6

Please sign in to comment.