diff --git a/client/package.json b/client/package.json index a07d02b..b6a3f5c 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,7 @@ "dependencies": { "axios": "^0.16.1", "babel-polyfill": "^6.23.0", + "jsonapi-serializer": "^3.5.2", "lodash": "^4.17.4", "object-path-immutable": "^0.5.1", "react": "^15.4.2", diff --git a/client/src/components/Categories/CategoryEdit.js b/client/src/components/Categories/CategoryEdit.js index 525cd5d..d3712f9 100644 --- a/client/src/components/Categories/CategoryEdit.js +++ b/client/src/components/Categories/CategoryEdit.js @@ -23,7 +23,7 @@ export class CategoryEdit extends Component { Back to Categories

-

{ isNew ? 'New Category' : category.attributes.name }

+

{ isNew ? 'New Category' : category.name }

{ JSON.stringify(category, null, 2) }
diff --git a/client/src/components/Categories/CategoryList.js b/client/src/components/Categories/CategoryList.js index 97a56d4..b8a6ba0 100644 --- a/client/src/components/Categories/CategoryList.js +++ b/client/src/components/Categories/CategoryList.js @@ -17,7 +17,7 @@ export class CategoryList extends Component {
{categories.data.map(category =>
- {category.attributes.name} + {category.name}
)}
diff --git a/client/src/components/Posts/PostEdit.js b/client/src/components/Posts/PostEdit.js index 6ce9e4c..99c2af4 100644 --- a/client/src/components/Posts/PostEdit.js +++ b/client/src/components/Posts/PostEdit.js @@ -28,27 +28,11 @@ export class PostEdit extends Component { onSubmit = (values) => { const { params, post, categories, createResource, updateResource, redirectToIndex } = this.props; - const relationships = { - category: { - data: { - type: 'categories', - } - }, - }; - - values.relationships.category.data.type = 'categories'; - // TODO find better way to manage new/edit payload - const payload = omit({ + const payload = { id: post.id, - relationships, ...values, - type: 'posts', - }, - 'links', - 'relationships.category.links', - 'relationships.comments' - ); + }; if (!params.id) { createResource(payload).then(redirectToIndex); @@ -72,7 +56,7 @@ export class PostEdit extends Component { Back to Posts

-

{ isNew ? 'New Post' : post.attributes.title }

+

{ isNew ? 'New Post' : post.title }

{ !isNew &&

diff --git a/client/src/components/Posts/PostForm.js b/client/src/components/Posts/PostForm.js index 83bf632..b33098b 100644 --- a/client/src/components/Posts/PostForm.js +++ b/client/src/components/Posts/PostForm.js @@ -10,20 +10,20 @@ class PostForm extends Component { const categoriesOptions = categories.map(category => ({ id: category.id, - name: category.attributes.name, + name: category.name, })); return (

- +
-
- +
@@ -36,9 +36,9 @@ class PostForm extends Component { const validate = values => { const errors = required(values, - 'attributes.title', - 'relationships.category.data.id', - 'attributes.body' + 'title', + 'category.id', + 'body' ); return errors; }; diff --git a/client/src/components/Posts/PostList.js b/client/src/components/Posts/PostList.js index 1c49560..722886a 100644 --- a/client/src/components/Posts/PostList.js +++ b/client/src/components/Posts/PostList.js @@ -13,8 +13,8 @@ export class PostList extends Component { } getCategoryForPost(post) { - const categoryId = get(post, 'relationships.category.data.id'); - return this.props.categoriesById[categoryId] || { attributes: {} }; + const categoryId = String(get(post, 'category.id')); + return this.props.categoriesById[categoryId] || {}; } fetchPage = (url) => (e) => { @@ -24,10 +24,10 @@ export class PostList extends Component { onFilter = (filter) => { this.props.fetchPosts(filter); - } + }; render() { - const { posts, categoriesById, categories } = this.props; + const { posts, categories } = this.props; const { prev, next } = posts.links; return ( @@ -38,8 +38,8 @@ export class PostList extends Component { {posts.data.map(post =>
- {post.attributes.title} - ({this.getCategoryForPost(post).attributes.name}) + {post.title} + ({this.getCategoryForPost(post).name})
)}

diff --git a/client/src/components/Posts/PostListFilter.js b/client/src/components/Posts/PostListFilter.js index 7ce2b66..86188c5 100644 --- a/client/src/components/Posts/PostListFilter.js +++ b/client/src/components/Posts/PostListFilter.js @@ -9,9 +9,13 @@ class PostListFilter extends Component { const categoriesOptions = categories.map(category => ({ id: category.id, - name: category.attributes.name, + name: category.name, })); - categoriesOptions.unshift({ id: "", name: 'All categories'}) + + categoriesOptions.unshift({ + id: '', + name: 'All categories' + }); return ( diff --git a/client/src/store/api/__snapshots__/normalize.spec.js.snap b/client/src/store/api/__snapshots__/normalize.spec.js.snap new file mode 100644 index 0000000..3c59759 --- /dev/null +++ b/client/src/store/api/__snapshots__/normalize.spec.js.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`normalize categories denormalize 1`] = ` +Object { + "data": Object { + "attributes": Object { + "name": "Category 11", + }, + "id": "11", + "type": "categories", + }, +} +`; + +exports[`normalize categories normalize 1`] = ` +Object { + "id": "11", + "name": "Category 11", +} +`; + +exports[`normalize posts denormalize 1`] = ` +Object { + "data": Object { + "attributes": Object { + "body": "Body 1", + "title": "Title 1", + }, + "id": "1", + "relationships": Object { + "category": Object { + "data": Object { + "id": "11", + "type": "categories", + }, + }, + }, + "type": "posts", + }, +} +`; + +exports[`normalize posts normalize 1`] = ` +Object { + "body": "Body 1", + "category": Object { + "id": "11", + "name": undefined, + }, + "id": "1", + "title": "Title 1", +} +`; diff --git a/client/src/store/api/client.js b/client/src/store/api/client.js index 7f0e00f..9a2ece1 100644 --- a/client/src/store/api/client.js +++ b/client/src/store/api/client.js @@ -1,6 +1,8 @@ import qs from 'qs'; import axios from 'axios'; -import { isEmpty, toArray } from 'lodash'; +import { get, isEmpty, toArray } from 'lodash'; + +import { denormalize, normalize, normalizeEach } from './normalize'; export const GET_ONE = 'GET_ONE'; export const GET_LIST = 'GET_LIST'; @@ -26,6 +28,11 @@ export default (request, payload, meta) => { url = `${meta.key}`, } = meta; + const normalizeResponse = response => Promise.all([ + normalize(meta.key, get(response, 'data')), + normalizeEach(get(response, 'data.included')), + ]).then(([data, included]) => ({...response.data, data, included})); + const params = payload; switch(request) { @@ -33,14 +40,15 @@ export default (request, payload, meta) => { return client({ url: withParams(url), method: 'POST', - data: { data: payload }, + data: denormalize(meta.key, payload), }).then(response => response.data); - case UPDATE: + case UPDATE: { return client({ url: withParams(`${url}/${payload.id}`), method: 'PUT', - data: { data: payload }, + data: denormalize(meta.key, payload), }).then(response => response.data); + } case DELETE: return client({ url: withParams(`${url}/${payload.id}`), @@ -51,12 +59,12 @@ export default (request, payload, meta) => { url: withParams(`${url}/${payload.id}`, params), method: 'GET', data: JSON.stringify(payload), - }).then(response => response.data); + }).then(normalizeResponse); default: return client({ url: withParams(`${url}`, params), method: 'GET', data: JSON.stringify(payload), - }).then(response => response.data); + }).then(normalizeResponse); } }; diff --git a/client/src/store/api/normalize.js b/client/src/store/api/normalize.js new file mode 100644 index 0000000..b162b99 --- /dev/null +++ b/client/src/store/api/normalize.js @@ -0,0 +1,45 @@ +import { omit } from 'lodash'; +import { Deserializer, Serializer } from 'jsonapi-serializer'; + +const serializers = { + categories: { + serializer: new Serializer('categories', { + attributes: [ + 'name', + ], + }), + deserializer: new Deserializer({}), + }, + + posts: { + serializer: new Serializer('posts', { + attributes: [ + 'title', + 'body', + 'category', + ], + category: { + ref: 'id', + included: false, + attributes: ['name'], + } + }), + deserializer: new Deserializer({ + categories: { + valueForRelationship: (relationship) => ({ + id: relationship.id, + name: relationship.name, + }), + } + }), + }, +}; + +export const normalize = (type, data) => serializers[type].deserializer.deserialize(data); + +export const normalizeEach = (items = []) => items.map(item => serializers[item.type].deserializer.deserialize({data: item})); + +export const denormalize = (type, data) => { + const res = serializers[type].serializer.serialize(data); + return data.id ? res : omit(res, 'data.id'); +}; diff --git a/client/src/store/api/normalize.spec.js b/client/src/store/api/normalize.spec.js new file mode 100644 index 0000000..8fe681f --- /dev/null +++ b/client/src/store/api/normalize.spec.js @@ -0,0 +1,31 @@ +import { denormalize, normalize } from './normalize'; + +const testNormalize = (type, values) => { + describe(type, () => { + it('denormalize', () => { + expect(denormalize(type, values)).toMatchSnapshot(); + }); + + it('normalize', async () => { + return (normalize(type, denormalize(type, values))) + .then(res => expect(res).toMatchSnapshot()); + }); + }); +}; + +describe('normalize', () => { + testNormalize('categories', { + id: 11, + name: 'Category 11', + }); + + testNormalize('posts', { + id: 1, + title: 'Title 1', + body: 'Body 1', + category: { + id: 11, + name: 'Category 11', + }, + }); +});