Skip to content
This repository has been archived by the owner on Aug 23, 2023. It is now read-only.

Commit

Permalink
feat(updateCollection): added
Browse files Browse the repository at this point in the history
  • Loading branch information
akcorp2003 committed Sep 8, 2020
1 parent 794f66e commit 573ef1c
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 0 deletions.
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export default connectAsync({ loadDataAsProps })(BookList);
#### `loadCollection({ resource, id, opts, forceFetch })`
#### `createResource({ resource, id, opts })`
#### `updateResource({ resource, id, opts })`
#### `updateCollection({ resource, id, opts })`
#### `destroyResource({ resource, id, opts })`
#### `patchResource({ resource, id, opts })`

Expand Down Expand Up @@ -275,6 +276,59 @@ function loadDataAsProps({ store: { dispatch } }) {
export default connectAsync({ loadDataAsProps })(Articles);
```

### Method Opts

Some REST APIs support bulk updates as opposed to individual resource updates. There are many thoughts on this matter but they generally suggest using a POST or a PUT for this type of CRUD action. For iguazu-rest, we take the opinion that by default, updating a collection should be a POST but provide the option to override the method. However, other CRUD actions' methods cannot be overriden (e.g. `loadCollection`).

```jsx
// Articles.jsx
import React, { Component, Fragment } from 'react';
import { connectAsync } from 'iguazu';
import { updateCollection } from 'iguazu-rest';

class MyUpdatingComponent extends Component {
constructor(props) {
super(props);
this.state = { articles: [{ id: '1', body: 'article 1' }, { id: '2', body: 'article 2' }] };
}

handleClick = () => {
const { updateManyArticles } = this.props;
const { articles } = this.state;
return updateManyArticles(articles)
.then((updatedArticles) => {
this.setState({ articles: updatedArticles });
});
};

render() {
const { isLoading, loadedWithErrors, myData } = this.props;
const { articles } = this.state;

return (
<Fragment>
<button type="button" onClick={this.handleClick}>Update</button>
<div>{articles.map((article) => <Article key={article.id} article={article} />)}</div>
</Fragment>
);
}
}

function mapDispatchToProps(dispatch) {
return {
updateManyArticles: (articles) => dispatch(updateCollection({
resource: 'articles',
opts: {
method: 'PUT',
body: articles,
},
})),
};
}

export default connect(null, mapDispatchToProps)(Articles);
```

## 🏆 Contributing

We welcome Your interest in the American Express Open Source Community on Github.
Expand Down
12 changes: 12 additions & 0 deletions __tests__/actions/crud.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
loadCollection,
createResource,
updateResource,
updateCollection,
destroyResource,
patchResource,
} from '../../src/actions/crud';
Expand Down Expand Up @@ -206,6 +207,17 @@ describe('CRUD actions', () => {
});
});

describe('update collection', () => {
it('should update an existing collection', async () => {
const thunk = updateCollection({ resource, id, opts });
await thunk(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith(mockFetchPromise);
expect(executeFetch).toHaveBeenCalledWith({
resource, id, opts, actionType: 'UPDATE_COLLECTION',
});
});
});

describe('destroy', () => {
it('should destroy an existing resource', async () => {
const thunk = destroyResource({ resource, id, opts });
Expand Down
86 changes: 86 additions & 0 deletions __tests__/actions/executeFetch.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,92 @@ describe('executeFetch', () => {
const data = await thunk(dispatch, getState);
expect(data).toEqual([{ id: '123', name: 'joe' }]);
});

it('should use specified REST method for UPDATE_COLLECTION in options if provided', async () => {
Object.assign(config, {
defaultOpts: { default: 'opt' },
resources: {
users: {
fetch: () => ({
url: 'http://api.domain.com/users/:id',
opts: {
resource: 'opt',
},
}),
},
},
});
fetch.mockResponseOnce(
JSON.stringify([{ id: '123', name: 'joe' }, { id: '456', name: 'josephine' }]),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
const thunk = executeFetch({
resource, id, opts: { ...opts, method: 'PUT' }, actionType: 'UPDATE_COLLECTION',
});
const data = await thunk(dispatch, getState);
expect(data).toEqual([{ id: '123', name: 'joe' }, { id: '456', name: 'josephine' }]);
expect(fetch).toHaveBeenCalledWith(
'http://api.domain.com/users/123',
{
default: 'opt', method: 'PUT', resource: 'opt', some: 'opt',
}
);
const promise = Promise.resolve(data);
expect(dispatch).toHaveBeenCalledWith({
type: types.UPDATE_COLLECTION_STARTED,
resource,
id,
opts: {
...opts,
method: 'PUT',
},
promise,
});
expect(dispatch).toHaveBeenCalledWith('waitAndDispatchFinishedThunk');
});

it('should not use specified REST method for non overrideable actions in options if provided', async () => {
Object.assign(config, {
defaultOpts: { default: 'opt' },
resources: {
users: {
fetch: () => ({
url: 'http://api.domain.com/users/:id',
opts: {
resource: 'opt',
},
}),
},
},
});
fetch.mockResponseOnce(
JSON.stringify([{ id: '123', name: 'joe' }, { id: '456', name: 'josephine' }]),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
const thunk = executeFetch({
resource, id, opts: { ...opts, method: 'POST' }, actionType: 'LOAD',
});
const data = await thunk(dispatch, getState);
expect(data).toEqual([{ id: '123', name: 'joe' }, { id: '456', name: 'josephine' }]);
expect(fetch).toHaveBeenCalledWith(
'http://api.domain.com/users/123',
{
default: 'opt', method: 'GET', resource: 'opt', some: 'opt',
}
);
const promise = Promise.resolve(data);
expect(dispatch).toHaveBeenCalledWith({
type: types.LOAD_STARTED,
resource,
id,
opts: {
...opts,
method: 'POST',
},
promise,
});
expect(dispatch).toHaveBeenCalledWith('waitAndDispatchFinishedThunk');
});
});
describe('fetchClient', () => {
// Helpers and Mocks
Expand Down
2 changes: 2 additions & 0 deletions __tests__/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
loadCollection,
createResource,
updateResource,
updateCollection,
destroyResource,
queryResource,
queryCollection,
Expand All @@ -43,6 +44,7 @@ describe('index', () => {
expect(getCollection).toBeDefined();
expect(clearResource).toBeDefined();
expect(clearCollection).toBeDefined();
expect(updateCollection).toBeDefined();

expect(resourcesReducer).toBeDefined();
expect(configureIguazuREST).toBeDefined();
Expand Down
97 changes: 97 additions & 0 deletions __tests__/reducer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
LOAD_COLLECTION_STARTED,
CREATE_STARTED,
UPDATE_STARTED,
UPDATE_COLLECTION_STARTED,
DESTROY_STARTED,
PATCH_STARTED,
LOAD_FINISHED,
Expand All @@ -36,6 +37,8 @@ import {
CREATE_ERROR,
UPDATE_FINISHED,
UPDATE_ERROR,
UPDATE_COLLECTION_FINISHED,
UPDATE_COLLECTION_ERROR,
DESTROY_FINISHED,
DESTROY_ERROR,
RESET,
Expand Down Expand Up @@ -102,6 +105,18 @@ describe('reducer', () => {
expect(newState.getIn(['updating', getResourceIdHash(id)])).toEqual(promise);
});

it('should handle UPDATE_COLLECTION_STARTED action', () => {
const promise = Promise.resolve();
const collectionIdHash = getCollectionIdHash();
const queryHash = getQueryHash();
const action = {
type: UPDATE_COLLECTION_STARTED,
promise,
};
const newState = resourceReducer(initialResourceState, action);
expect(newState.getIn(['updating', collectionIdHash, queryHash])).toEqual(promise);
});

it('should handle DESTROY_STARTED action', () => {
const promise = Promise.resolve();
const action = {
Expand Down Expand Up @@ -462,6 +477,88 @@ describe('reducer', () => {
expect(newState.getIn(['items', idHash])).toBeUndefined();
});

it('should handle UPDATE_COLLECTION_FINISHED action with a successful response', () => {
const promise = Promise.resolve();
const collectionIdHash = getCollectionIdHash({});
const queryHash = getQueryHash();
const resourceIdHash = getResourceIdHash(id);
const action = {
type: UPDATE_COLLECTION_FINISHED,
resource: 'users',
idHash: collectionIdHash,
data: [{ id: '123' }],
};
const opts = { query: 'value' };
const differentQueryHash = getQueryHash(opts);
const initialState = initialResourceState.setIn(
['updating', collectionIdHash],
iMap({ [queryHash]: promise, [differentQueryHash]: promise })
);
const newState = resourceReducer(initialState, action);
expect(newState.getIn(['updating', collectionIdHash, queryHash])).toBeUndefined();
expect(newState.getIn(['updating', collectionIdHash, differentQueryHash])).toBeDefined();
expect(newState.getIn(['items', resourceIdHash]).toJS()).toEqual({ id: '123' });

const updatedState = resourceReducer(newState, {
type: UPDATE_COLLECTION_FINISHED,
resource: 'users',
idHash: collectionIdHash,
opts,
data: '',
});
expect(updatedState.getIn(['updating', collectionIdHash, differentQueryHash])).toBeUndefined();
});

it('should reset error on subsequent UPDATE_COLLECTION_FINISHED success', () => {
const promise = Promise.resolve();
const error = new Error('load error');
const collectionIdHash = getCollectionIdHash({});
const queryHash = getQueryHash();
const action = {
type: UPDATE_COLLECTION_FINISHED,
resource: 'users',
idHash: collectionIdHash,
data: [{ id: '123' }],
};
const initialState = initialResourceState
.setIn(['collections', collectionIdHash, queryHash, 'error'], error)
.setIn(['updating', collectionIdHash], iMap({ [queryHash]: promise }));

const newState = resourceReducer(initialState, action);
expect(newState.getIn(['updating', collectionIdHash, queryHash])).toBeUndefined();
});

it('should handle UPDATE_COLLECTION_ERROR action', () => {
const promise = Promise.resolve();
const error = new Error('update error');
const collectionIdHash = getCollectionIdHash({});
const queryHash = getQueryHash();
const action = {
type: UPDATE_COLLECTION_ERROR,
resource: 'users',
idHash: collectionIdHash,
data: error,
};
const opts = { query: 'value' };
const differentQueryHash = getQueryHash(opts);
const initialState = initialResourceState.setIn(
['updating', collectionIdHash],
iMap({ [queryHash]: promise, [differentQueryHash]: promise })
);
const newState = resourceReducer(initialState, action);
expect(newState.getIn(['updating', collectionIdHash, queryHash])).toBeUndefined();
expect(newState.getIn(['updating', collectionIdHash, differentQueryHash])).toBeDefined();

const updatedState = resourceReducer(newState, {
type: UPDATE_COLLECTION_ERROR,
resource: 'users',
idHash: collectionIdHash,
opts,
data: error,
});
expect(updatedState.getIn(['updating', collectionIdHash, differentQueryHash])).toBeUndefined();
});

it('should handle DESTROY_FINISHED action', () => {
const promise = Promise.resolve();
const idHash = getResourceIdHash(id);
Expand Down
6 changes: 6 additions & 0 deletions src/actions/crud.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ export function updateResource({ resource, id, opts }) {
}));
}

export function updateCollection({ resource, id, opts }) {
return (dispatch) => dispatch(executeFetch({
resource, id, opts, actionType: 'UPDATE_COLLECTION',
}));
}

export function destroyResource({ resource, id, opts }) {
return (dispatch) => dispatch(executeFetch({
resource, id, opts, actionType: 'DESTROY',
Expand Down
4 changes: 4 additions & 0 deletions src/actions/executeFetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ const actionTypeMethodMap = {
LOAD_COLLECTION: 'GET',
CREATE: 'POST',
UPDATE: 'PUT',
UPDATE_COLLECTION: 'POST',
DESTROY: 'DELETE',
PATCH: 'PATCH',
};

const overridableActionMethods = new Set(['UPDATE_COLLECTION']);

async function getAsyncData({
resource, id, opts, actionType, state, fetchClient,
}) {
Expand All @@ -61,6 +64,7 @@ async function getAsyncData({
defaultOpts || {},
resourceOpts || {},
opts || {},
{ ...!overridableActionMethods.has(actionType) && { method: actionTypeMethodMap[actionType] } },
]);
const fetchUrl = buildFetchUrl({ url, id, opts: fetchOpts });

Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
loadCollection,
createResource,
updateResource,
updateCollection,
destroyResource,
patchResource,
} from './actions/crud';
Expand Down Expand Up @@ -48,6 +49,7 @@ export {
loadCollection,
createResource,
updateResource,
updateCollection,
patchResource,
destroyResource,
queryResource,
Expand Down

0 comments on commit 573ef1c

Please sign in to comment.