diff --git a/addons/api/addon/adapters/application.js b/addons/api/addon/adapters/application.js index 3fb95fda9..71c109cb0 100644 --- a/addons/api/addon/adapters/application.js +++ b/addons/api/addon/adapters/application.js @@ -18,4 +18,23 @@ export default class ApplicationAdapter extends RESTAdapter { * @type {string} */ namespace = get(config, 'api.namespace'); + + // =methods + + /** + * Overrides default ajax method by rewritting PUT requests as PATCH. + * PATCH is the request method used by our API for updates. + * + * @override + * @method ajax + * @private + * @param {String} url + * @param {String} type The request type GET, POST, PUT, DELETE etc. + * @param {Object} options + * @return {Promise} promise + */ + ajax(url, type, options) { + const method = type === 'PUT' ? 'PATCH' : type; + return super.ajax(url, method, options); + } } diff --git a/addons/api/addon/models/base.js b/addons/api/addon/models/base.js index d8d0d004d..389327972 100644 --- a/addons/api/addon/models/base.js +++ b/addons/api/addon/models/base.js @@ -1,3 +1,22 @@ import Model from '@ember-data/model'; +import { computed } from '@ember/object'; -export default class BaseModel extends Model {} +export default class BaseModel extends Model { + // =attributes + + /** + * @type {boolean} + */ + @computed('hasDirtyAttributes', 'isSaving') + get canSave() { + return this.hasDirtyAttributes && !this.isSaving; + } + + /** + * @type {boolean} + */ + @computed('canSave') + get cannotSave() { + return !this.canSave; + } +} diff --git a/addons/api/addon/serializers/application.js b/addons/api/addon/serializers/application.js index 92237e8f8..3ad83956e 100644 --- a/addons/api/addon/serializers/application.js +++ b/addons/api/addon/serializers/application.js @@ -20,6 +20,8 @@ export default class ApplicationSerializer extends RESTSerializer { /** * If an attribute is annotated as readOnly in the model, don't serialize it. * Otherwise delegate to default attribute serializer. + * + * @override * @method serializeAttribute * @param {Snapshot} snapshot * @param {Object} json @@ -30,6 +32,23 @@ export default class ApplicationSerializer extends RESTSerializer { return options.readOnly ? null : super.serializeAttribute(...arguments); } + /** + * In our API, request payloads are unrooted. But Ember Data roots request + * payloads for this adapter by default. Instead of assigned a root key + * on the outgoing hash, we copy the serialized attributes into it. + * + * @override + * @method serializeIntoHash + * @param {Object} hash + * @param {Model} typeClass + * @param {Snapshot} snapshot + * @param {Object} options + */ + serializeIntoHash(hash, typeClass, snapshot, options) { + const serialized = this.serialize(snapshot, options); + Object.assign(hash, serialized); + } + /** * In our API, array payloads are always rooted under the same key `items`. * Ember Data normally expects them to be rooted under their model name, diff --git a/addons/api/tests/unit/adapters/application-test.js b/addons/api/tests/unit/adapters/application-test.js index 16d34c9b1..5d07e1ccf 100644 --- a/addons/api/tests/unit/adapters/application-test.js +++ b/addons/api/tests/unit/adapters/application-test.js @@ -1,6 +1,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import config from 'ember-get-config'; +import RESTAdapter from '@ember-data/adapter/rest'; module('Unit | Adapter | application', function (hooks) { setupTest(hooks); @@ -17,4 +18,14 @@ module('Unit | Adapter | application', function (hooks) { const adapter = this.owner.lookup('adapter:application'); assert.equal(adapter.urlPrefix(), config.api.namespace); }); + + test('it rewrites PUT to PATCH, but leaves others unchanged', function (assert) { + assert.expect(1); + const adapter = this.owner.lookup('adapter:application'); + // TODO this is icky, should be changed to a spy or stub + RESTAdapter.prototype.ajax = (url, type) => { + assert.equal(type, 'PATCH'); + }; + adapter.ajax('/', 'PUT'); + }); }); diff --git a/addons/api/tests/unit/models/base-test.js b/addons/api/tests/unit/models/base-test.js index d2cb2f2b4..0fe0de40f 100644 --- a/addons/api/tests/unit/models/base-test.js +++ b/addons/api/tests/unit/models/base-test.js @@ -10,4 +10,30 @@ module('Unit | Model | base', function (hooks) { let model = store.createRecord('base', {}); assert.ok(model); }); + + test('it has canSave and cannotSave attributes', function (assert) { + assert.expect(8); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '1', + type: 'project', + attributes: {}, + }, + }); + const model = store.peekRecord('project', '1'); + assert.equal(model.canSave, false); + assert.equal(model.cannotSave, true); + // Should be able to save if dirty + model.name = 'Project'; + assert.equal(model.hasDirtyAttributes, true); + assert.equal(model.canSave, true); + assert.equal(model.cannotSave, false); + // Should not be able to save while currently saving + model.transitionTo('updated.inFlight'); + assert.equal(model.isSaving, true); + assert.equal(model.canSave, false); + assert.equal(model.cannotSave, true); + model.transitionTo('loaded'); + }); }); diff --git a/addons/api/tests/unit/serializers/application-test.js b/addons/api/tests/unit/serializers/application-test.js index fabb999ed..7949540b5 100644 --- a/addons/api/tests/unit/serializers/application-test.js +++ b/addons/api/tests/unit/serializers/application-test.js @@ -75,7 +75,7 @@ module('Unit | Serializer | application', function (hooks) { const projectModelClass = store.createRecord('project').constructor; const payload = { id: '1', - name: 'Project 1' + name: 'Project 1', }; const normalized = serializer.normalizeSingleResponse( store, @@ -89,7 +89,7 @@ module('Unit | Serializer | application', function (hooks) { type: 'project', attributes: { name: 'Project 1' }, relationships: {}, - } + }, }); }); });