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.
Links
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:
- http://guides.rubyonrails.org/association_basics.html#polymorphic-associations
- https://github.com/cerebris/jsonapi-resources#options (used polymorphic option)
- Example API in the dummy app: https://github.com/pixelhandler/blog-api/compare/polymorphic-example
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"
}
}