Skip to content

johndgiese/angular-hyper-resource

Repository files navigation

Angular Hyper Resource

Hyper Resource is an angularjs module that extends Angular's native $resource service to work well with Hypermedia APIs using HAL.

Installation

Installation is easy using Bower.

bower install angular-hyper-resource

Before explaining how to use the module, I think it is useful to provide a somewhat lengthy background to motivate what the problems the module is solving.

HATEOAS and Linking within an API

Single page applications usually require an API to persist data.

One of the tenants of RESTful APIs is HATEOAS, or Hypermedia as the Engine of Application State. The word sounds complicated but stated simply, HATEOAS says your API should be like a state-machine, and state-transitions should occur by following some sort of "hyperlink". If you think about it, this is how the web works: we go to a page (a resource, or state) and we move to other pages by clicking links (state transitions).

Very few APIs fully follow HATEOAS, and there is quite a bit of debate online as to how important it is. I think one of the reasons many APIs don't follow it, is simply because unlike humans browsing the web, who follow links as their curiosity directs, machines are usually interacting with a resource for a known purpose. Thus the idea that a machine will discover an API purely through links doesn't seem to work well in practice. That said, there are some serious benefits to following HATEOAS when it makes sense.

In the context of an API, this means a resource should have links to other resources. How is a link represented in an API? Well, there are many ways you could do it, but some smart people have spent a lot of time thinking about it and have developed good standards about web linking. If you follow this standard, you are in good company, because it is also used in

  • the HTML <link> tag
  • the HTTP link header
  • ATOM

Even after understanding link relation types, there are many ways of representing these relationships within a JSON API. There are no set standards for this yet, however Hypertext Application Language (or HAL for short) is widely used.

Hypertext Application Language (HAL)

HAL is a simple extension of JSON that provides a standard way of encoding related resources in APIs. A HAL resource may have a _links object which contains links to related resources. The _links object's keys are the relation types (see the official IANA registry if you are curious, but we don't believe in being so anal as to follow it exactly---even the HAL spec doesn't) and the values are either an object (if there is one resource with this relation) or an array (if there are more than one resources with this relation). Each link must contain an href property which is the URL representing the related resource. There are also some optional properties, including a name and type.

Here is an example:

{
    "name": "John David Giese",
    "_links": {
        "self": {
            "href": "/persons/4234"
        },
        "father": {
            "href": "/persons/2332"
        },
        "brothers": [
            {
                "href": "/persons/2242",
            },
            {
                "href": "/persons/2549",
            },
            {
                "href": "/persons/2600",
            }
        ]
    }
}

When interacting with RESTful APIs, it is often useful to embed multiple resources in a single request so that we can avoid multiple round trips to the server. For example, imagine we have a book resource with chapters. Whenever we retrieve the book resource we will nearly always want to look at the current chapter. HAL has a second reserved keyword, _embedded, for this purpose. It works nearly identically to links.

{
    "title": "Javascript: The Good Parts",
    "author": "Douglas Crockford",
    "_links": {
        "self": {
            "href": "/books/342"
        }
    },
    "_embedded": {
        "current": {
            "title": "Inheritance",
            "_links": {
                "self": {
                    "href": "/books/342/chapters/5",
                    "type": "chapter"
                },
                "next": {
                    "href": "/books/342/chapters/6",
                    "type": "chapter"
                },
                "prev": {
                    "href": "/books/342/chapters/4",
                    "type": "chapter"
                }
            }
        }
    }
}

As you can see, embedded resources can have links (and even further embedded resources).

So that is HAL! Quite simple really. I should also mention that you can use the special mime type, "application/hal+json", if you want to be cool.

Angular and HAL

Angular provides the $resource service (as part of the optional ngResource module) to help simplify API interactions. Without something like the $resource service, one needs to construct the API's URLs and making requests with the $http service directly. This can quickly get tedious. By using the $resource service, one can define the URLs for interacting with their data resources, as well as which HTTP methods are supported, and from then on they can use the resource at a higher level of abstraction.

Take a look at the documentation for $resource for more details and some examples.

When interacting with an API that uses HAL, we want all of ngResource's functionality, but it would also be nice if the service provided convenience methods for interacting with related resources. This is where the hyperResource module comes in. It provides a single service, hResource, that is a small and somewhat opinionated extension of the $resource service.

API Constraints

This "opinionated" approach to HAL places a few extra constraints on the API.

Because the hResource service attempts to abstract away the distinction between a _linked and _embedded resource, the API must always return one or the other (and not both, otherwise both will be returned).

Following this makes the angular app simpler, because it doesn't have to worry about which is which when resolving a related resource. Instead, the app is returned a promise for the resource, and the distinction between a linked vs. embedded resource only determines how quickly that promise will be fulfilled. If is a link, it will be a fulfilled after another round trip to the API, otherwise it will be fulfilled immediately.

This abstraction allows the API to worry about performance and caching issues, while freeing the client to work with the resources.

From now on, a "related resource" can be either linked or embedded, as the distinction is irrelevant.

Basic Use

We now know enough background to dive into the basics.

The hResource service can be called identically to how the $resource service is called, and like the $resource service it returns a constructor function for interacting with APIs. We recommend creating services for each of your resources (don't forget to capitalize them because they are constructor functions!). For example:

angular.module('app', ['hyperResource'])
.service('Book', ['hResource', function(hResource) {
    return hResource('/books/:bookId', {bookId: '@id'});
});

We can now use our Book service to retrieve books from the server like we would with $resource.
The following code could go anywhere where the Book service is injected.

var jsBooks = Book.query({includes: 'javascript'});
var linearAlgebra = Book.get({title: 'Linear Algebra', author: 'Strang'});

hResource instances are provided all of the $resource methods (e.g. $save), in addition to two convenience methods specifically for interacting with related resources.

The $rel resource instance method

The first resource instance method, $rel, provides a simple interface for grabbing related resources. It takes a single required argument that specifies the relationship (i.e. the [rel attribute] (https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types) in HTML links), and a second optional argument for relationship name.

The $rel method returns a promise for the related resources.

  • If there is a single match, the promise will resolve to a "hyper-object"---an object containing all of the data returned from the API plus the two extra $rel and $if methods.
  • If there are multiple matches, the promise resolves to an array of hyper objects.
  • If it can't find the resource, the promise is rejected.

This is best demonstrated with an example:

var City = hResource('/cities/:id');
var cityData = {
    name: 'Boston',
    _links: {
        self: { href: '/cities/5' },
        state: { href: '/states/7' },
    },
    _embedded: {
        country: { name: 'United States of America' }
    }
};
$httpBackend.expectGET('/city/5').respond(200, cityData);
var boston = City.get({name: 'Boston'});
$httpBackend.flush();

var stateData = { name: 'Massachusets' };

$httpBackend.expectGET('/states/7').respond(200, stateData);
var state = boston.$rel('state')
.then(function(){
    expect(state.name).toBe('Massachusets');
});

var country = boston.$rel('country')
.then(function(){ 
    expect(country.name).toBe('United States of America');
});

Again, notice that there is not distinction between embedded and linked resources!

The $if resource instance method

Hyper objects also get an $if method. It takes the same arguments as the $rel method, except instead of returning a promise to those resources, it simply returns the number of matching resources.
This is useful if you are conditionally displaying items in your view.

// continueing the example from above

expect(book.$if('state')).toBe(1);
expect(book.$if('mayor')).toBe(0);

Advanced Use and Active Records

The above usage pattern is great for many basic situations, however astute readers may have noticed that the related resources returned by $rel are no longer $resource instances!

var Chapter = hResource('/chapters/:id');
var chapterData = {
  title: 'Inheritance',
  _links: {
      first: { href: '/books/1/chapters/1' },
      last: { href: '/books/1/chapters/10', title: 'Beautiful Features' },
  },
  _embedded: {
    next: { title: 'Arrays' }
    prev: { title: 'Functions' }
  }
};

var chapterFour = new Chapter(chapterData);
// could also have used Chapter.get

expect(chapterFour instanceof Chapter).toBe(true);

var chapterFive = chapterFour.$rel('next');

expect(chapterFive instanceof Chapter).toBe(false);

// because how could it know what type it should be?

Again, this is probably fine for many situations, however it is often nice to attach functionality along with our resource data, and when using the $resource service this is done by extending the resource's prototype function. If related resources don't preserve the initial type, our instances won't be able to access our added functionality.

// here is how you would extend a person resource
var Person = hResource('/persons/:id');
Person.prototype.fullName = function() {
    return this.firstName + this.lastName;
};

var person = Person.get({firstName: 'David'});

var myName = person.$promise.then(function(me) {
    // this would work as expected
    return me.fullName();
});

var momsName = person.$rel('mother').then(function(mom) {
    // this would NOT work (yet!)
    return mom.fullName();
});

Fortunately, hResource provides a mechanism for resolve related resource's types.

Resolving related resource types

There are two steps involved with preserving resource types.

  1. the hResource service must be able to keep track of all types
  2. the hResource service must be able to resolve the type of a related resource from the HAL link

Both steps are pretty trivial.

The first step involves providing an extra typeName when creating your resource. For example:

var userTypeName = 'user';
var User = hResource(userTypeName, '/users/:id');

The second step is a bit more complicated. Essentially, every time the $rel method is called, it has a method called resolveResourceType which is passed in the link of the related resource. This will be the _self link for an embedded resource.

By default, the hResource service uses the optional type attribute of the link. So if we go back to the chapter example we would need to have:

var chapterData = {
  title: 'Inheritance',
  _links: {
      first: { href: '/books/1/chapters/1', type: 'chapter' },
      last: { 
          href: '/books/1/chapters/10',
          title: 'Beautiful Features',
          type: 'chapter' 
      },
  },
  _embedded: {
    next: { 
        _links: { self: {type: 'chapter' }},
        title: 'Arrays'
    }
    prev: {
        _links: { self: {type: 'chapter' }},
        title: 'Functions', type: 'chapter' 
    }
  }
};

If the hResource service is unable to resolve the type (or if the type it resolves to is not registered), it will simply revert to the basic behavior defined previously.

Custom resource type resolver

There are many other possibly approaches for resolving a resource's type.

  • Resolved from the URL
  • Default to the same type as the parent
  • From an href template scheme

For this reason, the hyperResource app provides the ability to override the default behavior with the hResourceProvider.

The hResourceProvider has a single function, setResourceTypeResolver, that takes a custom typeResolver.

This function that takes a link or embedded resource and returns a string matching the appropriate hResource's resourceName (the first argument passed in when constructing an hResource). All typeResolver functions should return undefined if they can not resolve a type. If the resolved type is undefined, or does not match any declared resourceTypes, then the Object type is used instead.

Questions

How does hResource deal with $resource's approach of returning empty arrays and objects?

The short answer is: The $rel exists on unresolved resources, but calling it before they resolve will throw an error. The $if exists and returns 0 on unresolved resources; this behavior is convenient because the $if method is often used in templates.

If this answer didn't make sense, continue reading:

A subtle but key aspect of the $resource service, is that resources returned from queries are not promises, but rather are empty objects or arrays that are filled in with data when the underlying promise is fulfilled.

For example:

var User = $resource('/users/:id');

var user = User.get({id: 1});  

// user is NOT a promise, but is a nearly empty object that will "fill" up with
// data once the underlying promise for the resource is fullfilled.

var allUsers = User.query();

// allUsers is an empty array that is filled as the promise for the resource is
// fulfilled

The $resource class does this to make it easy when injecting resource instances into the scope; if the queries returned a promise directly, one would need to do the following:

// if queries returned promises we would need to do this
var user = User.get({id: 1});
user.then(function(){
    $scope.user = user;
});

// because if we attached the promise to the scope directly, it wouldn't know
// how to resolve it; interestingly, angular used to handle promises, but they
// deprecated the feature (probably for performance reasons)

// since $resource queireis return empty objects or arrays, we can do this
$scope.user = User.get({id: 1});

// because $scope.user will initially be an empty object, and when the
// underlying promise resolves, the data is attached, and the $digest cycle will
// know how to update the view

The underlying promise can be accessed via the $promise attribute (e.g. user.$promise), and one can see if they have been resolved using the $resolved attribute.

Unfortunately, although this "reference injection" approach is useful in simple cases when attaching resources onto the scope, it can be confusing when there are related dependencies between resources. In particular, calling $if before a resource instance's underlying promise is resolved returns 0! This behavior is allowed because $if calls are often made inside of templates, and in particular in ng-show directives; when used in this way, the item is hidden until the resource is resolved---probably the most reasonable behavior.

Calling $rel before the resource resolves will throw an error, as it is unlikely that one would call $rel intentionally before the resource is resolved.

Status of this module

This module is still in the development phase; it hasn't been used in any production environments yet, and some of the core functionality is still under question.

That said, there is a set of unit tests for the core parts of the module, so it at least basically works as advertised.

In particular:

  • There may be a better way for the $rel method to determine if it should return an array vs an object. For example, It may be desirable to have the $rel method return an array if the _links or _embedded.
  • We may want to provide a different default type resolver.
  • It may be nice to provide simpler support for chaining $rel calls (where you don't have to use promises directly).

Please email me at [email protected] if you have any questions or suggestions!

Thanks

Check out my web-consulting company, Innolitics.

Thanks for helpful discussions with;

About

Angularjs module that provides HAL extensions to $resource

Resources

License

Stars

Watchers

Forks

Packages

No packages published