From d2e55b19a1b2c4bbd70f335730fbe5f87c265065 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Wed, 3 Jun 2020 17:58:24 -0400 Subject: [PATCH] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20error=20handling?= =?UTF-8?q?=20support=20to=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/api/addon/adapters/application.js | 85 +++++++++++++++++++ .../tests/unit/adapters/application-test.js | 41 +++++++++ 2 files changed, 126 insertions(+) diff --git a/addons/api/addon/adapters/application.js b/addons/api/addon/adapters/application.js index 71c109cb0..389212374 100644 --- a/addons/api/addon/adapters/application.js +++ b/addons/api/addon/adapters/application.js @@ -1,6 +1,7 @@ import RESTAdapter from '@ember-data/adapter/rest'; import config from 'ember-get-config'; import { get } from '@ember/object'; +import { InvalidError } from '@ember-data/adapter/error'; export default class ApplicationAdapter extends RESTAdapter { // =attributes @@ -37,4 +38,88 @@ export default class ApplicationAdapter extends RESTAdapter { const method = type === 'PUT' ? 'PATCH' : type; return super.ajax(url, method, options); } + + /** + * Our API represents invalid requests via the 400 error. But Ember Data + * expects 422 by default. We change it here. + * + * @override + * @method isInvalid + * @param {Number} status + * @param {Object} headers + * @param {Object} payload + * @return {Boolean} + */ + isInvalid(status /*, headers, payload*/) { + return status === 400; + } + + /** + * Ember Data blatantly assumes errors always present under an `errors` key + * in the payload, even _before normalizing errors_. Bizarre. Instead, + * we pass the payload through an adapter-level transformation step if the + * request was invalid. Then we can rely on the serializer's `extractErrors` + * to handle the rest. + * + * @override + * @method handleResponse + * @param {Number} status + * @param {Object} headers + * @param {Object} payload + * @param {Object} requestData - the original request information + * @return {Object | AdapterError} response + */ + handleResponse(status, headers, payload, requestData) { + if (this.isInvalid(status, headers, payload)) { + const detailedMessage = this.generatedDetailedMessage(...arguments); + const transformedPayload = this.transformValidationErrors(payload); + return new InvalidError(transformedPayload.errors, detailedMessage); + } + return super.handleResponse(status, headers, payload, requestData); + } + + /** + * Normalizes the errors found in a payload from our API into JSON API + * error format. + * + * @param {object} payload + * @return {object} + */ + transformValidationErrors(payload) { + const fieldErrors = get(payload, 'details.fields') || []; + // Normalize the primary error message into "base", which is an error + // that applies to the whole model instance + const baseError = { + status: payload.source, + code: payload.code, + detail: payload.message, + source: { pointer: '/data' }, + }; + // Normalize field-specific errors, if any. + const errors = fieldErrors.map((error) => ({ + detail: error.message, + source: { pointer: `/data/attributes/${error.name}` }, + })); + // Return a list of JSON API errors rooted under the `errors` key. + return { + errors: [baseError, ...errors], + }; + } + + /** + * Returns `payload.message`, which our API provides as a top-level + * description of the error that occurred. + * + * @override + * @method generatedDetailedMessage + * @private + * @param {Number} status + * @param {Object} headers + * @param {Object} payload + * @param {Object} requestData + * @return {String} detailed error message + */ + generatedDetailedMessage(status, headers, payload /*, requestData*/) { + return payload.message; + } } diff --git a/addons/api/tests/unit/adapters/application-test.js b/addons/api/tests/unit/adapters/application-test.js index 5d07e1ccf..8464a7513 100644 --- a/addons/api/tests/unit/adapters/application-test.js +++ b/addons/api/tests/unit/adapters/application-test.js @@ -2,6 +2,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import config from 'ember-get-config'; import RESTAdapter from '@ember-data/adapter/rest'; +import { InvalidError } from '@ember-data/adapter/error'; module('Unit | Adapter | application', function (hooks) { setupTest(hooks); @@ -28,4 +29,44 @@ module('Unit | Adapter | application', function (hooks) { }; adapter.ajax('/', 'PUT'); }); + + test('it correctly identifies 400 responses as invalid', function (assert) { + assert.expect(2); + const adapter = this.owner.lookup('adapter:application'); + assert.ok(adapter.isInvalid(400)); + assert.notOk(adapter.isInvalid(401)); + }); + + test('it returns an proper InvalidError from handleResponse', function (assert) { + assert.expect(3); + const adapter = this.owner.lookup('adapter:application'); + let payload = { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid.', + }; + let handledResponse = adapter.handleResponse(400, {}, payload); + assert.ok(handledResponse instanceof InvalidError); + assert.equal(handledResponse.errors.length, 1); + assert.equal(handledResponse.message, 'The request was invalid.'); + }); + + test('it returns field-level errors in InvalidError from handleResponse', function (assert) { + assert.expect(1); + const adapter = this.owner.lookup('adapter:application'); + let payload = { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid.', + details: { + fields: [{ name: 'name', message: 'Name is wrong.' }], + }, + }; + let handledResponse = adapter.handleResponse(400, {}, payload); + assert.equal( + handledResponse.errors.length, + 2, + 'A base error plus one field error' + ); + }); });