Skip to content

Commit

Permalink
add server errors to models (#65)
Browse files Browse the repository at this point in the history
Adds server errors directly to their corresponding attributes in models.

Previously we had made the decision to lump all server errors onto a "server" attribute so that they would at least be accessible. Now, when errors come back from the API we parse the source pointer for each error to determine which attribute the error is for and add it in the same manner as our model validations.

This also support errors for create/update requests using the JSONApi bulk extension. When these errors are returned, the pointer will contain an index that corresponds to the payload of the original request which is used to determine which object in the store the errors should be assigned to.
  • Loading branch information
jackie kircher authored Sep 4, 2020
1 parent 9a58d4e commit ff579bf
Show file tree
Hide file tree
Showing 19 changed files with 420 additions and 175 deletions.
114 changes: 76 additions & 38 deletions dist/mobx-async-store.cjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ var _applyDecoratedDescriptor = _interopDefault(require('@babel/runtime/helpers/
require('@babel/runtime/helpers/initializerWarningHelper');
var _typeof = _interopDefault(require('@babel/runtime/helpers/typeof'));
var mobx = require('mobx');
var _inherits = _interopDefault(require('@babel/runtime/helpers/inherits'));
var _possibleConstructorReturn = _interopDefault(require('@babel/runtime/helpers/possibleConstructorReturn'));
var _getPrototypeOf = _interopDefault(require('@babel/runtime/helpers/getPrototypeOf'));
var _wrapNativeSuper = _interopDefault(require('@babel/runtime/helpers/wrapNativeSuper'));
var uuidv1 = _interopDefault(require('uuid/v1'));
var qs = _interopDefault(require('qs'));
var pluralize = _interopDefault(require('pluralize'));
Expand All @@ -24,11 +28,7 @@ var cloneDeep = _interopDefault(require('lodash/cloneDeep'));
var _isEqual = _interopDefault(require('lodash/isEqual'));
var isObject = _interopDefault(require('lodash/isObject'));
var findLast = _interopDefault(require('lodash/findLast'));
var _possibleConstructorReturn = _interopDefault(require('@babel/runtime/helpers/possibleConstructorReturn'));
var _getPrototypeOf = _interopDefault(require('@babel/runtime/helpers/getPrototypeOf'));
var _assertThisInitialized = _interopDefault(require('@babel/runtime/helpers/assertThisInitialized'));
var _inherits = _interopDefault(require('@babel/runtime/helpers/inherits'));
var _wrapNativeSuper = _interopDefault(require('@babel/runtime/helpers/wrapNativeSuper'));

var QueryString = {
parse: function parse(str) {
Expand All @@ -43,6 +43,7 @@ var QueryString = {
}
};

function _wrapRegExp(re, groups) { _wrapRegExp = function _wrapRegExp(re, groups) { return new BabelRegExp(re, undefined, groups); }; var _RegExp = _wrapNativeSuper(RegExp); var _super = RegExp.prototype; var _groups = new WeakMap(); function BabelRegExp(re, flags, groups) { var _this = _RegExp.call(this, re, flags); _groups.set(_this, groups || _groups.get(re)); return _this; } _inherits(BabelRegExp, _RegExp); BabelRegExp.prototype.exec = function (str) { var result = _super.exec.call(this, str); if (result) result.groups = buildGroups(result, this); return result; }; BabelRegExp.prototype[Symbol.replace] = function (str, substitution) { if (typeof substitution === "string") { var groups = _groups.get(this); return _super[Symbol.replace].call(this, str, substitution.replace(/\$<([^>]+)>/g, function (_, name) { return "$" + groups[name]; })); } else if (typeof substitution === "function") { var _this = this; return _super[Symbol.replace].call(this, str, function () { var args = []; args.push.apply(args, arguments); if (_typeof(args[args.length - 1]) !== "object") { args.push(buildGroups(args, _this)); } return substitution.apply(this, args); }); } else { return _super[Symbol.replace].call(this, str, substitution); } }; function buildGroups(result, re) { var g = _groups.get(re); return Object.keys(g).reduce(function (groups, name) { groups[name] = result[g[name]]; return groups; }, Object.create(null)); } return _wrapRegExp.apply(this, arguments); }
var pending = {};
var counter = {};
var URL_MAX_LENGTH = 1024;
Expand Down Expand Up @@ -230,20 +231,50 @@ function diff() {
});
}
/**
* A naive way of extracting errors from the server.
* This needs some real work. Please don't track down the original author
* of the code (it's DEFINITELY not the person writing this documentation).
* Currently it only extracts the message from the first error, but not only
* can multiple errors be returned, they will correspond to different records
* in the case of a bulk JSONAPI response.
* Parses the pointer of the error to retrieve the index of the
* record the error belongs to and the full path to the attribute
* which will serve as the key for the error.
*
* If there is no parsed index, then assume the payload was for
* a single record and default to 0.
*
* ex.
* error = {
* detail: "Foo can't be blank",
* source: { pointer: '/data/1/attributes/options/foo' },
* title: 'Invalid foo'
* }
*
* @method parseApiErrors
* @param {Array} a request to the API
* @param {String} default error message
* parsePointer(error)
* > {
* index: 1,
* key: 'options.foo'
* }
*
* @method parseErrorPointer
* @param {Object} error
* @return {Object} the matching parts of the pointer
*/

function parseApiErrors(errors, defaultMessage) {
return errors[0].detail.length === 0 ? defaultMessage : errors[0].detail[0];
function parseErrorPointer() {
var error = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};

var regex = _wrapRegExp(/\/data\/([0-9]+)?\/?attributes\/(.*)$/, {
index: 1,
key: 2
});

var match = dig(error, 'source.pointer', '').match(regex);

var _ref = (match === null || match === void 0 ? void 0 : match.groups) || {},
_ref$index = _ref.index,
index = _ref$index === void 0 ? 0 : _ref$index,
key = _ref.key;

return {
index: parseInt(index),
key: key === null || key === void 0 ? void 0 : key.replace(/\//g, '.')
};
}
/**
* Splits an array of ids into a series of strings that can be used to form
Expand Down Expand Up @@ -2264,7 +2295,7 @@ function () {
var _ref3 = _asyncToGenerator(
/*#__PURE__*/
_regeneratorRuntime.mark(function _callee4(response) {
var status, json, data, included, message, _json, errorString;
var status, json, data, included, _json, errorString;

return _regeneratorRuntime.wrap(function _callee4$(_context4) {
while (1) {
Expand Down Expand Up @@ -2309,41 +2340,48 @@ function () {
recordsArray.forEach(function (record) {
record.isInFlight = false;
});
message = _this6.genericErrorMessage;
_json = {};
_context4.prev = 17;
_context4.next = 20;
_context4.prev = 16;
_context4.next = 19;
return response.json();

case 20:
case 19:
_json = _context4.sent;
message = parseApiErrors(_json.errors, message);
_context4.next = 26;
_context4.next = 25;
break;

case 24:
_context4.prev = 24;
_context4.t0 = _context4["catch"](17);

case 26:
// TODO: add all errors from the API response to the record
// also TODO: split server errors by record once the info is available from the API
recordsArray[0].errors = _objectSpread$2({}, recordsArray[0].errors, {
status: status,
base: [{
message: message
}],
server: _json.errors
case 22:
_context4.prev = 22;
_context4.t0 = _context4["catch"](16);
return _context4.abrupt("return", Promise.reject(new Error(_this6.genericErrorMessage)));

case 25:
// Add all errors from the API response to the record(s).
// This is done by comparing the pointer in the error to
// the request.
_json.errors.forEach(function (error) {
var _parseErrorPointer = parseErrorPointer(error),
index = _parseErrorPointer.index,
key = _parseErrorPointer.key;

if (key != null) {
var errors = recordsArray[index].errors[key] || [];
errors.push(error);
recordsArray[index].errors[key] = errors;
}
});
errorString = JSON.stringify(recordsArray[0].errors);

errorString = recordsArray.map(function (record) {
return JSON.stringify(record.errors);
}).join(';');
return _context4.abrupt("return", Promise.reject(new Error(errorString)));

case 29:
case 28:
case "end":
return _context4.stop();
}
}
}, _callee4, null, [[17, 24]]);
}, _callee4, null, [[16, 22]]);
}));

return function (_x9) {
Expand Down
114 changes: 76 additions & 38 deletions dist/mobx-async-store.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import _applyDecoratedDescriptor from '@babel/runtime/helpers/applyDecoratedDesc
import '@babel/runtime/helpers/initializerWarningHelper';
import _typeof from '@babel/runtime/helpers/typeof';
import { observable, computed, set, extendObservable, reaction, transaction, toJS, action } from 'mobx';
import _inherits from '@babel/runtime/helpers/inherits';
import _possibleConstructorReturn from '@babel/runtime/helpers/possibleConstructorReturn';
import _getPrototypeOf from '@babel/runtime/helpers/getPrototypeOf';
import _wrapNativeSuper from '@babel/runtime/helpers/wrapNativeSuper';
import uuidv1 from 'uuid/v1';
import qs from 'qs';
import pluralize from 'pluralize';
Expand All @@ -18,11 +22,7 @@ import cloneDeep from 'lodash/cloneDeep';
import _isEqual from 'lodash/isEqual';
import isObject from 'lodash/isObject';
import findLast from 'lodash/findLast';
import _possibleConstructorReturn from '@babel/runtime/helpers/possibleConstructorReturn';
import _getPrototypeOf from '@babel/runtime/helpers/getPrototypeOf';
import _assertThisInitialized from '@babel/runtime/helpers/assertThisInitialized';
import _inherits from '@babel/runtime/helpers/inherits';
import _wrapNativeSuper from '@babel/runtime/helpers/wrapNativeSuper';

var QueryString = {
parse: function parse(str) {
Expand All @@ -37,6 +37,7 @@ var QueryString = {
}
};

function _wrapRegExp(re, groups) { _wrapRegExp = function _wrapRegExp(re, groups) { return new BabelRegExp(re, undefined, groups); }; var _RegExp = _wrapNativeSuper(RegExp); var _super = RegExp.prototype; var _groups = new WeakMap(); function BabelRegExp(re, flags, groups) { var _this = _RegExp.call(this, re, flags); _groups.set(_this, groups || _groups.get(re)); return _this; } _inherits(BabelRegExp, _RegExp); BabelRegExp.prototype.exec = function (str) { var result = _super.exec.call(this, str); if (result) result.groups = buildGroups(result, this); return result; }; BabelRegExp.prototype[Symbol.replace] = function (str, substitution) { if (typeof substitution === "string") { var groups = _groups.get(this); return _super[Symbol.replace].call(this, str, substitution.replace(/\$<([^>]+)>/g, function (_, name) { return "$" + groups[name]; })); } else if (typeof substitution === "function") { var _this = this; return _super[Symbol.replace].call(this, str, function () { var args = []; args.push.apply(args, arguments); if (_typeof(args[args.length - 1]) !== "object") { args.push(buildGroups(args, _this)); } return substitution.apply(this, args); }); } else { return _super[Symbol.replace].call(this, str, substitution); } }; function buildGroups(result, re) { var g = _groups.get(re); return Object.keys(g).reduce(function (groups, name) { groups[name] = result[g[name]]; return groups; }, Object.create(null)); } return _wrapRegExp.apply(this, arguments); }
var pending = {};
var counter = {};
var URL_MAX_LENGTH = 1024;
Expand Down Expand Up @@ -224,20 +225,50 @@ function diff() {
});
}
/**
* A naive way of extracting errors from the server.
* This needs some real work. Please don't track down the original author
* of the code (it's DEFINITELY not the person writing this documentation).
* Currently it only extracts the message from the first error, but not only
* can multiple errors be returned, they will correspond to different records
* in the case of a bulk JSONAPI response.
* Parses the pointer of the error to retrieve the index of the
* record the error belongs to and the full path to the attribute
* which will serve as the key for the error.
*
* If there is no parsed index, then assume the payload was for
* a single record and default to 0.
*
* ex.
* error = {
* detail: "Foo can't be blank",
* source: { pointer: '/data/1/attributes/options/foo' },
* title: 'Invalid foo'
* }
*
* @method parseApiErrors
* @param {Array} a request to the API
* @param {String} default error message
* parsePointer(error)
* > {
* index: 1,
* key: 'options.foo'
* }
*
* @method parseErrorPointer
* @param {Object} error
* @return {Object} the matching parts of the pointer
*/

function parseApiErrors(errors, defaultMessage) {
return errors[0].detail.length === 0 ? defaultMessage : errors[0].detail[0];
function parseErrorPointer() {
var error = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};

var regex = _wrapRegExp(/\/data\/([0-9]+)?\/?attributes\/(.*)$/, {
index: 1,
key: 2
});

var match = dig(error, 'source.pointer', '').match(regex);

var _ref = (match === null || match === void 0 ? void 0 : match.groups) || {},
_ref$index = _ref.index,
index = _ref$index === void 0 ? 0 : _ref$index,
key = _ref.key;

return {
index: parseInt(index),
key: key === null || key === void 0 ? void 0 : key.replace(/\//g, '.')
};
}
/**
* Splits an array of ids into a series of strings that can be used to form
Expand Down Expand Up @@ -2258,7 +2289,7 @@ function () {
var _ref3 = _asyncToGenerator(
/*#__PURE__*/
_regeneratorRuntime.mark(function _callee4(response) {
var status, json, data, included, message, _json, errorString;
var status, json, data, included, _json, errorString;

return _regeneratorRuntime.wrap(function _callee4$(_context4) {
while (1) {
Expand Down Expand Up @@ -2303,41 +2334,48 @@ function () {
recordsArray.forEach(function (record) {
record.isInFlight = false;
});
message = _this6.genericErrorMessage;
_json = {};
_context4.prev = 17;
_context4.next = 20;
_context4.prev = 16;
_context4.next = 19;
return response.json();

case 20:
case 19:
_json = _context4.sent;
message = parseApiErrors(_json.errors, message);
_context4.next = 26;
_context4.next = 25;
break;

case 24:
_context4.prev = 24;
_context4.t0 = _context4["catch"](17);

case 26:
// TODO: add all errors from the API response to the record
// also TODO: split server errors by record once the info is available from the API
recordsArray[0].errors = _objectSpread$2({}, recordsArray[0].errors, {
status: status,
base: [{
message: message
}],
server: _json.errors
case 22:
_context4.prev = 22;
_context4.t0 = _context4["catch"](16);
return _context4.abrupt("return", Promise.reject(new Error(_this6.genericErrorMessage)));

case 25:
// Add all errors from the API response to the record(s).
// This is done by comparing the pointer in the error to
// the request.
_json.errors.forEach(function (error) {
var _parseErrorPointer = parseErrorPointer(error),
index = _parseErrorPointer.index,
key = _parseErrorPointer.key;

if (key != null) {
var errors = recordsArray[index].errors[key] || [];
errors.push(error);
recordsArray[index].errors[key] = errors;
}
});
errorString = JSON.stringify(recordsArray[0].errors);

errorString = recordsArray.map(function (record) {
return JSON.stringify(record.errors);
}).join(';');
return _context4.abrupt("return", Promise.reject(new Error(errorString)));

case 29:
case 28:
case "end":
return _context4.stop();
}
}
}, _callee4, null, [[17, 24]]);
}, _callee4, null, [[16, 22]]);
}));

return function (_x9) {
Expand Down
2 changes: 1 addition & 1 deletion docs/classes/Model.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<h1><img src="../assets/css/logo.png" title="mobx-async-store" width="117" height="52"></h1>
</div>
<div class="yui3-u-1-4 version">
<em>API Docs for: 2.0.1</em>
<em>API Docs for: 3.0.0</em>
</div>
</div>
<div id="bd" class="yui3-g">
Expand Down
2 changes: 1 addition & 1 deletion docs/classes/RelatedRecordsArray.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<h1><img src="../assets/css/logo.png" title="mobx-async-store" width="117" height="52"></h1>
</div>
<div class="yui3-u-1-4 version">
<em>API Docs for: 2.0.1</em>
<em>API Docs for: 3.0.0</em>
</div>
</div>
<div id="bd" class="yui3-g">
Expand Down
2 changes: 1 addition & 1 deletion docs/classes/Schema.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<h1><img src="../assets/css/logo.png" title="mobx-async-store" width="117" height="52"></h1>
</div>
<div class="yui3-u-1-4 version">
<em>API Docs for: 2.0.1</em>
<em>API Docs for: 3.0.0</em>
</div>
</div>
<div id="bd" class="yui3-g">
Expand Down
2 changes: 1 addition & 1 deletion docs/classes/Store.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<h1><img src="../assets/css/logo.png" title="mobx-async-store" width="117" height="52"></h1>
</div>
<div class="yui3-u-1-4 version">
<em>API Docs for: 2.0.1</em>
<em>API Docs for: 3.0.0</em>
</div>
</div>
<div id="bd" class="yui3-g">
Expand Down
Loading

0 comments on commit ff579bf

Please sign in to comment.