From 404af751fa23d0ea973cdf66e3857514f3d7f07f Mon Sep 17 00:00:00 2001 From: tchak Date: Wed, 17 Jun 2015 11:49:34 +0200 Subject: [PATCH] [FEATURE ds-extended-errors] Make adapter error extendable and add more error types --- FEATURES.md | 15 ++++ addon/adapters/errors.js | 75 +++++++++++++++---- addon/adapters/rest.js | 17 +++++ addon/index.js | 12 +++ config/features.json | 3 +- .../integration/adapter/rest-adapter-test.js | 60 +++++++++++++++ tests/unit/adapter-errors-test.js | 54 +++++++++++++ 7 files changed, 220 insertions(+), 16 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index b929d18f8ea..ab8f59f32f0 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -66,3 +66,18 @@ entry in `config/features.json`. This is particularly useful for polymorphic relationships not backed by STI when just including the id of the records is not enough. + +- `ds-extended-errors` + + Enables `extend` method on errors. It means you can extend from `DS.AdapterError`. + + ```js + const MyCustomError = DS.AdapterError.extend({ message: "My custom error." }); + ``` + + It will also add a few new errors to rest adapter based on http status. + + * [401] `DS.UnauthorizedError` + * [403] `DS.ForbiddenError` + * [404] `DS.NotFoundError` + * [409] `DS.ConflictError` diff --git a/addon/adapters/errors.js b/addon/adapters/errors.js index 7a5d42af936..93b8ca1e40c 100644 --- a/addon/adapters/errors.js +++ b/addon/adapters/errors.js @@ -1,5 +1,6 @@ import Ember from 'ember'; import {assert} from 'ember-data/-private/debug'; +import isEnabled from "ember-data/-private/features"; const EmberError = Ember.Error; @@ -23,8 +24,34 @@ export function AdapterError(errors, message = 'Adapter operation failed') { ]; } +const extendedErrorsEnabled = isEnabled('ds-extended-errors'); + +function extendFn(ErrorClass) { + return function({ message: defaultMessage } = {}) { + return extend(ErrorClass, defaultMessage); + }; +} + +function extend(ParentErrorClass, defaultMessage) { + let ErrorClass = function(errors, message) { + assert('`AdapterError` expects json-api formatted errors array.', Ember.isArray(errors || [])); + ParentErrorClass.call(this, errors, message || defaultMessage); + }; + ErrorClass.prototype = Object.create(ParentErrorClass.prototype); + + if (extendedErrorsEnabled) { + ErrorClass.extend = extendFn(ErrorClass); + } + + return ErrorClass; +} + AdapterError.prototype = Object.create(EmberError.prototype); +if (extendedErrorsEnabled) { + AdapterError.extend = extendFn(AdapterError); +} + /** A `DS.InvalidError` is used by an adapter to signal the external API was unable to process a request because the content was not @@ -83,32 +110,50 @@ AdapterError.prototype = Object.create(EmberError.prototype); @class InvalidError @namespace DS */ -export function InvalidError(errors) { - assert('`InvalidError` expects json-api formatted errors array.', Ember.isArray(errors || [])); - AdapterError.call(this, errors, 'The adapter rejected the commit because it was invalid'); -} - -InvalidError.prototype = Object.create(AdapterError.prototype); +export const InvalidError = extend(AdapterError, + 'The adapter rejected the commit because it was invalid'); /** @class TimeoutError @namespace DS */ -export function TimeoutError() { - AdapterError.call(this, null, 'The adapter operation timed out'); -} - -TimeoutError.prototype = Object.create(AdapterError.prototype); +export const TimeoutError = extend(AdapterError, + 'The adapter operation timed out'); /** @class AbortError @namespace DS */ -export function AbortError() { - AdapterError.call(this, null, 'The adapter operation was aborted'); -} +export const AbortError = extend(AdapterError, + 'The adapter operation was aborted'); -AbortError.prototype = Object.create(AdapterError.prototype); +/** + @class UnauthorizedError + @namespace DS +*/ +export const UnauthorizedError = extendedErrorsEnabled ? + extend(AdapterError, 'The adapter operation is unauthorized') : null; + +/** + @class ForbiddenError + @namespace DS +*/ +export const ForbiddenError = extendedErrorsEnabled ? + extend(AdapterError, 'The adapter operation is forbidden') : null; + +/** + @class NotFoundError + @namespace DS +*/ +export const NotFoundError = extendedErrorsEnabled ? + extend(AdapterError, 'The adapter could not find the resource') : null; + +/** + @class ConflictError + @namespace DS +*/ +export const ConflictError = extendedErrorsEnabled ? + extend(AdapterError, 'The adapter operation failed due to a conflict') : null; /** @method errorsHashToArray diff --git a/addon/adapters/rest.js b/addon/adapters/rest.js index f48b66ea56b..448c901af26 100644 --- a/addon/adapters/rest.js +++ b/addon/adapters/rest.js @@ -7,6 +7,10 @@ import Adapter from "ember-data/adapter"; import { AdapterError, InvalidError, + UnauthorizedError, + ForbiddenError, + NotFoundError, + ConflictError, TimeoutError, AbortError } from 'ember-data/adapters/errors'; @@ -906,6 +910,19 @@ var RESTAdapter = Adapter.extend(BuildURLMixin, { let errors = this.normalizeErrorResponse(status, headers, payload); let detailedMessage = this.generatedDetailedMessage(status, headers, payload, requestData); + if (isEnabled('ds-extended-errors')) { + switch (status) { + case 401: + return new UnauthorizedError(errors, detailedMessage); + case 403: + return new ForbiddenError(errors, detailedMessage); + case 404: + return new NotFoundError(errors, detailedMessage); + case 409: + return new ConflictError(errors, detailedMessage); + } + } + return new AdapterError(errors, detailedMessage); }, diff --git a/addon/index.js b/addon/index.js index 749e32c25b4..cb812ef3567 100644 --- a/addon/index.js +++ b/addon/index.js @@ -1,5 +1,6 @@ import Ember from "ember"; import { warn } from "ember-data/-private/debug"; +import isEnabled from "ember-data/-private/features"; /** Ember Data @module ember-data @@ -40,6 +41,10 @@ import DebugAdapter from "ember-data/-private/system/debug"; import { AdapterError, InvalidError, + UnauthorizedError, + ForbiddenError, + NotFoundError, + ConflictError, TimeoutError, AbortError, errorsHashToArray, @@ -102,6 +107,13 @@ DS.InvalidError = InvalidError; DS.TimeoutError = TimeoutError; DS.AbortError = AbortError; +if (isEnabled('ds-extended-errors')) { + DS.UnauthorizedError = UnauthorizedError; + DS.ForbiddenError = ForbiddenError; + DS.NotFoundError = NotFoundError; + DS.ConflictError = ConflictError; +} + DS.errorsHashToArray = errorsHashToArray; DS.errorsArrayToHash = errorsArrayToHash; diff --git a/config/features.json b/config/features.json index 4bb5e64d30b..9b315a88126 100644 --- a/config/features.json +++ b/config/features.json @@ -5,5 +5,6 @@ "ds-references": null, "ds-transform-pass-options": null, "ds-pushpayload-return": null, - "ds-serialize-ids-and-types": null + "ds-serialize-ids-and-types": null, + "ds-extended-errors": null } diff --git a/tests/integration/adapter/rest-adapter-test.js b/tests/integration/adapter/rest-adapter-test.js index 445cc1f0da8..f0c0fa26755 100644 --- a/tests/integration/adapter/rest-adapter-test.js +++ b/tests/integration/adapter/rest-adapter-test.js @@ -2302,6 +2302,66 @@ test('on error appends errorThrown for sanity', function(assert) { } }); +if (isEnabled('ds-extended-errors')) { + test("rejects promise with a specialized subclass of DS.AdapterError if ajax responds with http error codes", function(assert) { + assert.expect(8); + + var originalAjax = Ember.$.ajax; + var jqXHR = { + getAllResponseHeaders: function() { return ''; } + }; + + Ember.$.ajax = function(hash) { + jqXHR.status = 401; + hash.error(jqXHR, 'error'); + }; + + Ember.run(function() { + store.find('post', '1').then(null, function(reason) { + assert.ok(true, 'promise should be rejected'); + assert.ok(reason instanceof DS.UnauthorizedError, 'reason should be an instance of DS.UnauthorizedError'); + }); + }); + + Ember.$.ajax = function(hash) { + jqXHR.status = 403; + hash.error(jqXHR, 'error'); + }; + + Ember.run(function() { + store.find('post', '1').then(null, function(reason) { + assert.ok(true, 'promise should be rejected'); + assert.ok(reason instanceof DS.ForbiddenError, 'reason should be an instance of DS.ForbiddenError'); + }); + }); + + Ember.$.ajax = function(hash) { + jqXHR.status = 404; + hash.error(jqXHR, 'error'); + }; + + Ember.run(function() { + store.find('post', '1').then(null, function(reason) { + assert.ok(true, 'promise should be rejected'); + assert.ok(reason instanceof DS.NotFoundError, 'reason should be an instance of DS.NotFoundError'); + }); + }); + + Ember.$.ajax = function(hash) { + jqXHR.status = 409; + hash.error(jqXHR, 'error'); + }; + + Ember.run(function() { + store.find('post', '1').then(null, function(reason) { + assert.ok(true, 'promise should be rejected'); + assert.ok(reason instanceof DS.ConflictError, 'reason should be an instance of DS.ConflictError'); + }); + }); + + Ember.$.ajax = originalAjax; + }); +} test('on error wraps the error string in an DS.AdapterError object', function(assert) { assert.expect(2); diff --git a/tests/unit/adapter-errors-test.js b/tests/unit/adapter-errors-test.js index f3fc8c25dcd..a81950dc419 100644 --- a/tests/unit/adapter-errors-test.js +++ b/tests/unit/adapter-errors-test.js @@ -1,5 +1,6 @@ import Ember from 'ember'; +import isEnabled from "ember-data/-private/features"; import testInDebug from 'dummy/tests/helpers/test-in-debug'; import {module, test} from 'qunit'; @@ -12,6 +13,7 @@ test("DS.AdapterError", function(assert) { assert.ok(error instanceof Error); assert.ok(error instanceof Ember.Error); assert.ok(error.isAdapterError); + assert.equal(error.message, 'Adapter operation failed'); }); test("DS.InvalidError", function(assert) { @@ -19,6 +21,7 @@ test("DS.InvalidError", function(assert) { assert.ok(error instanceof Error); assert.ok(error instanceof DS.AdapterError); assert.ok(error.isAdapterError); + assert.equal(error.message, 'The adapter rejected the commit because it was invalid'); }); test("DS.TimeoutError", function(assert) { @@ -26,6 +29,7 @@ test("DS.TimeoutError", function(assert) { assert.ok(error instanceof Error); assert.ok(error instanceof DS.AdapterError); assert.ok(error.isAdapterError); + assert.equal(error.message, 'The adapter operation timed out'); }); test("DS.AbortError", function(assert) { @@ -33,8 +37,58 @@ test("DS.AbortError", function(assert) { assert.ok(error instanceof Error); assert.ok(error instanceof DS.AdapterError); assert.ok(error.isAdapterError); + assert.equal(error.message, 'The adapter operation was aborted'); }); +if (isEnabled('ds-extended-errors')) { + test("DS.UnauthorizedError", function(assert) { + var error = new DS.UnauthorizedError(); + assert.ok(error instanceof Error); + assert.ok(error instanceof DS.AdapterError); + assert.ok(error.isAdapterError); + assert.equal(error.message, 'The adapter operation is unauthorized'); + }); + + test("DS.ForbiddenError", function(assert) { + var error = new DS.ForbiddenError(); + assert.ok(error instanceof Error); + assert.ok(error instanceof DS.AdapterError); + assert.ok(error.isAdapterError); + assert.equal(error.message, 'The adapter operation is forbidden'); + }); + + test("DS.NotFoundError", function(assert) { + var error = new DS.NotFoundError(); + assert.ok(error instanceof Error); + assert.ok(error instanceof DS.AdapterError); + assert.ok(error.isAdapterError); + assert.equal(error.message, 'The adapter could not find the resource'); + }); + + test("DS.ConflictError", function(assert) { + var error = new DS.ConflictError(); + assert.ok(error instanceof Error); + assert.ok(error instanceof DS.AdapterError); + assert.ok(error.isAdapterError); + assert.equal(error.message, 'The adapter operation failed due to a conflict'); + }); + + test("CustomAdapterError", function(assert) { + var CustomAdapterError = DS.AdapterError.extend(); + var error = new CustomAdapterError(); + assert.ok(error instanceof Error); + assert.ok(error instanceof DS.AdapterError); + assert.ok(error.isAdapterError); + assert.equal(error.message, 'Adapter operation failed'); + }); + + test("CustomAdapterError with default message", function(assert) { + var CustomAdapterError = DS.AdapterError.extend({ message: 'custom error!' }); + var error = new CustomAdapterError(); + assert.equal(error.message, 'custom error!'); + }); +} + var errorsHash = { name: ['is invalid', 'must be a string'], age: ['must be a number']