Add data module for handling JSONApi responses

This commit is contained in:
Kimmo Puputti 2017-02-21 20:58:17 +02:00
parent 3d4c7834e2
commit 37761e97b2
2 changed files with 219 additions and 0 deletions

54
src/util/data.js Normal file
View file

@ -0,0 +1,54 @@
/**
* Merge the given relationships objects
*
* See: http://jsonapi.org/format/#document-resource-object-relationships
*/
export const mergeRelationships = (rels1, rels2) => {
if (!rels1 && !rels2) {
// Special case to avoid adding an empty relationships object when
// none of the resource objects had any relationships.
return null;
}
return { ...rels1, ...rels2 };
};
/**
* Merge the given resource objects
*
* See: http://jsonapi.org/format/#document-resource-objects
*/
export const mergeResourceObjects = (res1, res2) => {
const { id, type } = res1;
if (res2.id.uuid !== id.uuid || res2.type !== type) {
throw new Error('Cannot merge resource objects with different ids or types');
}
const attributes = res2.attributes || res1.attributes;
const attrs = attributes ? { attributes } : null;
const relationships = mergeRelationships(res1.relationships, res2.relationships);
const rels = relationships ? { relationships } : null;
return { id, type, ...attrs, ...rels };
};
/**
* Merge the resource objects form the given api response to the
* existing entities.
*/
export const mergeEntities = (oldEntities, apiResponse) => {
const { data, included = [] } = apiResponse;
const objects = (Array.isArray(data) ? data : [data]).concat(included);
/* eslint-disable no-param-reassign */
const newEntities = objects.reduce(
(entities, curr) => {
const { id, type } = curr;
entities[type] = entities[type] || {};
const entity = entities[type][id.uuid];
entities[type][id.uuid] = entity ? mergeResourceObjects(entity, curr) : curr;
return entities;
},
oldEntities,
);
/* eslint-enable no-param-reassign */
return newEntities;
};

165
src/util/data.test.js Normal file
View file

@ -0,0 +1,165 @@
import { types } from 'sharetribe-sdk';
import { mergeRelationships, mergeResourceObjects, mergeEntities } from './data';
const { UUID } = types;
describe('data utils', () => {
describe('mergeRelationships()', () => {
it('handles no rels', () => {
expect(mergeRelationships(undefined, undefined)).toEqual(null);
});
it('takes existings rels if no new rels are given', () => {
const listingRels = {
author: { data: { id: new UUID('author-id'), type: 'author' } },
images: {
data: [
{ id: new UUID('image1'), type: 'image' },
{ id: new UUID('image2'), type: 'image' },
],
},
};
expect(mergeRelationships(listingRels, undefined)).toEqual(listingRels);
});
it('takes new rels if no existing rels are given', () => {
const listingRels = {
author: { data: { id: new UUID('author-id'), type: 'author' } },
images: { data: { id: new UUID('image1'), type: 'image' } },
};
expect(mergeRelationships(undefined, listingRels)).toEqual(listingRels);
});
it('merges two nonempty rels', () => {
const authorRel = { data: { id: new UUID('author-id'), type: 'author' } };
const imagesRel = {
data: [
{ id: new UUID('image1'), type: 'image' },
{ id: new UUID('image2'), type: 'image' },
],
};
const newImagesRel = { data: { id: new UUID('image1'), type: 'image' } };
const reviewsRel = {
data: [
{ id: new UUID('review1'), type: 'review' },
{ id: new UUID('review2'), type: 'review' },
],
};
const oldListingRels = { author: authorRel, images: imagesRel };
const newListingRels = { images: newImagesRel, reviews: reviewsRel };
expect(mergeRelationships(oldListingRels, newListingRels)).toEqual({
author: authorRel,
images: newImagesRel,
reviews: reviewsRel,
});
});
});
describe('mergeResourceObjects()', () => {
it("throws if ids don't match", () => {
const listing1 = { id: new UUID('listing1'), type: 'listing' };
const listing2 = { id: new UUID('listing2'), type: 'listing' };
expect(() => mergeResourceObjects(listing1, listing2)).toThrow(
'Cannot merge resource objects with different ids or types',
);
});
it("throws if types don't match", () => {
const listing1 = { id: new UUID('listing1'), type: 'listing' };
const author1 = { id: new UUID('author1'), type: 'author' };
expect(() => mergeResourceObjects(listing1, author1)).toThrow(
'Cannot merge resource objects with different ids or types',
);
});
it("doesn't add attributes or relationships keys unnecessarily", () => {
const res1 = { id: new UUID('listing1'), type: 'listing' };
const res2 = { id: new UUID('listing1'), type: 'listing' };
const merged = mergeResourceObjects(res1, res2);
expect(merged).toEqual(res1);
const keys = Object.keys(merged).sort();
expect(keys).toEqual(['id', 'type']);
});
it('merges new attributes', () => {
const res1 = {
id: new UUID('listing1'),
type: 'listing',
attributes: { title: 'Listing 1 title', description: 'Listing 1 description' },
};
const res2 = {
id: new UUID('listing1'),
type: 'listing',
attributes: { title: 'Changed title', description: 'Some description' },
};
expect(mergeResourceObjects(res1, res2)).toEqual(res2);
});
it("keeps old attributes if new does't have any", () => {
const res1Attributes = { title: 'Listing 1 title', description: 'Listing 1 description' };
const res2Relationships = { author: { data: { id: new UUID('author1'), type: 'author' } } };
const res1 = { id: new UUID('listing1'), type: 'listing', attributes: res1Attributes };
const res2 = { id: new UUID('listing1'), type: 'listing', relationships: res2Relationships };
expect(mergeResourceObjects(res1, res2)).toEqual({
id: new UUID('listing1'),
type: 'listing',
attributes: res1Attributes,
relationships: res2Relationships,
});
});
});
describe('mergeEntities()', () => {
it('adds a single entity', () => {
const listing1 = { id: new UUID('listing1'), type: 'listing' };
const response = { data: listing1 };
const entities = mergeEntities({}, response);
expect(entities).toEqual({ listing: { listing1 } });
});
it('add multiple entities', () => {
const listing1 = { id: new UUID('listing1'), type: 'listing' };
const listing2 = { id: new UUID('listing2'), type: 'listing' };
const response = { data: [listing1, listing2] };
const entities = mergeEntities({}, response);
expect(entities).toEqual({ listing: { listing1, listing2 } });
});
it('handles a more complex merge', () => {
const listing1 = {
id: new UUID('listing1'),
type: 'listing',
attributes: { title: 'Listing 1 title', description: 'Listing 1 description' },
};
const listing2 = {
id: new UUID('listing2'),
type: 'listing',
attributes: { title: 'Listing 2 title', description: 'Listing 2 description' },
};
const initialResponse = { data: [listing1, listing2] };
const initialEntities = mergeEntities({}, initialResponse);
expect(initialEntities).toEqual({ listing: { listing1, listing2 } });
const author1 = { id: new UUID('author1'), type: 'author' };
const author1WithAttributes = {
id: new UUID('author1'),
type: 'author',
attributes: { name: 'Author 1 name' },
};
const listing2Updated = {
id: new UUID('listing2'),
type: 'listing',
attributes: { title: 'New listing 2 title', description: 'new listing 2 description' },
relationships: { author: { data: author1 } },
};
const included = [author1WithAttributes];
const response = { data: [listing2Updated], included };
const entities = mergeEntities(initialEntities, response, true);
expect(entities).toEqual({
listing: { listing1, listing2: listing2Updated },
author: { author1: author1WithAttributes },
});
});
});
});