diff --git a/FEATURES.md b/FEATURES.md index 8e86c748d37..9456bb987bd 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -11,6 +11,12 @@ entry in `config/features.json`. ## Feature Flags +- `ds-find-include` + + Allows an `include` query parameter to be specified with using + `store.findRecord()` and `store.findAll()` as described in [RFC + 99](https://github.com/emberjs/rfcs/pull/99) + - `ds-references` Adds references as described in [RFC 57](https://github.com/emberjs/rfcs/pull/57) diff --git a/addon/-private/adapters/rest-adapter.js b/addon/-private/adapters/rest-adapter.js index 7e28d4fdbf8..f27b8e164c6 100644 --- a/addon/-private/adapters/rest-adapter.js +++ b/addon/-private/adapters/rest-adapter.js @@ -11,10 +11,13 @@ import { AbortError } from 'ember-data/-private/adapters/errors'; import EmptyObject from "ember-data/-private/system/empty-object"; -var get = Ember.get; -var MapWithDefault = Ember.MapWithDefault; - import BuildURLMixin from "ember-data/-private/adapters/build-url-mixin"; +import isEnabled from 'ember-data/-private/features'; + +const { + MapWithDefault, + get +} = Ember; /** The REST adapter allows your store to communicate with an HTTP server by @@ -372,7 +375,10 @@ export default Adapter.extend(BuildURLMixin, { @return {Promise} promise */ findRecord(store, type, id, snapshot) { - return this.ajax(this.buildURL(type.modelName, id, snapshot, 'findRecord'), 'GET'); + const url = this.buildURL(type.modelName, id, snapshot, 'findRecord'); + const query = this.buildQuery(snapshot); + + return this.ajax(url, 'GET', { data: query }); }, /** @@ -390,14 +396,13 @@ export default Adapter.extend(BuildURLMixin, { @return {Promise} promise */ findAll(store, type, sinceToken, snapshotRecordArray) { - var query, url; + const url = this.buildURL(type.modelName, null, null, 'findAll'); + const query = this.buildQuery(snapshotRecordArray); if (sinceToken) { - query = { since: sinceToken }; + query.since = sinceToken; } - url = this.buildURL(type.modelName, null, null, 'findAll'); - return this.ajax(url, 'GET', { data: query }); }, @@ -959,6 +964,20 @@ export default Adapter.extend(BuildURLMixin, { return ['Ember Data Request ' + requestDescription + ' returned a ' + status, payloadDescription, shortenedPayload].join('\n'); + }, + + buildQuery(snapshot) { + const { include } = snapshot; + + let query = {}; + + if (isEnabled('ds-finder-include')) { + if (include) { + query.include = include; + } + } + + return query; } }); diff --git a/addon/-private/system/model/internal-model.js b/addon/-private/system/model/internal-model.js index 51efca00e4c..9b42b151868 100644 --- a/addon/-private/system/model/internal-model.js +++ b/addon/-private/system/model/internal-model.js @@ -251,10 +251,7 @@ InternalModel.prototype = { @private */ createSnapshot(options) { - var adapterOptions = options && options.adapterOptions; - var snapshot = new Snapshot(this); - snapshot.adapterOptions = adapterOptions; - return snapshot; + return new Snapshot(this, options); }, /** diff --git a/addon/-private/system/record-arrays/record-array.js b/addon/-private/system/record-arrays/record-array.js index bb84147d6a4..a34aa397d62 100644 --- a/addon/-private/system/record-arrays/record-array.js +++ b/addon/-private/system/record-arrays/record-array.js @@ -202,8 +202,7 @@ export default Ember.ArrayProxy.extend(Ember.Evented, { }, createSnapshot(options) { - var adapterOptions = options && options.adapterOptions; - var meta = this.get('meta'); - return new SnapshotRecordArray(this, meta, adapterOptions); + const meta = this.get('meta'); + return new SnapshotRecordArray(this, meta, options); } }); diff --git a/addon/-private/system/snapshot-record-array.js b/addon/-private/system/snapshot-record-array.js index 130cde032c2..a7ccfa8c13d 100644 --- a/addon/-private/system/snapshot-record-array.js +++ b/addon/-private/system/snapshot-record-array.js @@ -2,6 +2,8 @@ @module ember-data */ +import isEnabled from 'ember-data/-private/features'; + /** @class SnapshotRecordArray @namespace DS @@ -10,7 +12,7 @@ @param {Array} snapshots An array of snapshots @param {Object} meta */ -export default function SnapshotRecordArray(recordArray, meta, adapterOptions) { +export default function SnapshotRecordArray(recordArray, meta, options = {}) { /** An array of snapshots @private @@ -48,7 +50,11 @@ export default function SnapshotRecordArray(recordArray, meta, adapterOptions) { @property adapterOptions @type {Object} */ - this.adapterOptions = adapterOptions; + this.adapterOptions = options.adapterOptions; + + if (isEnabled('ds-finder-include')) { + this.include = options.include; + } } /** diff --git a/addon/-private/system/snapshot.js b/addon/-private/system/snapshot.js index c7f23c9e29f..e594b4dd252 100644 --- a/addon/-private/system/snapshot.js +++ b/addon/-private/system/snapshot.js @@ -4,6 +4,8 @@ import Ember from 'ember'; import EmptyObject from "ember-data/-private/system/empty-object"; +import isEnabled from 'ember-data/-private/features'; + var get = Ember.get; /** @@ -13,7 +15,7 @@ var get = Ember.get; @constructor @param {DS.Model} internalModel The model to create a snapshot from */ -export default function Snapshot(internalModel) { +export default function Snapshot(internalModel, options = {}) { this._attributes = new EmptyObject(); this._belongsToRelationships = new EmptyObject(); this._belongsToIds = new EmptyObject(); @@ -29,6 +31,17 @@ export default function Snapshot(internalModel) { this.type = internalModel.type; this.modelName = internalModel.type.modelName; + /** + A hash of adapter options + @property adapterOptions + @type {Object} + */ + this.adapterOptions = options.adapterOptions; + + if (isEnabled('ds-finder-include')) { + this.include = options.include; + } + this._changedAttributes = record.changedAttributes(); } diff --git a/config/features.json b/config/features.json index 444c96da30d..82108bcfad3 100644 --- a/config/features.json +++ b/config/features.json @@ -1,3 +1,4 @@ { + "ds-finder-include": null, "ds-references": null } diff --git a/tests/integration/adapter/rest-adapter-test.js b/tests/integration/adapter/rest-adapter-test.js index 0f6749259d5..cb44493cd1f 100644 --- a/tests/integration/adapter/rest-adapter-test.js +++ b/tests/integration/adapter/rest-adapter-test.js @@ -4,6 +4,7 @@ import Ember from 'ember'; import {module, test} from 'qunit'; import DS from 'ember-data'; +import isEnabled from 'ember-data/-private/features'; var env, store, adapter, Post, Comment, SuperUser; var passedUrl, passedVerb, passedHash; @@ -56,7 +57,7 @@ test("findRecord - basic payload", function(assert) { run(store, 'findRecord', 'post', 1).then(assert.wait(function(post) { assert.equal(passedUrl, "/posts/1"); assert.equal(passedVerb, "GET"); - assert.equal(passedHash, undefined); + assert.deepEqual(passedHash.data, {}); assert.equal(post.get('id'), "1"); assert.equal(post.get('name'), "Rails is omakase"); @@ -83,7 +84,7 @@ test("find - basic payload (with legacy singular name)", function(assert) { run(store, 'findRecord', 'post', 1).then(assert.wait(function(post) { assert.equal(passedUrl, "/posts/1"); assert.equal(passedVerb, "GET"); - assert.equal(passedHash, undefined); + assert.deepEqual(passedHash.data, {}); assert.equal(post.get('id'), "1"); assert.equal(post.get('name'), "Rails is omakase"); @@ -101,7 +102,7 @@ test("findRecord - payload with sideloaded records of the same type", function(a run(store, 'findRecord', 'post', 1).then(assert.wait(function(post) { assert.equal(passedUrl, "/posts/1"); assert.equal(passedVerb, "GET"); - assert.equal(passedHash, undefined); + assert.deepEqual(passedHash.data, {}); assert.equal(post.get('id'), "1"); assert.equal(post.get('name'), "Rails is omakase"); @@ -121,7 +122,7 @@ test("findRecord - payload with sideloaded records of a different type", functio run(store, 'findRecord', 'post', 1).then(assert.wait(function(post) { assert.equal(passedUrl, "/posts/1"); assert.equal(passedVerb, "GET"); - assert.equal(passedHash, undefined); + assert.deepEqual(passedHash.data, {}); assert.equal(post.get('id'), "1"); assert.equal(post.get('name'), "Rails is omakase"); @@ -143,7 +144,7 @@ test("findRecord - payload with an serializer-specified primary key", function(a run(store, 'findRecord', 'post', 1).then(assert.wait(function(post) { assert.equal(passedUrl, "/posts/1"); assert.equal(passedVerb, "GET"); - assert.equal(passedHash, undefined); + assert.deepEqual(passedHash.data, {}); assert.equal(post.get('id'), "1"); assert.equal(post.get('name'), "Rails is omakase"); @@ -167,7 +168,7 @@ test("findRecord - payload with a serializer-specified attribute mapping", funct run(store, 'findRecord', 'post', 1).then(assert.wait(function(post) { assert.equal(passedUrl, "/posts/1"); assert.equal(passedVerb, "GET"); - assert.equal(passedHash, undefined); + assert.deepEqual(passedHash.data, {}); assert.equal(post.get('id'), "1"); assert.equal(post.get('name'), "Rails is omakase"); @@ -175,6 +176,23 @@ test("findRecord - payload with a serializer-specified attribute mapping", funct })); }); +if (isEnabled('ds-finder-include')) { + test("findRecord - passes `include` as a query parameter to ajax", function(assert) { + adapter.ajax = function(url, verb, hash) { + assert.deepEqual(hash.data, { include: 'comments' }, + '`include` parameter sent to adapter.ajax'); + + return run(Ember.RSVP, 'resolve', { + post: { id: 1, name: 'Rails is very expensive sushi' } + }); + }; + + run(store, 'findRecord', 'post', 1, { include: 'comments' }).then(assert.wait(function() { + // Noop + })); + }); +} + test("createRecord - an empty payload is a basic success if an id was specified", function(assert) { ajaxResponse(); var post; @@ -983,7 +1001,7 @@ test("findAll - returning an array populates the array", function(assert) { store.findAll('post').then(assert.wait(function(posts) { assert.equal(passedUrl, "/posts"); assert.equal(passedVerb, "GET"); - assert.equal(passedHash.data, undefined); + assert.deepEqual(passedHash.data, {}); var post1 = store.peekRecord('post', 1); var post2 = store.peekRecord('post', 2); @@ -1028,6 +1046,23 @@ test("findAll - passes buildURL the requestType", function(assert) { })); }); +if (isEnabled('ds-finder-include')) { + test("findAll - passed `include` as a query parameter to ajax", function(assert) { + adapter.ajax = function(url, verb, hash) { + assert.deepEqual(hash.data, { include: 'comments' }, + '`include` params sent to adapter.ajax'); + + return run(Ember.RSVP, 'resolve', { + posts: [{ id: 1, name: 'Rails is very expensive sushi' }] + }); + }; + + run(store, 'findAll', 'post', { include: 'comments' }).then(assert.wait(function() { + // Noop + })); + }); +} + test("findAll - returning sideloaded data loads the data", function(assert) { ajaxResponse({ posts: [ @@ -1473,7 +1508,7 @@ test("findMany - findMany does not coalesce by default", function(assert) { }); run(post, 'get', 'comments').then(assert.wait(function(comments) { assert.equal(passedUrl, "/comments/3"); - assert.equal(passedHash, null); + assert.deepEqual(passedHash.data, {}); })); }); diff --git a/tests/integration/adapter/store-adapter-test.js b/tests/integration/adapter/store-adapter-test.js index 1ccc6c3c418..008f967738f 100644 --- a/tests/integration/adapter/store-adapter-test.js +++ b/tests/integration/adapter/store-adapter-test.js @@ -4,6 +4,7 @@ import Ember from 'ember'; import {module, test} from 'qunit'; import DS from 'ember-data'; +import isEnabled from 'ember-data/-private/features'; /* This is an integration test that tests the communication between a store @@ -1303,7 +1304,7 @@ test("record.save should pass adapterOptions to the deleteRecord method", functi }); -test("findRecord should pass adapterOptions to the find method", function(assert) { +test("store.findRecord should pass adapterOptions to adapter.findRecord", function(assert) { assert.expect(1); env.adapter.findRecord = assert.wait(function(store, type, id, snapshot) { @@ -1316,8 +1317,20 @@ test("findRecord should pass adapterOptions to the find method", function(assert }); }); +if (isEnabled('ds-finder-include')) { + test("store.findRecord should pass 'include' to adapter.findRecord", function(assert) { + assert.expect(1); -test("findAll should pass adapterOptions to the findAll method", function(assert) { + env.adapter.findRecord = assert.wait((store, type, id, snapshot) => { + assert.equal(snapshot.include, 'books', 'include passed to adapter.findRecord'); + return Ember.RSVP.resolve({ id: 1 }); + }); + + run(() => store.findRecord('person', 1, { include: 'books' })); + }); +} + +test("store.findAll should pass adapterOptions to the adapter.findAll method", function(assert) { assert.expect(1); env.adapter.findAll = assert.wait(function(store, type, sinceToken, arraySnapshot) { @@ -1331,6 +1344,18 @@ test("findAll should pass adapterOptions to the findAll method", function(assert }); }); +if (isEnabled('ds-finder-include')) { + test("store.findAll should pass 'include' to adapter.findAll", function(assert) { + assert.expect(1); + + env.adapter.findAll = assert.wait((store, type, sinceToken, arraySnapshot) => { + assert.equal(arraySnapshot.include, 'books', 'include passed to adapter.findAll'); + return Ember.RSVP.resolve([{ id: 1 }]); + }); + + run(() => store.findAll('person', { include: 'books' })); + }); +} test("An async hasMany relationship with links should not trigger shouldBackgroundReloadRecord", function(assert) { var Post = DS.Model.extend({ diff --git a/tests/unit/adapters/rest-adapter/ajax-test.js b/tests/unit/adapters/rest-adapter/ajax-test.js index ade40bc15d3..f699282cf67 100644 --- a/tests/unit/adapters/rest-adapter/ajax-test.js +++ b/tests/unit/adapters/rest-adapter/ajax-test.js @@ -35,8 +35,8 @@ test("When an id is searched, the correct url should be generated", function(ass return Ember.RSVP.resolve(); }; run(function() { - adapter.findRecord(store, Person, 1); - adapter.findRecord(store, Place, 1); + adapter.findRecord(store, Person, 1, {}); + adapter.findRecord(store, Place, 1, {}); }); }); @@ -47,7 +47,7 @@ test("id's should be sanatized", function(assert) { return Ember.RSVP.resolve(); }; run(function() { - adapter.findRecord(store, Person, '../place/1'); + adapter.findRecord(store, Person, '../place/1', {}); }); }); diff --git a/tests/unit/adapters/rest-adapter/build-query-test.js b/tests/unit/adapters/rest-adapter/build-query-test.js new file mode 100644 index 00000000000..fa098e56c2e --- /dev/null +++ b/tests/unit/adapters/rest-adapter/build-query-test.js @@ -0,0 +1,25 @@ +import { module, test } from 'qunit'; +import DS from 'ember-data'; +import isEnabled from 'ember-data/-private/features'; + +module("unit/adapters/rest-adapter/build-query - building queries"); + +test("buildQuery() returns an empty query when snapshot has no query params", function(assert) { + const adapter = DS.RESTAdapter.create(); + const snapshotStub = {}; + + const query = adapter.buildQuery(snapshotStub); + + assert.deepEqual(query, {}, 'query is empty'); +}); + +if (isEnabled('ds-finder-include')) { + test("buildQuery() returns query with `include` from snapshot", function(assert) { + const adapter = DS.RESTAdapter.create(); + const snapshotStub = { include: 'comments' }; + + const query = adapter.buildQuery(snapshotStub); + + assert.deepEqual(query, { include: 'comments' }, 'query includes `include`'); + }); +}