Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion addon/adapters/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,19 @@ export default Ember.Object.extend(Ember.Evented, {

@method findRelated
@param {String} resource name (plural) to lookup the service object w/ serializer
or {Object} `{'resource': relation, 'type': type}` used when type is not the
same as the resource name, to fetch relationship using a different service
@param {String} url
@return {Promise}
*/
findRelated(resource, url) {
const service = this.container.lookup('service:' + pluralize(resource));
let type = resource;
if (typeof type === 'object') {
resource = resource.resource;
type = resource.type;
}
let service = this.container.lookup('service:' + pluralize(type));
url = this.fetchUrl(url);
return service.fetch(url, { method: 'GET' });
},

Expand Down
55 changes: 44 additions & 11 deletions addon/models/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,15 @@ const RelatedProxyUtil = Ember.Object.extend({
*/
relationship: null,

/**
The name of the type of resource

@property type
@type String
@required
*/
type: null,

/**
Proxy for the requested relation, resolves w/ content from fulfilled promise

Expand All @@ -389,11 +398,12 @@ const RelatedProxyUtil = Ember.Object.extend({
@return {PromiseProxy} proxy
*/
createProxy: function (resource, proxyFactory) {
const relation = this.get('relationship');
const url = this.proxyUrl(resource, relation);
const service = resource.container.lookup('service:' + pluralize(relation));
let relation = this.get('relationship');
let type = this.get('type');
let url = this.proxyUrl(resource, relation);
let service = resource.container.lookup('service:' + pluralize(type));
let promise = this.promiseFromCache(resource, relation, service);
promise = promise || service.findRelated(relation, url);
promise = promise || service.findRelated({'resource': relation, 'type': type}, url);
let proxy = proxyFactory.extend(Ember.PromiseProxyMixin, {
'promise': promise, 'type': relation
});
Expand Down Expand Up @@ -476,14 +486,30 @@ function linksPath(relation) {

@method hasOne
@param {String} relation
Or, {Object} with properties for `resource` and `type`
*/
export function hasOne(relation) {
assertDasherizedHasOneRelation(relation);
const util = RelatedProxyUtil.create({'relationship': relation});
const path = linksPath(relation);
let type = relation;
if (typeof type === 'object') {
assertResourceAndTypeProps(relation);
type = relation.type;
relation = relation.resource;
}
assertDasherizedHasOneRelation(type);
let util = RelatedProxyUtil.create({'relationship': relation, 'type': type});
let path = linksPath(relation);
return Ember.computed(path, function () {
return util.createProxy(this, Ember.ObjectProxy);
}).meta({relation: relation, kind: 'hasOne'});
}).meta({relation: relation, type: type, kind: 'hasOne'});
}

function assertResourceAndTypeProps(relation) {
try {
let msg = 'Options must include properties: resource, type';
Ember.assert(msg, relation && relation.resource && relation.type);
} catch(e) {
console.warn(e.message);
}
}

function assertDasherizedHasOneRelation(name) {
Expand All @@ -502,14 +528,21 @@ function assertDasherizedHasOneRelation(name) {

@method hasMany
@param {String} relation
Or, {Object} with properties for `resource` and `type`
*/
export function hasMany(relation) {
let type = relation;
if (typeof type === 'object') {
assertResourceAndTypeProps(relation);
type = relation.type;
relation = relation.resource;
}
assertDasherizedHasManyRelation(relation);
const util = RelatedProxyUtil.create({'relationship': relation});
const path = linksPath(relation);
let util = RelatedProxyUtil.create({'relationship': relation, 'type': type});
let path = linksPath(relation);
return Ember.computed(path, function () {
return util.createProxy(this, Ember.ArrayProxy);
}).meta({relation: relation, kind: 'hasMany'});
}).meta({relation: relation, type: type, kind: 'hasMany'});
}

function assertDasherizedHasManyRelation(name) {
Expand Down
21 changes: 21 additions & 0 deletions tests/helpers/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ export const Commenter = Resource.extend({
comments: hasMany('comments')
});

export const Person = Resource.extend({
type: 'people',
name: attr()
});

export const Employee = Person.extend({
type: 'employees',
supervisor: hasOne({ resource: 'supervisor', type: 'people' })
});

export const Supervisor = Employee.extend({
type: 'supervisors',
directReports: hasMany({ resource: 'employees', type: 'people' })
});

export function setup() {
const opts = { instantiate: false, singleton: false };
Post.prototype.container = this.container;
Expand All @@ -38,6 +53,12 @@ export function setup() {
this.registry.register('model:comments', Comment, opts);
Commenter.prototype.container = this.container;
this.registry.register('model:commenters', Commenter, opts);
Person.prototype.container = this.container;
this.registry.register('model:persons', Person, opts);
Employee.prototype.container = this.container;
this.registry.register('model:employees', Employee, opts);
Supervisor.prototype.container = this.container;
this.registry.register('model:supervisors', Supervisor, opts);
}

export function teardown() {
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/adapters/application-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,36 @@ test('#findRelated', function(assert) {
assert.ok(service.fetch.calledWith(expectURL, { method: 'GET' }), 'url for relation passed to service#fetch');
});

test('#findRelated can be called with optional type for the resource', function (assert) {
assert.expect(4);
const done = assert.async();
let PersonAdapter = Adapter.extend({type: 'people', url: '/people'});
PersonAdapter.reopenClass({ isServiceFactory: true });
this.registry.register('service:people', PersonAdapter.extend());
let service = this.container.lookup('service:people');
let stub = sandbox.stub(service, 'findRelated', function () { return Ember.RSVP.Promise.resolve(null); });
let resource = this.container.lookupFactory('model:employees').create({
type: 'employees',
id: 1000001,
name: 'The Special',
relationships: {
supervisor: {
links: {
related: 'http://locahost:3000/api/v1/employees/1/supervisor'
}
}
}
});
let url = resource.get( ['relationships', 'supervisor', 'links', 'related'].join('.') );
resource.get('supervisor').then(function() {
assert.ok(stub.calledOnce, 'people service findRelated method called once');
assert.equal(stub.firstCall.args[0].resource, 'supervisor', 'findRelated called with supervisor resource');
assert.equal(stub.firstCall.args[0].type, 'people', 'findRelated called with people type');
assert.equal(stub.firstCall.args[1], url, 'findRelated called with url, ' + url);
done();
});
});

test('#createResource', function(assert) {
const adapter = this.subject({type: 'posts', url: '/posts'});
adapter.serializer = { serialize: function () { return postMock; } };
Expand Down