Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE ds-extended-errors] Make adapter error extendable and add more error types #3586

Merged
merged 1 commit into from
Mar 29, 2016
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
15 changes: 15 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
75 changes: 60 additions & 15 deletions addon/adapters/errors.js
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions addon/adapters/rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import Adapter from "ember-data/adapter";
import {
AdapterError,
InvalidError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
ConflictError,
TimeoutError,
AbortError
} from 'ember-data/adapters/errors';
Expand Down Expand Up @@ -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);
},

Expand Down
12 changes: 12 additions & 0 deletions addon/index.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -40,6 +41,10 @@ import DebugAdapter from "ember-data/-private/system/debug";
import {
AdapterError,
InvalidError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
ConflictError,
TimeoutError,
AbortError,
errorsHashToArray,
Expand Down Expand Up @@ -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;

Expand Down
3 changes: 2 additions & 1 deletion config/features.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
60 changes: 60 additions & 0 deletions tests/integration/adapter/rest-adapter-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/adapter-errors-test.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -12,29 +13,82 @@ 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) {
var error = new DS.InvalidError();
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) {
var error = new DS.TimeoutError();
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) {
var error = new DS.AbortError();
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']
Expand Down