Skip to content

Commit

Permalink
fix: only apply extractErrors to JSONAPI serializers
Browse files Browse the repository at this point in the history
  • Loading branch information
Gilles MORIANI committed Jun 6, 2024
1 parent 3cf8e39 commit 8a5360d
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 267 deletions.
139 changes: 139 additions & 0 deletions packages/serializer/src/json-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { assert } from '@warp-drive/build-config/macros';

import JSONSerializer from './json';

const SOURCE_POINTER_REGEXP = /^\/?data\/(attributes|relationships)\/(.*)/;
const SOURCE_POINTER_PRIMARY_REGEXP = /^\/?data/;
const PRIMARY_ATTRIBUTE_KEY = 'base';

/**
* <blockquote style="margin: 1em; padding: .1em 1em .1em 1em; border-left: solid 1em #E34C32; background: #e0e0e0;">
<p>
Expand Down Expand Up @@ -336,6 +340,141 @@ const JSONAPISerializer = JSONSerializer.extend({
return relationships;
},

/**
`extractErrors` is used to extract model errors when a call
to `Model#save` fails with an `InvalidError`. By default
Ember Data expects error information to be located on the `errors`
property of the payload object.
This serializer expects this `errors` object to be an Array similar
to the following, compliant with the https://jsonapi.org/format/#errors specification:
```js
{
"errors": [
{
"detail": "This username is already taken!",
"source": {
"pointer": "data/attributes/username"
}
}, {
"detail": "Doesn't look like a valid email.",
"source": {
"pointer": "data/attributes/email"
}
}
]
}
```
The key `detail` provides a textual description of the problem.
Alternatively, the key `title` can be used for the same purpose.
The nested keys `source.pointer` detail which specific element
of the request data was invalid.
Note that JSON-API also allows for object-level errors to be placed
in an object with pointer `data`, signifying that the problem
cannot be traced to a specific attribute:
```javascript
{
"errors": [
{
"detail": "Some generic non property error message",
"source": {
"pointer": "data"
}
}
]
}
```
When turn into a `Errors` object, you can read these errors
through the property `base`:
```handlebars
{{#each @model.errors.base as |error|}}
<div class="error">
{{error.message}}
</div>
{{/each}}
```
Example of alternative implementation, overriding the default
behavior to deal with a different format of errors:
```app/serializers/post.js
import JSONSerializer from '@ember-data/serializer/json';
export default class PostSerializer extends JSONSerializer {
extractErrors(store, typeClass, payload, id) {
if (payload && typeof payload === 'object' && payload._problems) {
payload = payload._problems;
this.normalizeErrors(typeClass, payload);
}
return payload;
}
}
```
@method extractErrors
@public
@param {Store} store
@param {Model} typeClass
@param {Object} payload
@param {(String|Number)} id
@return {Object} json The deserialized errors
*/
extractErrors(store, typeClass, payload, id) {
if (payload && typeof payload === 'object' && payload.errors) {
// the default assumption is that errors is already in JSON:API format
const extracted = {};

payload.errors.forEach((error) => {
if (error.source && error.source.pointer) {
let key = error.source.pointer.match(SOURCE_POINTER_REGEXP);

if (key) {
key = key[2];
} else if (error.source.pointer.search(SOURCE_POINTER_PRIMARY_REGEXP) !== -1) {
key = PRIMARY_ATTRIBUTE_KEY;
}

if (key) {
extracted[key] = extracted[key] || [];
extracted[key].push(error.detail || error.title);
}
}
});

// if the user has an attrs hash, convert keys using it
this.normalizeUsingDeclaredMapping(typeClass, extracted);

// for each attr and relationship, make sure that we use
// the normalized key
typeClass.eachAttribute((name) => {
const key = this.keyForAttribute(name, 'deserialize');
if (key !== name && extracted[key] !== undefined) {
extracted[name] = extracted[key];
delete extracted[key];
}
});

typeClass.eachRelationship((name) => {
const key = this.keyForRelationship(name, 'deserialize');
if (key !== name && extracted[key] !== undefined) {
extracted[name] = extracted[key];
delete extracted[key];
}
});

return extracted;
}

return payload;
},

/**
@method _extractType
@param {Model} modelClass
Expand Down
139 changes: 0 additions & 139 deletions packages/serializer/src/json.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ import { assert } from '@warp-drive/build-config/macros';
import Serializer from '.';
import { coerceId } from './-private/utils';

const SOURCE_POINTER_REGEXP = /^\/?data\/(attributes|relationships)\/(.*)/;
const SOURCE_POINTER_PRIMARY_REGEXP = /^\/?data/;
const PRIMARY_ATTRIBUTE_KEY = 'base';

/**
* <blockquote style="margin: 1em; padding: .1em 1em .1em 1em; border-left: solid 1em #E34C32; background: #e0e0e0;">
<p>
Expand Down Expand Up @@ -1389,141 +1385,6 @@ const JSONSerializer = Serializer.extend({
}
},

/**
`extractErrors` is used to extract model errors when a call
to `Model#save` fails with an `InvalidError`. By default
Ember Data expects error information to be located on the `errors`
property of the payload object.
This serializer expects this `errors` object to be an Array similar
to the following, compliant with the https://jsonapi.org/format/#errors specification:
```js
{
"errors": [
{
"detail": "This username is already taken!",
"source": {
"pointer": "data/attributes/username"
}
}, {
"detail": "Doesn't look like a valid email.",
"source": {
"pointer": "data/attributes/email"
}
}
]
}
```
The key `detail` provides a textual description of the problem.
Alternatively, the key `title` can be used for the same purpose.
The nested keys `source.pointer` detail which specific element
of the request data was invalid.
Note that JSON-API also allows for object-level errors to be placed
in an object with pointer `data`, signifying that the problem
cannot be traced to a specific attribute:
```javascript
{
"errors": [
{
"detail": "Some generic non property error message",
"source": {
"pointer": "data"
}
}
]
}
```
When turn into a `Errors` object, you can read these errors
through the property `base`:
```handlebars
{{#each @model.errors.base as |error|}}
<div class="error">
{{error.message}}
</div>
{{/each}}
```
Example of alternative implementation, overriding the default
behavior to deal with a different format of errors:
```app/serializers/post.js
import JSONSerializer from '@ember-data/serializer/json';
export default class PostSerializer extends JSONSerializer {
extractErrors(store, typeClass, payload, id) {
if (payload && typeof payload === 'object' && payload._problems) {
payload = payload._problems;
this.normalizeErrors(typeClass, payload);
}
return payload;
}
}
```
@method extractErrors
@public
@param {Store} store
@param {Model} typeClass
@param {Object} payload
@param {(String|Number)} id
@return {Object} json The deserialized errors
*/
extractErrors(store, typeClass, payload, id) {
if (payload && typeof payload === 'object' && payload.errors) {
// the default assumption is that errors is already in JSON:API format
const extracted = {};

payload.errors.forEach((error) => {
if (error.source && error.source.pointer) {
let key = error.source.pointer.match(SOURCE_POINTER_REGEXP);

if (key) {
key = key[2];
} else if (error.source.pointer.search(SOURCE_POINTER_PRIMARY_REGEXP) !== -1) {
key = PRIMARY_ATTRIBUTE_KEY;
}

if (key) {
extracted[key] = extracted[key] || [];
extracted[key].push(error.detail || error.title);
}
}
});

// if the user has an attrs hash, convert keys using it
this.normalizeUsingDeclaredMapping(typeClass, extracted);

// for each attr and relationship, make sure that we use
// the normalized key
typeClass.eachAttribute((name) => {
const key = this.keyForAttribute(name, 'deserialize');
if (key !== name && extracted[key] !== undefined) {
extracted[name] = extracted[key];
delete extracted[key];
}
});

typeClass.eachRelationship((name) => {
const key = this.keyForRelationship(name, 'deserialize');
if (key !== name && extracted[key] !== undefined) {
extracted[name] = extracted[key];
delete extracted[key];
}
});

return extracted;
}

return payload;
},

/**
`keyForAttribute` can be used to define rules for how to convert an
attribute name in your model to a key in your JSON.
Expand Down
Loading

0 comments on commit 8a5360d

Please sign in to comment.