Resources

A Resource (or 'model') resembles the structure of an object as defined by the JSON API 1.0 Specification. The Resource prototype creates hashes for: attributes, relationships, links, and meta. The prototype has attributes: id and type. The properties and structure of a model follow the objects defined by the JSON API specification. Create model prototypes by extending Resource.

A resource instance has its attributes set through the deserialize methods of a serializer after an adapter makes a request. The attributes of the resource instance receive the same attributes in the payload of the request. The same pattern applies for the relationships of a JSON API resource. The relationships property of a resource instance has all the related URL's to fetch the related resources.

The related objects of a resource are setup using helpers toOne and toMany. The attr helper is a computed property that defines a getter and setter. The getter and setter reference the same value on that is set on the "protected" attributes hash of the resource object. Use the attr helpers when you extend Resource to create models (or resources) in your Ember application.

Blueprint

Here is the blueprint for a resource (model) prototype:

import Ember from 'ember';
import Resource from 'ember-jsonapi-resources/models/resource';
import { attr, toOne, toMany } from 'ember-jsonapi-resources/models/resource';

let <%= classifiedModuleName %>Model = Resource.extend({
  type: '<%= resource %>',
  service: Ember.inject.service('<%= resource %>'),

  <%= attrs %>
});

<%= classifiedModuleName %>Model.reopenClass({

  getDefaults() {
    return {
      attributes: {}
    };
  }
});

export default <%= classifiedModuleName %>Model;

Relationships are async - using promise proxy objects. To create relations use the toOne or toMany helper. See examples below under "Extending".

When a template uses the resource's relationship(s), the service makes a request for the relation. See the working with Relationships section below.

An initializer registers model prototypes (or resources) in the container as factories.

The factory uses the options: { instantiate: false, singleton: false }. To create a model instance use the owner API (or the container) to lookup the factory †, for example:

let owner = Ember.getOwner(this);
let model = owner._lookupFactory('model:entity').create({ attributes: { key: value } });

Note: eventually factoryFor will replace _lookupFactory.

Extending Resource

Below are a few resources that are related. A post has one author, and an author has many posts. A post has many comments and a comment has one post. A comment has one commenter and a commenter has many comments.

Use the toOne and toMany helpers to setup relationships on a resource prototype. The relations are resolved on demand using an asynchronous request. The relationships are computed properties on the resource that use a promise proxy object for content of the relationship.

(hasOne and hasMany can be used, as they are aliases for toOne and toMany.)

  • A Post Resource:
import Ember from 'ember';
import Resource from 'ember-jsonapi-resources/models/resource';
import { attr, toOne, toMany } from 'ember-jsonapi-resources/models/resource';

export default Resource.extend({
  type: 'post',
  service: Ember.inject.service('posts'),

  title: attr('string'),
  date: attr('date'),
  excerpt: attr('string'),

  author: toOne('author'),
  comments: toMany('comments')
});
  • An Author Resource:
import Ember from 'ember';
import Resource from 'ember-jsonapi-resources/models/resource';
import { attr, toMany } from 'ember-jsonapi-resources/models/resource';

export default Resource.extend({
  type: 'author',
  service: Ember.inject.service('authors'),

  name: attr('string'),
  email: attr('string'),

  posts: toMany('posts')
});
  • A Comment Resource:
import Ember from 'ember';
import Resource from 'ember-jsonapi-resources/models/resource';
import { attr, toOne } from 'ember-jsonapi-resources/models/resource';

export default Resource.extend({
  type: 'comment',
  service: Ember.inject.service('comments'),

  body: attr('string'),

  date: Ember.computed('attributes', {
    get() {
      return this.get('attributes.created-at');
    }
  }),

  commenter: toOne('commenter'),
  post: toOne('post')
});
  • A Commenter Resource:
import Ember from 'ember';
import Resource from 'ember-jsonapi-resources/models/resource';
import { attr, toMany } from 'ember-jsonapi-resources/models/resource';

export default Resource.extend({
  type: 'commenter',
  service: Ember.inject.service('commenters'),

  name: attr('string'),
  email: attr('string'),
  hash: attr(),

  comments: toMany('comments')
});

Helpers for defining Attributes

The attr, toOne, and toMany helpers define relationships in a resource prototype (class). Use attr helper without an argument to support any type. Or, pass a type to enforce that the attribute is set with the desired type, see above examples. You may set an attribute as read-only by passing false as the second argument.

const immutable = false;
export default Resource.extend({
  type: 'things',
  service: Ember.inject.service('things'),
  "name": attr('string'),
  "created-at": attr('date', immutable),
  "updated-at": attr('date', immutable)
});

The toOne and toMany relationship helpers setup behavior for fetching related resources. They use a promise proxy (object or array) object.

Working with Relationships

For a new resource the relationships are unknown. The new resource instance does not have any URLs from a server. It's the server's job to inform the client of the relationship URLs.

The resource's updateRelationships method manages changes to the relationships data; the resource identifier objects for relationships. Also, there are methods to addRelationships, addRelationship, removeRelationships and removeRelationship. The resource identifier objects are only are identifiers which include the id and type. A new resource does not construct URLs for its relationships' links properties.

Resources use a computed property that is the reference for the relationship. When defining relationship properties on a resource prototype use toOne() and toMany(). The model (or resource) uses the properties as promise proxy objects/arrays. The service fetches relationships when requested (e.g. model.get('relation') or by a template, model.relation).

Methods which operate on the resources relationships, such as updateRelationships, receive arguments for the type and id(s). Instead of using ORM-like behavior, the resource tracks the type and ids for its relationships.

Polymorphic Relationships

A resource prototype defines relationships with helpers: toOne('relation') and toMany('relations'). To define a polymorphic association - create a resource that represents that association and use it's service to fetch resources.

The dummy app has an example, it uses resources: employee, product and pictures. The pictures have an imageable relation which can be either an employee or product resource. There is no model prototype for the imageable resources only a service.

The solution to support polymorphic associations requires an specific service that represents the association. This service utilizes the findRelated method and the URLs provided in the relationship objects of the resource.

Below are modules (resources and service) used in a service for a polymorphic association:

models/employee.js

export default Resource.extend({
  type: 'employees',
  service: Ember.inject.service('employees'),

  name: attr(),

  pictures: toMany('pictures')
});

models/picture.js

export default Resource.extend({
  type: 'pictures',
  service: Ember.inject.service('pictures'),

  name: attr(),

  imageable: toOne('imageable') // polymorphic
});

models/product.js

export default Resource.extend({
  type: 'products',
  service: Ember.inject.service('products'),

  name: attr(),

  pictures: toMany('pictures')
});

services/imageables.js

Adapter.reopenClass({ isServiceFactory: true });

export default Adapter.extend(ServiceCache);

templates/pictures/detail.hbs

<p>Name: {{model.name}}</p>
<p>Imageable: {{model.imageable.name}}</p>

{{outlet}}

Backend examples:

Below is a payload for a resource that uses polymorphic associations.

There are 3 types: Employee, Product, Picture. The Picture belongs to an Imageable resource (Employees and Products both use a to-many relation for pictures).

In the payload below, the request is for /api/v1/pictures?include=imageable. So, the 1st 5 of 6 pictures are returned. And, included are the imageable types: some products and an employee.

The links under each resource's relationships below list the URLs which return the related imageable resource (regardless of type). E.g. /api/v1/pictures/5/imageable may be an Employee and /api/v1/pictures/4/imageable may be a Product.

{
  "data": [
    {
      "id": "1",
      "type": "pictures",
      "links": {
        "self": "http://localhost:3000/api/v1/pictures/1"
      },
      "attributes": {
        "name": "box of chocolates"
      },
      "relationships": {
        "imageable": {
          "links": {
            "self": "http://localhost:3000/api/v1/pictures/1/relationships/imageable",
            "related": "http://localhost:3000/api/v1/pictures/1/imageable"
          },
          "data": {
            "type": "products",
            "id": "3"
          }
        }
      }
    },
    {
      "id": "2",
      "type": "pictures",
      "links": {
        "self": "http://localhost:3000/api/v1/pictures/2"
      },
      "attributes": {
        "name": "10 foot candy cane"
      },
      "relationships": {
        "imageable": {
          "links": {
            "self": "http://localhost:3000/api/v1/pictures/2/relationships/imageable",
            "related": "http://localhost:3000/api/v1/pictures/2/imageable"
          },
          "data": {
            "type": "products",
            "id": "2"
          }
        }
      }
    },
    {
      "id": "3",
      "type": "pictures",
      "links": {
        "self": "http://localhost:3000/api/v1/pictures/3"
      },
      "attributes": {
        "name": "Hot apple fritter"
      },
      "relationships": {
        "imageable": {
          "links": {
            "self": "http://localhost:3000/api/v1/pictures/3/relationships/imageable",
            "related": "http://localhost:3000/api/v1/pictures/3/imageable"
          },
          "data": {
            "type": "products",
            "id": "1"
          }
        }
      }
    },
    {
      "id": "4",
      "type": "pictures",
      "links": {
        "self": "http://localhost:3000/api/v1/pictures/4"
      },
      "attributes": {
        "name": "Boston Creme"
      },
      "relationships": {
        "imageable": {
          "links": {
            "self": "http://localhost:3000/api/v1/pictures/4/relationships/imageable",
            "related": "http://localhost:3000/api/v1/pictures/4/imageable"
          },
          "data": {
            "type": "products",
            "id": "1"
          }
        }
      }
    },
    {
      "id": "5",
      "type": "pictures",
      "links": {
        "self": "http://localhost:3000/api/v1/pictures/5"
      },
      "attributes": {
        "name": "Bill at EmberConf"
      },
      "relationships": {
        "imageable": {
          "links": {
            "self": "http://localhost:3000/api/v1/pictures/5/relationships/imageable",
            "related": "http://localhost:3000/api/v1/pictures/5/imageable"
          },
          "data": {
            "type": "employees",
            "id": "1"
          }
        }
      }
    }
  ],
  "included": [
    {
      "id": "3",
      "type": "products",
      "links": {
        "self": "http://localhost:3000/api/v1/products/3"
      },
      "attributes": {
        "name": "Chocolates"
      },
      "relationships": {
        "pictures": {
          "links": {
            "self": "http://localhost:3000/api/v1/products/3/relationships/pictures",
            "related": "http://localhost:3000/api/v1/products/3/pictures"
          }
        }
      }
    },
    {
      "id": "2",
      "type": "products",
      "links": {
        "self": "http://localhost:3000/api/v1/products/2"
      },
      "attributes": {
        "name": "Candy Canes"
      },
      "relationships": {
        "pictures": {
          "links": {
            "self": "http://localhost:3000/api/v1/products/2/relationships/pictures",
            "related": "http://localhost:3000/api/v1/products/2/pictures"
          }
        }
      }
    },
    {
      "id": "1",
      "type": "products",
      "links": {
        "self": "http://localhost:3000/api/v1/products/1"
      },
      "attributes": {
        "name": "Donuts"
      },
      "relationships": {
        "pictures": {
          "links": {
            "self": "http://localhost:3000/api/v1/products/1/relationships/pictures",
            "related": "http://localhost:3000/api/v1/products/1/pictures"
          }
        }
      }
    },
    {
      "id": "1",
      "type": "employees",
      "links": {
        "self": "http://localhost:3000/api/v1/employees/1"
      },
      "attributes": {
        "name": "Bill Heaton"
      },
      "relationships": {
        "pictures": {
          "links": {
            "self": "http://localhost:3000/api/v1/employees/1/relationships/pictures",
            "related": "http://localhost:3000/api/v1/employees/1/pictures"
          }
        }
      }
    }
  ],
  "meta": {
    "page": {
      "total": 7,
      "sort": [
        {
          "field": "id",
          "direction": "asc"
        }
      ],
      "offset": 0,
      "limit": 5
    }
  },
  "links": {
    "first": "http://localhost:3000/api/v1/pictures?include=imageable&page%5Blimit%5D=5&page%5Boffset%5D=0",
    "next": "http://localhost:3000/api/v1/pictures?include=imageable&page%5Blimit%5D=5&page%5Boffset%5D=5",
    "last": "http://localhost:3000/api/v1/pictures?include=imageable&page%5Blimit%5D=5&page%5Boffset%5D=2"
  }
}

results matching ""

    No results matching ""