diff --git a/addon/adapters/application.js b/addon/adapters/application.js index 3e5c638..e13a48b 100644 --- a/addon/adapters/application.js +++ b/addon/adapters/application.js @@ -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' }); }, diff --git a/addon/models/resource.js b/addon/models/resource.js index 56ece5c..4b03f71 100644 --- a/addon/models/resource.js +++ b/addon/models/resource.js @@ -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 @@ -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 }); @@ -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) { @@ -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) { diff --git a/tests/helpers/resources.js b/tests/helpers/resources.js index 5d492aa..5e6879d 100644 --- a/tests/helpers/resources.js +++ b/tests/helpers/resources.js @@ -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; @@ -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() { diff --git a/tests/unit/adapters/application-test.js b/tests/unit/adapters/application-test.js index 97b1b2b..8446d30 100644 --- a/tests/unit/adapters/application-test.js +++ b/tests/unit/adapters/application-test.js @@ -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; } };