From 4eda4f75dc396b12123e1189f8382aef345d4fd3 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Mon, 12 Sep 2016 15:48:06 +0100 Subject: [PATCH 01/24] Updates and fixes the README.md --- README.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 71ee4ad..fcc2a83 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A [node-wpapi](https://github.com/WP-API/node-wpapi) integration for a Redux based Application. ## How it Works -This library exposes [node-wpapi](https://github.com/WP-API/node-wpapi) instance through the actionCreator [wp](#wp-Action-Creator). The resulting +This library exposes [node-wpapi](https://github.com/WP-API/node-wpapi) instance through the actionCreator [callAPI](#/src/actions/callAPI.js). The resulting action is interpreted in [the middleware](#the-middleware), doing so by resolving the request and controlling the reducer through actions. ## Installation @@ -35,18 +35,26 @@ import { wp, selectQuery, ResponseStatus } from 'redux-wpapi'; import { connect } from 'react-redux'; export class HomePage extends React.Component { - componentWillMount() { - this.props.wp( + static loadData(props) { + return props.callAPI( // The name where the request state will be placed 'HomePagePosts', // A callback where wpapi instance is injected api => - api.posts() - .page(this.props.page) - .perPage(this.props.perPage) + api.posts() + .page(props.page) + .perPage(props.perPage) ); } + componentWillMount() { + HomePage.loadData(this.props); + } + + componentWillReceiveProps(props) { + HomePage.loadData(props); + } + render() { const { status, data: posts } = this.props.request; @@ -67,7 +75,7 @@ export class HomePage extends React.Component { export default connect({ request: selectQuery('HomePagePosts'), -}, { wp })(HomePage); +}, { callAPI })(HomePage); ``` ## Contributions From 8e63e296467fed87bb950be603ff41edab1ce85d Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Tue, 13 Sep 2016 10:41:11 +0100 Subject: [PATCH 02/24] Middleware tests on the way --- src/ReduxWPAPI.js | 33 ++++++++++++------- test/middleware.spec.js | 15 +++++++++ test/mocks/AdapterMockForReducer.js | 12 +++++++ .../actions}/cacheHitCollection.js | 2 +- .../actions}/cacheHitSingle.js | 2 +- .../actions}/collectionRequest.js | 2 +- .../actions}/modifyingRequest.js | 2 +- .../actions}/successfulCollectionRequest.js | 2 +- .../actions}/successfullQueryBySlug.js | 2 +- .../actions}/unsuccessfulCollectionRequest.js | 2 +- .../actions}/unsuccessfulModifyingRequest.js | 2 +- test/mocks/createFakeAdapter.js | 16 +++++++++ test/{ => mocks}/data/collectionResponse.js | 0 test/{ => mocks}/data/queryBySlugResponse.js | 0 test/mocks/store.js | 1 + test/reducer.spec.js | 26 +++++++-------- 16 files changed, 87 insertions(+), 32 deletions(-) create mode 100644 test/middleware.spec.js create mode 100644 test/mocks/AdapterMockForReducer.js rename test/{mocked-actions => mocks/actions}/cacheHitCollection.js (78%) rename test/{mocked-actions => mocks/actions}/cacheHitSingle.js (79%) rename test/{mocked-actions => mocks/actions}/collectionRequest.js (75%) rename test/{mocked-actions => mocks/actions}/modifyingRequest.js (77%) rename test/{mocked-actions => mocks/actions}/successfulCollectionRequest.js (82%) rename test/{mocked-actions => mocks/actions}/successfullQueryBySlug.js (83%) rename test/{mocked-actions => mocks/actions}/unsuccessfulCollectionRequest.js (80%) rename test/{mocked-actions => mocks/actions}/unsuccessfulModifyingRequest.js (78%) create mode 100644 test/mocks/createFakeAdapter.js rename test/{ => mocks}/data/collectionResponse.js (100%) rename test/{ => mocks}/data/queryBySlugResponse.js (100%) create mode 100644 test/mocks/store.js diff --git a/src/ReduxWPAPI.js b/src/ReduxWPAPI.js index a55c538..8214ee4 100644 --- a/src/ReduxWPAPI.js +++ b/src/ReduxWPAPI.js @@ -107,7 +107,11 @@ export default class ReduxWPAPI { }, }); - let ttl = this.adapter.getTTL(request); + let ttl; + if (this.adapter.getTTL) { + ttl = this.adapter.getTTL(request); + } + if (ttl !== 0 && !ttl) { ttl = this.settings.ttl; } @@ -322,18 +326,25 @@ export default class ReduxWPAPI { localID = newState.get('resources').size; } - newState = newState.setIn( - ['resources', localID], - this.settings.transformResource(this.adapter.transformResource({ - ...oldState, - ...resource, - _links, - _embedded, - lastCacheUpdate: meta.lastCacheUpdate, - })) - ); + let resourceTransformed = { + ...oldState, + ...resource, + _links, + _embedded, + lastCacheUpdate: meta.lastCacheUpdate, + }; + if (this.adapter.transformResource) { + resourceTransformed = this.adapter.transformResource(resourceTransformed); + } + + if (this.settings.transformResource) { + resourceTransformed = this.settings.transformResource(resourceTransformed); + } + + newState = newState.setIn(['resources', localID], resourceTransformed); const indexers = this.settings.customCacheIndexes[aggregator]; + forEach(isArray(indexers) ? ['id'].concat(indexers) : ['id', indexers], indexer => { if (!isUndefined(resource[indexer])) { newState = newState.setIn( diff --git a/test/middleware.spec.js b/test/middleware.spec.js new file mode 100644 index 0000000..16c5cf4 --- /dev/null +++ b/test/middleware.spec.js @@ -0,0 +1,15 @@ +import { describe, it } from 'mocha'; +import expect from 'expect'; +import ReduxWPAPI from '../src/index.js'; +import createFakeAdapter from './mocks/createFakeAdapter'; + +describe('Middleware', () => { + it('should return a function', () => { + const { middleware } = new ReduxWPAPI({ + adapter: createFakeAdapter({}), + }); + + expect(middleware({})).toBeA('function'); + }); +}); + diff --git a/test/mocks/AdapterMockForReducer.js b/test/mocks/AdapterMockForReducer.js new file mode 100644 index 0000000..86c9f8a --- /dev/null +++ b/test/mocks/AdapterMockForReducer.js @@ -0,0 +1,12 @@ +export default class AdapterMockForReducer { + // _paging extractor + getPagination = ({ _paging }) => _paging; + + // no link renaming + embedLinkAs = ({ name }) => name; + + // expects users or any for test reducers + getAggregator(url) { + return url.match(/users/) ? 'users' : 'any'; + } +} diff --git a/test/mocked-actions/cacheHitCollection.js b/test/mocks/actions/cacheHitCollection.js similarity index 78% rename from test/mocked-actions/cacheHitCollection.js rename to test/mocks/actions/cacheHitCollection.js index 9219a61..009352a 100644 --- a/test/mocked-actions/cacheHitCollection.js +++ b/test/mocks/actions/cacheHitCollection.js @@ -1,4 +1,4 @@ -import { REDUX_WP_API_CACHE_HIT } from '../../src/constants/actions'; +import { REDUX_WP_API_CACHE_HIT } from '../../../src/constants/actions'; export default { type: REDUX_WP_API_CACHE_HIT, diff --git a/test/mocked-actions/cacheHitSingle.js b/test/mocks/actions/cacheHitSingle.js similarity index 79% rename from test/mocked-actions/cacheHitSingle.js rename to test/mocks/actions/cacheHitSingle.js index 4eeb98f..c9c9fa8 100644 --- a/test/mocked-actions/cacheHitSingle.js +++ b/test/mocks/actions/cacheHitSingle.js @@ -1,4 +1,4 @@ -import { REDUX_WP_API_CACHE_HIT } from '../../src/constants/actions'; +import { REDUX_WP_API_CACHE_HIT } from '../../../src/constants/actions'; export default { type: REDUX_WP_API_CACHE_HIT, diff --git a/test/mocked-actions/collectionRequest.js b/test/mocks/actions/collectionRequest.js similarity index 75% rename from test/mocked-actions/collectionRequest.js rename to test/mocks/actions/collectionRequest.js index fdc3a3a..c9baac6 100644 --- a/test/mocked-actions/collectionRequest.js +++ b/test/mocks/actions/collectionRequest.js @@ -1,4 +1,4 @@ -import { REDUX_WP_API_REQUEST } from '../../src/constants/actions'; +import { REDUX_WP_API_REQUEST } from '../../../src/constants/actions'; export default { type: REDUX_WP_API_REQUEST, diff --git a/test/mocked-actions/modifyingRequest.js b/test/mocks/actions/modifyingRequest.js similarity index 77% rename from test/mocked-actions/modifyingRequest.js rename to test/mocks/actions/modifyingRequest.js index 3ab1719..f3f750c 100644 --- a/test/mocked-actions/modifyingRequest.js +++ b/test/mocks/actions/modifyingRequest.js @@ -1,4 +1,4 @@ -import { REDUX_WP_API_REQUEST } from '../../src/constants/actions'; +import { REDUX_WP_API_REQUEST } from '../../../src/constants/actions'; export default { type: REDUX_WP_API_REQUEST, diff --git a/test/mocked-actions/successfulCollectionRequest.js b/test/mocks/actions/successfulCollectionRequest.js similarity index 82% rename from test/mocked-actions/successfulCollectionRequest.js rename to test/mocks/actions/successfulCollectionRequest.js index 9c2b502..493e119 100644 --- a/test/mocked-actions/successfulCollectionRequest.js +++ b/test/mocks/actions/successfulCollectionRequest.js @@ -1,4 +1,4 @@ -import { REDUX_WP_API_SUCCESS } from '../../src/constants/actions'; +import { REDUX_WP_API_SUCCESS } from '../../../src/constants/actions'; import collectionResponse from '../data/collectionResponse'; export default { diff --git a/test/mocked-actions/successfullQueryBySlug.js b/test/mocks/actions/successfullQueryBySlug.js similarity index 83% rename from test/mocked-actions/successfullQueryBySlug.js rename to test/mocks/actions/successfullQueryBySlug.js index ab6bd05..5b43744 100644 --- a/test/mocked-actions/successfullQueryBySlug.js +++ b/test/mocks/actions/successfullQueryBySlug.js @@ -1,4 +1,4 @@ -import { REDUX_WP_API_SUCCESS } from '../../src/constants/actions'; +import { REDUX_WP_API_SUCCESS } from '../../../src/constants/actions'; import queryBySlugResponse from '../data/queryBySlugResponse'; export default { diff --git a/test/mocked-actions/unsuccessfulCollectionRequest.js b/test/mocks/actions/unsuccessfulCollectionRequest.js similarity index 80% rename from test/mocked-actions/unsuccessfulCollectionRequest.js rename to test/mocks/actions/unsuccessfulCollectionRequest.js index 25effda..ada7ba9 100644 --- a/test/mocked-actions/unsuccessfulCollectionRequest.js +++ b/test/mocks/actions/unsuccessfulCollectionRequest.js @@ -1,4 +1,4 @@ -import { REDUX_WP_API_FAILURE } from '../../src/constants/actions'; +import { REDUX_WP_API_FAILURE } from '../../../src/constants/actions'; export default { type: REDUX_WP_API_FAILURE, diff --git a/test/mocked-actions/unsuccessfulModifyingRequest.js b/test/mocks/actions/unsuccessfulModifyingRequest.js similarity index 78% rename from test/mocked-actions/unsuccessfulModifyingRequest.js rename to test/mocks/actions/unsuccessfulModifyingRequest.js index 111c0cb..e466e67 100644 --- a/test/mocked-actions/unsuccessfulModifyingRequest.js +++ b/test/mocks/actions/unsuccessfulModifyingRequest.js @@ -1,4 +1,4 @@ -import { REDUX_WP_API_FAILURE } from '../../src/constants/actions'; +import { REDUX_WP_API_FAILURE } from '../../../src/constants/actions'; export default { type: REDUX_WP_API_FAILURE, diff --git a/test/mocks/createFakeAdapter.js b/test/mocks/createFakeAdapter.js new file mode 100644 index 0000000..62357ef --- /dev/null +++ b/test/mocks/createFakeAdapter.js @@ -0,0 +1,16 @@ +export default (actionTarget, override) => { + class Adapter { + getAggregator() { return actionTarget.meta.aggregator; } + getOperation() { return actionTarget.meta.operation; } + generateCacheID() { return actionTarget.payload.cacheID; } + getRequestedPage() { return actionTarget.payload.page; } + + getUrl() { return ''; } + getIndexes() { return {}; } + buildRequest() { return Promise.resolve(); } + } + + Object.assign(Adapter.prototype, override); + return new Adapter(); +}; + diff --git a/test/data/collectionResponse.js b/test/mocks/data/collectionResponse.js similarity index 100% rename from test/data/collectionResponse.js rename to test/mocks/data/collectionResponse.js diff --git a/test/data/queryBySlugResponse.js b/test/mocks/data/queryBySlugResponse.js similarity index 100% rename from test/data/queryBySlugResponse.js rename to test/mocks/data/queryBySlugResponse.js diff --git a/test/mocks/store.js b/test/mocks/store.js new file mode 100644 index 0000000..86dadab --- /dev/null +++ b/test/mocks/store.js @@ -0,0 +1 @@ +export const storeFactory = fakeData => ({ getState: () => fakeData }); diff --git a/test/reducer.spec.js b/test/reducer.spec.js index ac333ec..18eaa2d 100644 --- a/test/reducer.spec.js +++ b/test/reducer.spec.js @@ -1,26 +1,26 @@ import { describe, it } from 'mocha'; import expect from 'expect'; -import WPAPI from 'wpapi'; import Immutable from 'immutable'; import ReduxWPAPI from '../src/index.js'; import { pending, resolved, rejected } from '../src/constants/requestStatus'; -import collectionRequest from './mocked-actions/collectionRequest'; -import modifyingRequest from './mocked-actions/modifyingRequest'; -import successfulCollectionRequest from './mocked-actions/successfulCollectionRequest'; -import successfullQueryBySlug from './mocked-actions/successfullQueryBySlug'; -import unsuccessfulCollectionRequest from './mocked-actions/unsuccessfulCollectionRequest'; -import unsuccessfulModifyingRequest from './mocked-actions/unsuccessfulModifyingRequest'; -import cacheHitSingle from './mocked-actions/cacheHitSingle'; -import cacheHitCollection from './mocked-actions/cacheHitCollection'; +import collectionRequest from './mocks/actions/collectionRequest'; +import modifyingRequest from './mocks/actions/modifyingRequest'; +import successfulCollectionRequest from './mocks/actions/successfulCollectionRequest'; +import successfullQueryBySlug from './mocks/actions/successfullQueryBySlug'; +import unsuccessfulCollectionRequest from './mocks/actions/unsuccessfulCollectionRequest'; +import unsuccessfulModifyingRequest from './mocks/actions/unsuccessfulModifyingRequest'; +import cacheHitSingle from './mocks/actions/cacheHitSingle'; +import cacheHitCollection from './mocks/actions/cacheHitCollection'; +import AdapterMockForReducer from './mocks/AdapterMockForReducer'; describe('Reducer', () => { - const modifingOperations = ['create', 'update', 'delete']; + const modifyingOperations = ['create', 'update', 'delete']; let reducer; beforeEach(() => { reducer = new ReduxWPAPI({ - api: new WPAPI({ endpoint: 'http://dumb.url/wp-json/' }), + adapter: new AdapterMockForReducer(), customCacheIndexes: { any: 'slug', }, @@ -86,7 +86,7 @@ describe('Reducer', () => { }); }); - modifingOperations + modifyingOperations .forEach(type => describe(`on operation ${type.toUpperCase()}`, () => { const request = { ...modifyingRequest, meta: { ...modifyingRequest.meta, type } }; @@ -190,7 +190,7 @@ describe('Reducer', () => { }); }); - modifingOperations.forEach(type => + modifyingOperations.forEach(type => describe(`on ${type} operation`, () => { const response = { ...unsuccessfulModifyingRequest, From 94b9a4ba159a57e6c31f8542906fa507912ef728 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Tue, 13 Sep 2016 16:56:14 +0100 Subject: [PATCH 03/24] [WIP] Middlewares are now been tested --- src/ReduxWPAPI.js | 4 +- test/middleware.spec.js | 133 ++++++++++++++++++- test/mocks/actions/successfullQueryBySlug.js | 2 +- test/mocks/createFakeAdapter.js | 3 +- test/mocks/data/queryBySlugResponse.js | 2 +- test/mocks/store.js | 11 +- 6 files changed, 145 insertions(+), 10 deletions(-) diff --git a/src/ReduxWPAPI.js b/src/ReduxWPAPI.js index 8214ee4..61ba6fb 100644 --- a/src/ReduxWPAPI.js +++ b/src/ReduxWPAPI.js @@ -25,7 +25,7 @@ import { rejected, } from './constants/requestStatus'; -const initialReducerState = Immutable.fromJS({ +export const initialReducerState = Immutable.fromJS({ requestsByName: {}, requestsByQuery: {}, @@ -66,7 +66,6 @@ export default class ReduxWPAPI { name: action.payload.name, aggregator: this.adapter.getAggregator(this.adapter.getUrl(request)), operation: this.adapter.getOperation(request), - params: action.payload.params, requestAt: Date.now(), }; @@ -93,7 +92,6 @@ export default class ReduxWPAPI { cache = state.getIn(['requestsByQuery', payload.cacheID, payload.page]); data = state.get('data'); } - if (cache && (localID || (isUndefined(localID) && !cache.get('error')))) { lastCacheUpdate = lastCacheUpdate || cache.get('responseAt') || cache.get('requestAt'); next({ diff --git a/test/middleware.spec.js b/test/middleware.spec.js index 16c5cf4..6d3c799 100644 --- a/test/middleware.spec.js +++ b/test/middleware.spec.js @@ -2,14 +2,141 @@ import { describe, it } from 'mocha'; import expect from 'expect'; import ReduxWPAPI from '../src/index.js'; import createFakeAdapter from './mocks/createFakeAdapter'; +import noop from 'lodash/noop'; +import Immutable from 'immutable'; + +import collectionRequest from './mocks/actions/collectionRequest'; +import successfulCollectionRequest from './mocks/actions/successfulCollectionRequest'; +import successfullQueryBySlug from './mocks/actions/successfullQueryBySlug'; +import { createFakeStore } from './mocks/store'; +import { initialReducerState } from '../src/ReduxWPAPI'; + +import { REDUX_WP_API_CALL, REDUX_WP_API_CACHE_HIT } from '../src/constants/actions'; + +const createCallAPIActionFrom = ({ + meta: { name }, + payload: { cacheID, page }, + response, +}) => ({ + type: REDUX_WP_API_CALL, + payload: { + name, + request: { cacheID, page }, + aditionalParams: !Array.isArray(response) ? response : {}, + }, +}); describe('Middleware', () => { - it('should return a function', () => { + it('should implement middleware signature (store => next => action =>)', () => { + const { middleware } = new ReduxWPAPI({ + adapter: createFakeAdapter(successfulCollectionRequest), + }); + const fakeEmptyStore = createFakeStore(); + const fakeNext = noop; + + expect(middleware(fakeEmptyStore)(fakeNext)).toBeA('function'); + }); + + + it('should propagate other actions and next dispatch/middleware return for NO-OP actions', () => { const { middleware } = new ReduxWPAPI({ - adapter: createFakeAdapter({}), + adapter: createFakeAdapter(successfulCollectionRequest), }); - expect(middleware({})).toBeA('function'); + const dispatched = []; + const nextMiddlewareReturn = Symbol(); + const fakeNext = dispatch => dispatched.push(dispatch) && nextMiddlewareReturn; + const action = { type: 'NO-OP ACTION' }; + + const result = middleware(createFakeStore())(fakeNext)(action); + expect(result).toBe(nextMiddlewareReturn); + + expect(dispatched.length).toBe(1); + expect(dispatched[0]).toBe(action); + }); + + it('should dispatch REQUEST and SUCCESSFUL action when request is not cached', () => { + const { middleware } = new ReduxWPAPI({ + adapter: createFakeAdapter(successfulCollectionRequest), + }); + + const dispatched = []; + const fakeNext = dispatch => dispatched.push(dispatch); + const action = createCallAPIActionFrom(successfulCollectionRequest); + + const result = middleware(createFakeStore())(fakeNext)(action); + expect(result).toBeA(Promise); + + return result.then(() => { + expect(dispatched.length).toBe(2); + expect(dispatched[0]).toEqual({ + ...collectionRequest, + meta: { + ...collectionRequest.meta, + requestAt: dispatched[0].meta.requestAt, + }, + }); + expect(dispatched[1]).toEqual({ + ...successfulCollectionRequest, + meta: { + ...successfulCollectionRequest.meta, + requestAt: dispatched[1].meta.requestAt, + responseAt: dispatched[1].meta.responseAt, + }, + }); + }); + }); + + it('should dispatch only CACHE_HIT action when request is cached within TTL', () => { + const { middleware } = new ReduxWPAPI({ + adapter: createFakeAdapter(successfullQueryBySlug, { + getTTL() { return Infinity; }, + getIndexes() { return { slug: 'dumb1-modified' }; }, + }), + customCacheIndexes: { + any: 'slug', + }, + }); + + const dispatched = []; + const nextMiddlewareReturn = Symbol(); + const fakeNext = dispatch => dispatched.push(dispatch) && nextMiddlewareReturn; + const action = createCallAPIActionFrom(successfullQueryBySlug); + + const any = successfullQueryBySlug.payload.response[0]; + const author = any._embedded.author[0]; + + // CACHED STATE + const fakeStore = createFakeStore({ + wp: initialReducerState.set( + 'resources', + new Immutable.List([ + { ...author, lastCacheUpdate: Date.now() }, + { + ...any, + lastCacheUpdate: Date.now(), + _embedded: { author: 0 }, + }, + ]) + ) + .set( + 'resourcesIndexes', + Immutable.fromJS({ + any: { + slug: { 'dumb1-modified': 1 }, + id: { 1: 1 }, + }, + }) + ), + }); + + const result = middleware(fakeStore)(fakeNext)(action); + expect(result).toBeA(Promise); + + return result.then(() => { + expect(dispatched.length).toBe(1); + expect(dispatched[0].type).toBe(REDUX_WP_API_CACHE_HIT); + }); }); }); diff --git a/test/mocks/actions/successfullQueryBySlug.js b/test/mocks/actions/successfullQueryBySlug.js index 5b43744..5c9cf2b 100644 --- a/test/mocks/actions/successfullQueryBySlug.js +++ b/test/mocks/actions/successfullQueryBySlug.js @@ -4,7 +4,7 @@ import queryBySlugResponse from '../data/queryBySlugResponse'; export default { type: REDUX_WP_API_SUCCESS, payload: { - cacheID: '/namespace/any?slug=dumb2', + cacheID: '/namespace/any?slug=dumb1-modified', page: 1, response: queryBySlugResponse, }, diff --git a/test/mocks/createFakeAdapter.js b/test/mocks/createFakeAdapter.js index 62357ef..e0ac7c4 100644 --- a/test/mocks/createFakeAdapter.js +++ b/test/mocks/createFakeAdapter.js @@ -7,7 +7,8 @@ export default (actionTarget, override) => { getUrl() { return ''; } getIndexes() { return {}; } - buildRequest() { return Promise.resolve(); } + buildRequest() { return {}; } + sendRequest() { return Promise.resolve(actionTarget.payload.response); } } Object.assign(Adapter.prototype, override); diff --git a/test/mocks/data/queryBySlugResponse.js b/test/mocks/data/queryBySlugResponse.js index c479f3c..049de5a 100644 --- a/test/mocks/data/queryBySlugResponse.js +++ b/test/mocks/data/queryBySlugResponse.js @@ -32,7 +32,7 @@ const queryBySlugResponse = [ queryBySlugResponse._paging = { total: 1, - totalPages: 2, + totalPages: 1, }; export default queryBySlugResponse; diff --git a/test/mocks/store.js b/test/mocks/store.js index 86dadab..4351583 100644 --- a/test/mocks/store.js +++ b/test/mocks/store.js @@ -1 +1,10 @@ -export const storeFactory = fakeData => ({ getState: () => fakeData }); +import { initialReducerState } from '../../src/ReduxWPAPI'; + +const initialStore = { wp: initialReducerState }; +export const createFakeStore = (fakeData = initialStore) => ({ getState: () => fakeData }); + + +// export const dispatchWithStoreOf = (storeData, action) => +// new Promise((resolve, reject) => { +// x +// }); From ea8dbe42b96a840508dbc7f37555b711d6d9ec63 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Wed, 14 Sep 2016 10:26:59 +0100 Subject: [PATCH 04/24] Fixing selector --- src/selectors.js | 16 ++++++++-------- test/mocks/store.js | 1 - 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/selectors.js b/src/selectors.js index 592a8df..9d9a860 100644 --- a/src/selectors.js +++ b/src/selectors.js @@ -2,22 +2,22 @@ import { createSelector } from 'reselect'; import { pending } from './constants/requestStatus'; import { mapDeep } from './helpers'; -export const denormalize = (entities, id, memoized = {}) => { +export const denormalize = (resources, id, memoized = {}) => { /* eslint-disable no-param-reassign, no-underscore-dangle */ if (memoized[id]) return memoized[id]; - const entity = entities.get(id); + const resource = resources.get(id); memoized[id] = { - ...entity, - ...mapDeep(entity._embedded || {}, - embeddedId => denormalize(entities, embeddedId, memoized) + ...resource, + ...mapDeep(resource._embedded || {}, + embeddedId => denormalize(resources, embeddedId, memoized) ), }; return memoized[id]; }; -export const localEntities = state => state.wp.getIn(['entities']); +export const localEntities = state => state.wp.getIn(['resources']); export const selectQuery = name => createSelector( createSelector( @@ -39,7 +39,7 @@ export const selectQuery = name => createSelector( } ), localEntities, - (request, entities) => { + (request, resources) => { if (!request) { return { status: pending, @@ -65,7 +65,7 @@ export const selectQuery = name => createSelector( return request.set( 'data', request.get('operation') === 'get' ? - data.map(id => denormalize(entities, id, memo)) : + data.map(id => denormalize(resources, id, memo)) : data ).toJSON(); } diff --git a/test/mocks/store.js b/test/mocks/store.js index 4351583..3038619 100644 --- a/test/mocks/store.js +++ b/test/mocks/store.js @@ -3,7 +3,6 @@ import { initialReducerState } from '../../src/ReduxWPAPI'; const initialStore = { wp: initialReducerState }; export const createFakeStore = (fakeData = initialStore) => ({ getState: () => fakeData }); - // export const dispatchWithStoreOf = (storeData, action) => // new Promise((resolve, reject) => { // x From 76ecf39902aa22763ae47ebec4a0a8c44622baa5 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Wed, 14 Sep 2016 18:44:01 +0100 Subject: [PATCH 05/24] Adds Middleware and Selector tests --- src/callAPI.js | 2 - test/middleware.spec.js | 39 +++++++++++ test/selector.spec.js | 143 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 test/selector.spec.js diff --git a/src/callAPI.js b/src/callAPI.js index a84d13a..c009dd5 100644 --- a/src/callAPI.js +++ b/src/callAPI.js @@ -1,7 +1,5 @@ import { REDUX_WP_API_CALL } from './constants/actions'; -export * as types from './constants/actions'; - export default (name, request, aditionalParams = {}) => ({ type: REDUX_WP_API_CALL, payload: { name, request, aditionalParams }, diff --git a/test/middleware.spec.js b/test/middleware.spec.js index 6d3c799..6002b85 100644 --- a/test/middleware.spec.js +++ b/test/middleware.spec.js @@ -7,6 +7,7 @@ import Immutable from 'immutable'; import collectionRequest from './mocks/actions/collectionRequest'; import successfulCollectionRequest from './mocks/actions/successfulCollectionRequest'; +import unsuccessfulCollectionRequest from './mocks/actions/unsuccessfulCollectionRequest'; import successfullQueryBySlug from './mocks/actions/successfullQueryBySlug'; import { createFakeStore } from './mocks/store'; import { initialReducerState } from '../src/ReduxWPAPI'; @@ -138,5 +139,43 @@ describe('Middleware', () => { expect(dispatched[0].type).toBe(REDUX_WP_API_CACHE_HIT); }); }); + + it('should dispatch REQUEST and FAILURE action when request is not cached and fails', () => { + const { middleware } = new ReduxWPAPI({ + adapter: createFakeAdapter(unsuccessfulCollectionRequest, { + sendRequest: () => Promise.reject(unsuccessfulCollectionRequest.error), + }), + }); + + const dispatched = []; + const fakeNext = dispatch => dispatched.push(dispatch); + const action = createCallAPIActionFrom(unsuccessfulCollectionRequest); + + const result = middleware(createFakeStore())(fakeNext)(action); + expect(result).toBeA(Promise); + + return result.then(() => { + expect(dispatched.length).toBe(2); + expect(dispatched[0]).toEqual({ + ...collectionRequest, + payload: { + ...collectionRequest.payload, + cacheID: unsuccessfulCollectionRequest.payload.cacheID, + }, + meta: { + ...collectionRequest.meta, + requestAt: dispatched[0].meta.requestAt, + }, + }); + expect(dispatched[1]).toEqual({ + ...unsuccessfulCollectionRequest, + meta: { + ...unsuccessfulCollectionRequest.meta, + requestAt: dispatched[1].meta.requestAt, + responseAt: dispatched[1].meta.responseAt, + }, + }); + }); + }); }); diff --git a/test/selector.spec.js b/test/selector.spec.js new file mode 100644 index 0000000..f975549 --- /dev/null +++ b/test/selector.spec.js @@ -0,0 +1,143 @@ +import { describe, it } from 'mocha'; +import expect from 'expect'; +import Immutable from 'immutable'; +import { initialReducerState } from '../src/ReduxWPAPI'; +import { selectQuery } from '../src/selectors'; +import { pending, resolved } from '../src/constants/requestStatus'; + +describe('Selector', () => { + it('should return a Request for empty state', () => { + const state = { wp: initialReducerState }; + expect(selectQuery('test')(state)) + .toEqual({ + status: pending, + error: false, + data: false, + }); + }); + + it('should return a Request for pending state', () => { + const cacheID = 'test/'; + const queryState = { status: pending, error: false, requestAt: Date.now(), operation: 'get' }; + const state = { + wp: ( + initialReducerState + .mergeIn(['requestsByName', 'test'], { cacheID, page: 1 }) + .setIn(['requestsByQuery', 'test/', 1], queryState) + ), + }; + expect(selectQuery('test')(state)) + .toEqual({ + status: pending, + operation: queryState.operation, + requestAt: queryState.requestAt, + cacheID, + error: false, + data: false, + page: 1, + }); + }); + + it('should return a Request for resolved state without embedded', () => { + const resource = { id: 1, title: 'lol' }; + const cacheID = 'test/'; + const queryState = { + status: resolved, + error: false, + requestAt: Date.now(), + operation: 'get', + data: [0], + }; + const state = { + wp: ( + initialReducerState + .setIn(['resources', 0], resource) + .mergeIn(['requestsByName', 'test'], { cacheID, page: 1 }) + .setIn(['requestsByQuery', 'test/', 1], queryState) + ), + }; + + const selector = selectQuery('test'); + const selectedState = selector(state); + expect(selectedState) + .toContain({ + status: resolved, + operation: queryState.operation, + requestAt: queryState.requestAt, + cacheID, + error: false, + page: 1, + }); + + expect(selectedState.data).toBeAn('array'); + expect(selectedState.data[0]).toEqual(resource); + }); + + it('should return a Request for resolved state with embedded', () => { + const resources = [ + { id: 1, + title: 'lol', + _links: { parent: { url: 'http://dumb.com/test/2' } }, + _embedded: { parent: 1 }, + }, + { id: 2, title: 'lol 2' }, + ]; + const cacheID = 'test/'; + const queryState = { + status: resolved, + error: false, + requestAt: Date.now(), + operation: 'get', + data: [0], + }; + const state = { + wp: ( + initialReducerState + .setIn(['resources'], new Immutable.List(resources)) + .mergeIn(['requestsByName', 'test'], { cacheID, page: 1 }) + .setIn(['requestsByQuery', 'test/', 1], queryState) + ), + }; + + const selector = selectQuery('test'); + const selectedState = selector(state); + expect(selectedState) + .toContain({ + status: resolved, + operation: queryState.operation, + requestAt: queryState.requestAt, + cacheID, + error: false, + page: 1, + }); + + expect(selectedState.data).toBeAn('array'); + expect(selectedState.data[0]).toInclude(resources[0]); + expect(selectedState.data[0].parent).toInclude(resources[1]); + }); + + it('should refer to same objects between two selections with same input state', () => { + const resource = { id: 1, title: 'lol' }; + const cacheID = 'test/'; + const queryState = { + status: resolved, + error: false, + requestAt: Date.now(), + operation: 'get', + data: [0], + }; + const state = { + wp: ( + initialReducerState + .setIn(['resources', 0], resource) + .mergeIn(['requestsByName', 'test'], { cacheID, page: 1 }) + .setIn(['requestsByQuery', 'test/', 1], queryState) + ), + }; + + const selector = selectQuery('test'); + const selectedState = selector(state); + expect(selector(state)).toBe(selectedState); + }); +}); + From 3edee49efb41d2814c7219c21dfb6b007fef7c66 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Thu, 15 Sep 2016 19:00:56 +0100 Subject: [PATCH 06/24] Test Middleware return; Denormalize in selectors operation unwise --- src/ReduxWPAPI.js | 38 +++---- src/selectors.js | 5 +- test/adapter.spec.js | 13 +++ test/middleware.spec.js | 143 +++++++++++++++++++------ test/mocks/data/collectionResponse.js | 12 +-- test/mocks/data/queryBySlugResponse.js | 6 +- test/mocks/store.js | 12 +-- 7 files changed, 161 insertions(+), 68 deletions(-) create mode 100644 test/adapter.spec.js diff --git a/src/ReduxWPAPI.js b/src/ReduxWPAPI.js index 61ba6fb..5697a26 100644 --- a/src/ReduxWPAPI.js +++ b/src/ReduxWPAPI.js @@ -116,7 +116,7 @@ export default class ReduxWPAPI { if (Date.now() - lastCacheUpdate < ttl) { return Promise.resolve( - store.getState().wp.getIn(['requestsByName', meta.name]) + selectQuery(meta.name)(store.getState()) ); } } @@ -128,23 +128,25 @@ export default class ReduxWPAPI { meta, }); - return this.adapter.sendRequest(request) - .then( - response => - next({ - type: REDUX_WP_API_SUCCESS, - payload: { ...payload, response }, - meta: { ...meta, responseAt: Date.now() }, - }), - error => - next({ - type: REDUX_WP_API_FAILURE, - payload, - error, - meta: { ...meta, responseAt: Date.now() }, - }) - ) - .then(() => selectQuery(meta.name)(store.getState())); + return ( + this.adapter.sendRequest(request) + .then( + response => + next({ + type: REDUX_WP_API_SUCCESS, + payload: { ...payload, response }, + meta: { ...meta, responseAt: Date.now() }, + }), + error => + next({ + type: REDUX_WP_API_FAILURE, + payload, + error, + meta: { ...meta, responseAt: Date.now() }, + }) + ) + .then(() => selectQuery(meta.name)(store.getState())) + ); } reducer = (state = initialReducerState, action) => { diff --git a/src/selectors.js b/src/selectors.js index dda6f76..3feb531 100644 --- a/src/selectors.js +++ b/src/selectors.js @@ -63,10 +63,7 @@ export const selectQuery = name => createSelector( } return request.set( - 'data', - request.get('operation') === 'get' ? - data.map(id => denormalize(resources, id, memo)) : - data + 'data', data.map(id => denormalize(resources, id, memo)) ).toJSON(); } ); diff --git a/test/adapter.spec.js b/test/adapter.spec.js new file mode 100644 index 0000000..8cadd46 --- /dev/null +++ b/test/adapter.spec.js @@ -0,0 +1,13 @@ +import { describe, it } from 'mocha'; +import expect from 'expect'; +import WPAPIAdapter from '../src/adapters/wpapi'; + +describe('WPAPI Adapter', () => { + it('should complain if no api is given', () => { + expect(() => { + // eslint-disable-next-line + new WPAPIAdapter(); + }).toThrow(); + }); +}); + diff --git a/test/middleware.spec.js b/test/middleware.spec.js index 6002b85..659358e 100644 --- a/test/middleware.spec.js +++ b/test/middleware.spec.js @@ -1,18 +1,19 @@ import { describe, it } from 'mocha'; import expect from 'expect'; -import ReduxWPAPI from '../src/index.js'; -import createFakeAdapter from './mocks/createFakeAdapter'; import noop from 'lodash/noop'; import Immutable from 'immutable'; +import ReduxWPAPI from '../src/index.js'; +import createFakeAdapter from './mocks/createFakeAdapter'; + import collectionRequest from './mocks/actions/collectionRequest'; import successfulCollectionRequest from './mocks/actions/successfulCollectionRequest'; import unsuccessfulCollectionRequest from './mocks/actions/unsuccessfulCollectionRequest'; import successfullQueryBySlug from './mocks/actions/successfullQueryBySlug'; import { createFakeStore } from './mocks/store'; import { initialReducerState } from '../src/ReduxWPAPI'; - import { REDUX_WP_API_CALL, REDUX_WP_API_CACHE_HIT } from '../src/constants/actions'; +import { resolved, rejected } from '../src/constants/requestStatus'; const createCallAPIActionFrom = ({ meta: { name }, @@ -27,6 +28,44 @@ const createCallAPIActionFrom = ({ }, }); +const successfullQueryBySlugState = initialReducerState.set( + 'resources', + new Immutable.List([ + { + ...successfullQueryBySlug.payload.response[0]._embedded.author[0], + lastCacheUpdate: Date.now() }, + { + ...successfullQueryBySlug.payload.response[0], + lastCacheUpdate: Date.now(), + _embedded: { author: 0 }, + }, + ]) +) +.set( + 'resourcesIndexes', + Immutable.fromJS({ + any: { + slug: { 'dumb1-modified': 1 }, + id: { 1: 1 }, + }, + }) +) +.mergeIn( + ['requestsByName', successfullQueryBySlug.meta.name], + { + cacheID: successfullQueryBySlug.payload.cacheID, + page: successfullQueryBySlug.payload.page, + } +) +.mergeIn( + ['requestsByQuery', successfullQueryBySlug.payload.cacheID, successfullQueryBySlug.payload.page], + { + status: resolved, + error: false, + data: [0], + } +); + describe('Middleware', () => { it('should implement middleware signature (store => next => action =>)', () => { const { middleware } = new ReduxWPAPI({ @@ -63,8 +102,8 @@ describe('Middleware', () => { const dispatched = []; const fakeNext = dispatch => dispatched.push(dispatch); - const action = createCallAPIActionFrom(successfulCollectionRequest); + const action = createCallAPIActionFrom(successfulCollectionRequest); const result = middleware(createFakeStore())(fakeNext)(action); expect(result).toBeA(Promise); @@ -104,33 +143,8 @@ describe('Middleware', () => { const fakeNext = dispatch => dispatched.push(dispatch) && nextMiddlewareReturn; const action = createCallAPIActionFrom(successfullQueryBySlug); - const any = successfullQueryBySlug.payload.response[0]; - const author = any._embedded.author[0]; - // CACHED STATE - const fakeStore = createFakeStore({ - wp: initialReducerState.set( - 'resources', - new Immutable.List([ - { ...author, lastCacheUpdate: Date.now() }, - { - ...any, - lastCacheUpdate: Date.now(), - _embedded: { author: 0 }, - }, - ]) - ) - .set( - 'resourcesIndexes', - Immutable.fromJS({ - any: { - slug: { 'dumb1-modified': 1 }, - id: { 1: 1 }, - }, - }) - ), - }); - + const fakeStore = createFakeStore({ wp: successfullQueryBySlugState }); const result = middleware(fakeStore)(fakeNext)(action); expect(result).toBeA(Promise); @@ -177,5 +191,74 @@ describe('Middleware', () => { }); }); }); + + it('should return a promise that resolves to selectQuery result', () => { + const { middleware } = new ReduxWPAPI({ + adapter: createFakeAdapter(successfulCollectionRequest), + }); + + const dispatched = []; + const store = createFakeStore(); + const fakeNext = dispatch => { + dispatched.push(dispatch); + store.state = { wp: successfullQueryBySlugState }; + }; + const action = createCallAPIActionFrom(successfulCollectionRequest); + const result = middleware(store)(fakeNext)(action); + expect(result).toBeA(Promise); + + return result.then(response => { + expect(response) + .toInclude({ + status: resolved, + error: false, + }); + expect(response.data).toBeA('array'); + expect(response.data.length).toBe(1); + expect(response.data[0]).toBeAn('object'); + }); + }); + + it('should return a promise that reject to selectQuery result', () => { + const { middleware } = new ReduxWPAPI({ + adapter: createFakeAdapter(unsuccessfulCollectionRequest, { + sendRequest: () => Promise.reject(unsuccessfulCollectionRequest.error), + }), + }); + + const dispatched = []; + const store = createFakeStore(); + const { name } = unsuccessfulCollectionRequest.meta; + const { page, cacheID } = unsuccessfulCollectionRequest.payload; + const fakeNext = dispatch => { + dispatched.push(dispatch); + store.state = { + wp: initialReducerState.mergeIn( + ['requestsByQuery', cacheID, page], + { + status: rejected, + data: false, + error: unsuccessfulCollectionRequest.error, + } + ) + .mergeIn( + ['requestsByName', name], + { cacheID, page } + ), + }; + }; + const action = createCallAPIActionFrom(successfulCollectionRequest); + const result = middleware(store)(fakeNext)(action); + expect(result).toBeA(Promise); + + return result.then(response => { + expect(response) + .toInclude({ + status: rejected, + data: false, + error: unsuccessfulCollectionRequest.error, + }); + }); + }); }); diff --git a/test/mocks/data/collectionResponse.js b/test/mocks/data/collectionResponse.js index 276ec6e..969e3e0 100644 --- a/test/mocks/data/collectionResponse.js +++ b/test/mocks/data/collectionResponse.js @@ -19,11 +19,11 @@ const collectionResponse = [ author: [{ id: 1, name: 'admin', - link: 'http://km.nos.dev/author/admin/', + link: 'http://dumb.url/wp-json/author/admin/', slug: 'admin', _links: { - self: [{ href: 'http://km.nos.dev/wp-json/wp/v2/users/1' }], - collection: [{ href: 'http://km.nos.dev/wp-json/wp/v2/users' }], + self: [{ href: 'http://dumb.url/wp-json/wp/v2/users/1' }], + collection: [{ href: 'http://dumb.url/wp-json/wp/v2/users' }], }, }], parent: [{ @@ -65,11 +65,11 @@ const collectionResponse = [ author: [{ id: 2, name: 'edygar', - link: 'http://km.nos.dev/author/edygar/', + link: 'http://dumb.url/wp-json/author/edygar/', slug: 'edygar', _links: { - self: [{ href: 'http://km.nos.dev/wp-json/wp/v2/users/2' }], - collection: [{ href: 'http://km.nos.dev/wp-json/wp/v2/users' }], + self: [{ href: 'http://dumb.url/wp-json/wp/v2/users/2' }], + collection: [{ href: 'http://dumb.url/wp-json/wp/v2/users' }], }, }], }, diff --git a/test/mocks/data/queryBySlugResponse.js b/test/mocks/data/queryBySlugResponse.js index 049de5a..1964c03 100644 --- a/test/mocks/data/queryBySlugResponse.js +++ b/test/mocks/data/queryBySlugResponse.js @@ -19,11 +19,11 @@ const queryBySlugResponse = [ author: [{ id: 2, name: 'edygar', - link: 'http://km.nos.dev/author/edygar/', + link: 'http://dumb.url/author/edygar/', slug: 'edygar', _links: { - self: [{ href: 'http://km.nos.dev/wp-json/wp/v2/users/2' }], - collection: [{ href: 'http://km.nos.dev/wp-json/wp/v2/users' }], + self: [{ href: 'http://dumb.url/wp-json/wp/v2/users/2' }], + collection: [{ href: 'http://dumb.url/wp-json/wp/v2/users' }], }, }], }, diff --git a/test/mocks/store.js b/test/mocks/store.js index 3038619..22a70a2 100644 --- a/test/mocks/store.js +++ b/test/mocks/store.js @@ -1,9 +1,7 @@ import { initialReducerState } from '../../src/ReduxWPAPI'; -const initialStore = { wp: initialReducerState }; -export const createFakeStore = (fakeData = initialStore) => ({ getState: () => fakeData }); - -// export const dispatchWithStoreOf = (storeData, action) => -// new Promise((resolve, reject) => { -// x -// }); +export const initialStore = { wp: initialReducerState }; +export const createFakeStore = (fakeData = initialStore) => ({ + state: fakeData, + getState() { return this.state; }, +}); From b50699e210042d2b7712ba6c5ae28b4e3849576a Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Thu, 15 Sep 2016 19:05:47 +0100 Subject: [PATCH 07/24] Document patch release --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef6ea47..c8ee467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ Some guidelines in reading this document: * Being that these are the early days of the repository, we have some code changes that were added directly and without much detail, for these we have a link to the commit instead of the PR. * Annotations starting with **[BC]** indicates breaking change. +## 1.0.2 [UNRELEASED] + +* Fix the Promise return from middleware dispatch, which should always resolve to selectQuery result + ## 1.0.1 * Fix selector, which was referring to `entity` instead `resource`. diff --git a/package.json b/package.json index c67e73c..6bccf12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux-wpapi", - "version": "1.0.1", + "version": "1.0.2", "description": "Wordpress integration Redux middleware based on node-wpapi.", "main": "lib/index.js", "files": [ From 140e72538dcf1226e9ade440cb97ef85d6d08959 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Fri, 16 Sep 2016 16:16:09 +0100 Subject: [PATCH 08/24] Conforms Changelog with guidelines --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ee467..cefa641 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Some guidelines in reading this document: * Being that these are the early days of the repository, we have some code changes that were added directly and without much detail, for these we have a link to the commit instead of the PR. * Annotations starting with **[BC]** indicates breaking change. -## 1.0.2 [UNRELEASED] +## [new release] * Fix the Promise return from middleware dispatch, which should always resolve to selectQuery result From 255c97e2e32598f9f01585ce87783296ac5328ae Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Fri, 16 Sep 2016 16:31:10 +0100 Subject: [PATCH 09/24] Symbols introduced --- src/ReduxWPAPI.js | 7 ++++--- src/constants/symbols.js | 2 ++ src/index.js | 1 + src/selectors.js | 2 ++ 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/constants/symbols.js diff --git a/src/ReduxWPAPI.js b/src/ReduxWPAPI.js index 5697a26..755f523 100644 --- a/src/ReduxWPAPI.js +++ b/src/ReduxWPAPI.js @@ -10,6 +10,7 @@ import isArray from 'lodash/isArray'; import isUndefined from 'lodash/isUndefined'; import { selectQuery } from './selectors'; import WPAPIAdapter from './adapters/wpapi'; +import { lastCacheUpdate as lastCacheUpdateSymbol } from './constants/symbols'; import { REDUX_WP_API_CALL, @@ -87,7 +88,7 @@ export default class ReduxWPAPI { } if (cache) { - lastCacheUpdate = cache.lastCacheUpdate; + lastCacheUpdate = cache[lastCacheUpdateSymbol]; } else { cache = state.getIn(['requestsByQuery', payload.cacheID, payload.page]); data = state.get('data'); @@ -221,7 +222,7 @@ export default class ReduxWPAPI { } const data = []; - const aditionalData = { lastCacheUpdate: requestState.responseAt }; + const aditionalData = { [lastCacheUpdateSymbol]: requestState.responseAt }; body.forEach(resource => { newState = this.indexResource(newState, aggregator, resource, aditionalData); @@ -329,9 +330,9 @@ export default class ReduxWPAPI { let resourceTransformed = { ...oldState, ...resource, + ...meta, _links, _embedded, - lastCacheUpdate: meta.lastCacheUpdate, }; if (this.adapter.transformResource) { diff --git a/src/constants/symbols.js b/src/constants/symbols.js new file mode 100644 index 0000000..974bce8 --- /dev/null +++ b/src/constants/symbols.js @@ -0,0 +1,2 @@ +export const id = Symbol('LocalID'); +export const lastCacheUpdate = Symbol('lastCacheUpdate'); diff --git a/src/index.js b/src/index.js index 183c08e..6315903 100644 --- a/src/index.js +++ b/src/index.js @@ -2,5 +2,6 @@ import ReduxWPAPI from './ReduxWPAPI'; export default ReduxWPAPI; export * as RequestStatus from './constants/requestStatus'; +export * as Symbols from './constants/symbols'; export callAPI from './actions/callAPI'; export { selectQuery } from './selectors'; diff --git a/src/selectors.js b/src/selectors.js index 3feb531..24049ca 100644 --- a/src/selectors.js +++ b/src/selectors.js @@ -1,6 +1,7 @@ import { createSelector } from 'reselect'; import { pending } from './constants/requestStatus'; import { mapDeep } from './helpers'; +import { id as idSymbol } from './constants/symbols'; export const denormalize = (resources, id, memoized = {}) => { /* eslint-disable no-param-reassign, no-underscore-dangle */ @@ -8,6 +9,7 @@ export const denormalize = (resources, id, memoized = {}) => { const resource = resources.get(id); memoized[id] = { + [idSymbol]: id, ...resource, ...mapDeep(resource._embedded || {}, embeddedId => denormalize(resources, embeddedId, memoized) From 00847c6560cf5f3c68ab8dab75019e3e0185ab86 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Fri, 16 Sep 2016 16:34:40 +0100 Subject: [PATCH 10/24] Symbols introduced --- src/ReduxWPAPI.js | 6 +++++- test/middleware.spec.js | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ReduxWPAPI.js b/src/ReduxWPAPI.js index 755f523..9bd8760 100644 --- a/src/ReduxWPAPI.js +++ b/src/ReduxWPAPI.js @@ -94,7 +94,11 @@ export default class ReduxWPAPI { data = state.get('data'); } if (cache && (localID || (isUndefined(localID) && !cache.get('error')))) { - lastCacheUpdate = lastCacheUpdate || cache.get('responseAt') || cache.get('requestAt'); + try { + lastCacheUpdate = lastCacheUpdate || cache.get('responseAt') || cache.get('requestAt'); + } catch (e) { + console.log(cache); // eslint-disable-line + } next({ meta, type: REDUX_WP_API_CACHE_HIT, diff --git a/test/middleware.spec.js b/test/middleware.spec.js index 659358e..791319d 100644 --- a/test/middleware.spec.js +++ b/test/middleware.spec.js @@ -14,6 +14,7 @@ import { createFakeStore } from './mocks/store'; import { initialReducerState } from '../src/ReduxWPAPI'; import { REDUX_WP_API_CALL, REDUX_WP_API_CACHE_HIT } from '../src/constants/actions'; import { resolved, rejected } from '../src/constants/requestStatus'; +import { lastCacheUpdate as lastCacheUpdateSymbol } from '../src/constants/symbols'; const createCallAPIActionFrom = ({ meta: { name }, @@ -33,10 +34,10 @@ const successfullQueryBySlugState = initialReducerState.set( new Immutable.List([ { ...successfullQueryBySlug.payload.response[0]._embedded.author[0], - lastCacheUpdate: Date.now() }, + [lastCacheUpdateSymbol]: Date.now() }, { ...successfullQueryBySlug.payload.response[0], - lastCacheUpdate: Date.now(), + [lastCacheUpdateSymbol]: Date.now(), _embedded: { author: 0 }, }, ]) From 86a4e7a6a3c54d39fc10c081db66499bc74a58cb Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Fri, 16 Sep 2016 19:13:07 +0100 Subject: [PATCH 11/24] withSelector on its way --- src/selectors.js | 10 ++++++++++ test/selector.spec.js | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/selectors.js b/src/selectors.js index 24049ca..67ef9e4 100644 --- a/src/selectors.js +++ b/src/selectors.js @@ -21,6 +21,16 @@ export const denormalize = (resources, id, memoized = {}) => { export const localResources = state => state.wp.getIn(['resources']); +export const withDenormalize = injectTo => + createSelector( + (...args) => args, + localResources, + ([state, props], resources) => { + const memo = {}; + return injectTo(id => denormalize(resources, id, memo))(state, props); + } + ); + export const selectQuery = name => createSelector( createSelector( state => state.wp.getIn(['requestsByName', name]), diff --git a/test/selector.spec.js b/test/selector.spec.js index f975549..ff20b64 100644 --- a/test/selector.spec.js +++ b/test/selector.spec.js @@ -2,10 +2,11 @@ import { describe, it } from 'mocha'; import expect from 'expect'; import Immutable from 'immutable'; import { initialReducerState } from '../src/ReduxWPAPI'; -import { selectQuery } from '../src/selectors'; +import { selectQuery, withDenormalize } from '../src/selectors'; import { pending, resolved } from '../src/constants/requestStatus'; +import { createSelector } from 'reselect'; -describe('Selector', () => { +describe('Selector selectQuery', () => { it('should return a Request for empty state', () => { const state = { wp: initialReducerState }; expect(selectQuery('test')(state)) @@ -141,3 +142,31 @@ describe('Selector', () => { }); }); + +describe('withDenormalize', () => { + it('should allow consumers to denormalize resources by local ids within selector', () => { + const resources = [ + { id: 1, + title: 'lol', + _links: { parent: { url: 'http://dumb.com/test/2' } }, + _embedded: { parent: 1 }, + }, + { id: 2, title: 'lol 2' }, + ]; + const storeState = { + wp: ( + initialReducerState + .setIn(['resources'], new Immutable.List(resources)) + ), + }; + + const selector = withDenormalize( + createSelector( + () => 1, + (id, denormalize) => denormalize(id) + ) + ); + const selectedState = selector(storeState); + expect(selectedState).toInclude(resources[1]); + }); +}); From 81744ccda2b3ed115bbca8bcae1c6f0c3b3252a7 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Sat, 17 Sep 2016 20:51:24 +0100 Subject: [PATCH 12/24] withDenormalize now provides `denormalize` func for a selector --- src/selectors.js | 13 +++++++++---- test/selector.spec.js | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/selectors.js b/src/selectors.js index 67ef9e4..cedc650 100644 --- a/src/selectors.js +++ b/src/selectors.js @@ -2,6 +2,7 @@ import { createSelector } from 'reselect'; import { pending } from './constants/requestStatus'; import { mapDeep } from './helpers'; import { id as idSymbol } from './constants/symbols'; +import isFunction from 'lodash/isFunction'; export const denormalize = (resources, id, memoized = {}) => { /* eslint-disable no-param-reassign, no-underscore-dangle */ @@ -21,13 +22,17 @@ export const denormalize = (resources, id, memoized = {}) => { export const localResources = state => state.wp.getIn(['resources']); -export const withDenormalize = injectTo => +export const withDenormalize = thunk => createSelector( - (...args) => args, localResources, - ([state, props], resources) => { + thunk, + (resources, target) => { + if (!isFunction(target)) { + return target; + } + const memo = {}; - return injectTo(id => denormalize(resources, id, memo))(state, props); + return target(id => denormalize(resources, id, memo)); } ); diff --git a/test/selector.spec.js b/test/selector.spec.js index ff20b64..f1774dd 100644 --- a/test/selector.spec.js +++ b/test/selector.spec.js @@ -163,7 +163,7 @@ describe('withDenormalize', () => { const selector = withDenormalize( createSelector( () => 1, - (id, denormalize) => denormalize(id) + id => denormalize => denormalize(id) ) ); const selectedState = selector(storeState); From 0c355466b5e9ffa7b1196045050f9096e293be1a Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Sat, 17 Sep 2016 20:56:42 +0100 Subject: [PATCH 13/24] Fix typos --- src/ReduxWPAPI.js | 4 +-- src/actions/callAPI.js | 4 +-- src/adapters/wpapi.js | 6 ++-- src/callAPI.js | 4 +-- test/middleware.spec.js | 26 +++++++------- ...ueryBySlug.js => successfulQueryBySlug.js} | 0 test/mocks/data/collectionResponse.js | 36 +++++++++---------- test/mocks/data/queryBySlugResponse.js | 14 ++++---- test/reducer.spec.js | 8 ++--- 9 files changed, 51 insertions(+), 51 deletions(-) rename test/mocks/actions/{successfullQueryBySlug.js => successfulQueryBySlug.js} (100%) diff --git a/src/ReduxWPAPI.js b/src/ReduxWPAPI.js index 5697a26..f46b965 100644 --- a/src/ReduxWPAPI.js +++ b/src/ReduxWPAPI.js @@ -221,10 +221,10 @@ export default class ReduxWPAPI { } const data = []; - const aditionalData = { lastCacheUpdate: requestState.responseAt }; + const additionalData = { lastCacheUpdate: requestState.responseAt }; body.forEach(resource => { - newState = this.indexResource(newState, aggregator, resource, aditionalData); + newState = this.indexResource(newState, aggregator, resource, additionalData); data.push(this.getResourceLocalID(newState, aggregator, resource)); }); diff --git a/src/actions/callAPI.js b/src/actions/callAPI.js index bbe7fe8..8e8c098 100644 --- a/src/actions/callAPI.js +++ b/src/actions/callAPI.js @@ -1,6 +1,6 @@ import { REDUX_WP_API_CALL } from '../constants/actions'; -export default (name, request, aditionalParams = {}) => ({ +export default (name, request, additionalParams = {}) => ({ type: REDUX_WP_API_CALL, - payload: { name, request, aditionalParams }, + payload: { name, request, additionalParams }, }); diff --git a/src/adapters/wpapi.js b/src/adapters/wpapi.js index 900e88c..08bd4b9 100644 --- a/src/adapters/wpapi.js +++ b/src/adapters/wpapi.js @@ -271,14 +271,14 @@ export default class WPAPIAdapter { * * @param {Object} payload The action payload * @param {Object} payload.request The lib consumer input for calling the api - * @param {Object} payload.aditionalParams Aditional params in order to make request, generally + * @param {Object} payload.additionalParams additional params in order to make request, generally * meta data such as method or header to be handled by * `callAPI` * @return {Object} The Request Object */ - buildRequest({ request: requestBuilder, aditionalParams }) { + buildRequest({ request: requestBuilder, additionalParams }) { const wpRequest = requestBuilder(this.api); - const { operation = 'get', ...body } = aditionalParams; + const { operation = 'get', ...body } = additionalParams; return { wpRequest, operation, body }; } diff --git a/src/callAPI.js b/src/callAPI.js index c009dd5..3670acf 100644 --- a/src/callAPI.js +++ b/src/callAPI.js @@ -1,6 +1,6 @@ import { REDUX_WP_API_CALL } from './constants/actions'; -export default (name, request, aditionalParams = {}) => ({ +export default (name, request, additionalParams = {}) => ({ type: REDUX_WP_API_CALL, - payload: { name, request, aditionalParams }, + payload: { name, request, additionalParams }, }); diff --git a/test/middleware.spec.js b/test/middleware.spec.js index 659358e..a3a7c3e 100644 --- a/test/middleware.spec.js +++ b/test/middleware.spec.js @@ -9,7 +9,7 @@ import createFakeAdapter from './mocks/createFakeAdapter'; import collectionRequest from './mocks/actions/collectionRequest'; import successfulCollectionRequest from './mocks/actions/successfulCollectionRequest'; import unsuccessfulCollectionRequest from './mocks/actions/unsuccessfulCollectionRequest'; -import successfullQueryBySlug from './mocks/actions/successfullQueryBySlug'; +import successfulQueryBySlug from './mocks/actions/successfulQueryBySlug'; import { createFakeStore } from './mocks/store'; import { initialReducerState } from '../src/ReduxWPAPI'; import { REDUX_WP_API_CALL, REDUX_WP_API_CACHE_HIT } from '../src/constants/actions'; @@ -24,18 +24,18 @@ const createCallAPIActionFrom = ({ payload: { name, request: { cacheID, page }, - aditionalParams: !Array.isArray(response) ? response : {}, + additionalParams: !Array.isArray(response) ? response : {}, }, }); -const successfullQueryBySlugState = initialReducerState.set( +const successfulQueryBySlugState = initialReducerState.set( 'resources', new Immutable.List([ { - ...successfullQueryBySlug.payload.response[0]._embedded.author[0], + ...successfulQueryBySlug.payload.response[0]._embedded.author[0], lastCacheUpdate: Date.now() }, { - ...successfullQueryBySlug.payload.response[0], + ...successfulQueryBySlug.payload.response[0], lastCacheUpdate: Date.now(), _embedded: { author: 0 }, }, @@ -51,14 +51,14 @@ const successfullQueryBySlugState = initialReducerState.set( }) ) .mergeIn( - ['requestsByName', successfullQueryBySlug.meta.name], + ['requestsByName', successfulQueryBySlug.meta.name], { - cacheID: successfullQueryBySlug.payload.cacheID, - page: successfullQueryBySlug.payload.page, + cacheID: successfulQueryBySlug.payload.cacheID, + page: successfulQueryBySlug.payload.page, } ) .mergeIn( - ['requestsByQuery', successfullQueryBySlug.payload.cacheID, successfullQueryBySlug.payload.page], + ['requestsByQuery', successfulQueryBySlug.payload.cacheID, successfulQueryBySlug.payload.page], { status: resolved, error: false, @@ -129,7 +129,7 @@ describe('Middleware', () => { it('should dispatch only CACHE_HIT action when request is cached within TTL', () => { const { middleware } = new ReduxWPAPI({ - adapter: createFakeAdapter(successfullQueryBySlug, { + adapter: createFakeAdapter(successfulQueryBySlug, { getTTL() { return Infinity; }, getIndexes() { return { slug: 'dumb1-modified' }; }, }), @@ -141,10 +141,10 @@ describe('Middleware', () => { const dispatched = []; const nextMiddlewareReturn = Symbol(); const fakeNext = dispatch => dispatched.push(dispatch) && nextMiddlewareReturn; - const action = createCallAPIActionFrom(successfullQueryBySlug); + const action = createCallAPIActionFrom(successfulQueryBySlug); // CACHED STATE - const fakeStore = createFakeStore({ wp: successfullQueryBySlugState }); + const fakeStore = createFakeStore({ wp: successfulQueryBySlugState }); const result = middleware(fakeStore)(fakeNext)(action); expect(result).toBeA(Promise); @@ -201,7 +201,7 @@ describe('Middleware', () => { const store = createFakeStore(); const fakeNext = dispatch => { dispatched.push(dispatch); - store.state = { wp: successfullQueryBySlugState }; + store.state = { wp: successfulQueryBySlugState }; }; const action = createCallAPIActionFrom(successfulCollectionRequest); const result = middleware(store)(fakeNext)(action); diff --git a/test/mocks/actions/successfullQueryBySlug.js b/test/mocks/actions/successfulQueryBySlug.js similarity index 100% rename from test/mocks/actions/successfullQueryBySlug.js rename to test/mocks/actions/successfulQueryBySlug.js diff --git a/test/mocks/data/collectionResponse.js b/test/mocks/data/collectionResponse.js index 969e3e0..c90c56f 100644 --- a/test/mocks/data/collectionResponse.js +++ b/test/mocks/data/collectionResponse.js @@ -4,26 +4,26 @@ const collectionResponse = [ slug: 'dumb2', dumbAttr: 'dumb2', _links: { - self: [{ href: 'http://dumb.url/wp-json/namespace/any/2' }], - collection: [{ href: 'http://dumb.url/wp-json/namespace/any' }], + self: [{ href: 'http://wordpress.dev/wp-json/namespace/any/2' }], + collection: [{ href: 'http://wordpress.dev/wp-json/namespace/any' }], parent: [{ embeddable: true, - href: 'http://dumb.url/wp-json/namespace/any/1', + href: 'http://wordpress.dev/wp-json/namespace/any/1', }], author: [{ embeddable: true, - href: 'http://dumb.url/wp-json/wp/v2/users/1', + href: 'http://wordpress.dev/wp-json/wp/v2/users/1', }], }, _embedded: { author: [{ id: 1, name: 'admin', - link: 'http://dumb.url/wp-json/author/admin/', + link: 'http://wordpress.dev/wp-json/author/admin/', slug: 'admin', _links: { - self: [{ href: 'http://dumb.url/wp-json/wp/v2/users/1' }], - collection: [{ href: 'http://dumb.url/wp-json/wp/v2/users' }], + self: [{ href: 'http://wordpress.dev/wp-json/wp/v2/users/1' }], + collection: [{ href: 'http://wordpress.dev/wp-json/wp/v2/users' }], }, }], parent: [{ @@ -31,15 +31,15 @@ const collectionResponse = [ slug: 'dumb1', dumbAttr: 'dumb1', _links: { - self: [{ href: 'http://dumb.url/wp-json/namespace/any/1' }], - collection: [{ href: 'http://dumb.url/wp-json/namespace/any' }], + self: [{ href: 'http://wordpress.dev/wp-json/namespace/any/1' }], + collection: [{ href: 'http://wordpress.dev/wp-json/namespace/any' }], parent: [{ embeddable: true, - href: 'http://dumb.url/wp-json/namespace/any/1', + href: 'http://wordpress.dev/wp-json/namespace/any/1', }], author: [{ embeddable: true, - href: 'http://dumb.url/wp-json/wp/v2/users/2', + href: 'http://wordpress.dev/wp-json/wp/v2/users/2', }], }, }], @@ -50,26 +50,26 @@ const collectionResponse = [ slug: 'dumb1', dumbAttr: 'dumb1', _links: { - self: [{ href: 'http://dumb.url/wp-json/namespace/any/1' }], - collection: [{ href: 'http://dumb.url/wp-json/namespace/any' }], + self: [{ href: 'http://wordpress.dev/wp-json/namespace/any/1' }], + collection: [{ href: 'http://wordpress.dev/wp-json/namespace/any' }], parent: [{ embeddable: true, - href: 'http://dumb.url/wp-json/namespace/any/1', + href: 'http://wordpress.dev/wp-json/namespace/any/1', }], author: [{ embeddable: true, - href: 'http://dumb.url/wp-json/wp/v2/users/2', + href: 'http://wordpress.dev/wp-json/wp/v2/users/2', }], }, _embedded: { author: [{ id: 2, name: 'edygar', - link: 'http://dumb.url/wp-json/author/edygar/', + link: 'http://wordpress.dev/wp-json/author/edygar/', slug: 'edygar', _links: { - self: [{ href: 'http://dumb.url/wp-json/wp/v2/users/2' }], - collection: [{ href: 'http://dumb.url/wp-json/wp/v2/users' }], + self: [{ href: 'http://wordpress.dev/wp-json/wp/v2/users/2' }], + collection: [{ href: 'http://wordpress.dev/wp-json/wp/v2/users' }], }, }], }, diff --git a/test/mocks/data/queryBySlugResponse.js b/test/mocks/data/queryBySlugResponse.js index 1964c03..1370ce8 100644 --- a/test/mocks/data/queryBySlugResponse.js +++ b/test/mocks/data/queryBySlugResponse.js @@ -4,26 +4,26 @@ const queryBySlugResponse = [ slug: 'dumb1-modified', dumbAttr: 'dumb1 - modified', _links: { - self: [{ href: 'http://dumb.url/wp-json/namespace/any/1' }], - collection: [{ href: 'http://dumb.url/wp-json/namespace/any' }], + self: [{ href: 'http://wordpress.dev/wp-json/namespace/any/1' }], + collection: [{ href: 'http://wordpress.dev/wp-json/namespace/any' }], parent: [{ embeddable: true, - href: 'http://dumb.url/wp-json/namespace/any/1', + href: 'http://wordpress.dev/wp-json/namespace/any/1', }], author: [{ embeddable: true, - href: 'http://dumb.url/wp-json/wp/v2/users/2', + href: 'http://wordpress.dev/wp-json/wp/v2/users/2', }], }, _embedded: { author: [{ id: 2, name: 'edygar', - link: 'http://dumb.url/author/edygar/', + link: 'http://wordpress.dev/author/edygar/', slug: 'edygar', _links: { - self: [{ href: 'http://dumb.url/wp-json/wp/v2/users/2' }], - collection: [{ href: 'http://dumb.url/wp-json/wp/v2/users' }], + self: [{ href: 'http://wordpress.dev/wp-json/wp/v2/users/2' }], + collection: [{ href: 'http://wordpress.dev/wp-json/wp/v2/users' }], }, }], }, diff --git a/test/reducer.spec.js b/test/reducer.spec.js index d7afb36..6f75d37 100644 --- a/test/reducer.spec.js +++ b/test/reducer.spec.js @@ -7,7 +7,7 @@ import { pending, resolved, rejected } from '../src/constants/requestStatus'; import collectionRequest from './mocks/actions/collectionRequest'; import modifyingRequest from './mocks/actions/modifyingRequest'; import successfulCollectionRequest from './mocks/actions/successfulCollectionRequest'; -import successfullQueryBySlug from './mocks/actions/successfullQueryBySlug'; +import successfulQueryBySlug from './mocks/actions/successfulQueryBySlug'; import unsuccessfulCollectionRequest from './mocks/actions/unsuccessfulCollectionRequest'; import unsuccessfulModifyingRequest from './mocks/actions/unsuccessfulModifyingRequest'; import cacheHitSingle from './mocks/actions/cacheHitSingle'; @@ -165,12 +165,12 @@ describe('Reducer', () => { it('should update previous resource\'s state', () => { const previous = reducer(undefined, successfulCollectionRequest); - const state = reducer(previous, successfullQueryBySlug); - const queryState = state.getIn(['requestsByQuery', successfullQueryBySlug.payload.cacheID]); + const state = reducer(previous, successfulQueryBySlug); + const queryState = state.getIn(['requestsByQuery', successfulQueryBySlug.payload.cacheID]); const [id] = queryState.getIn([1, 'data']); const resource = state.getIn(['resources', id]); expect(resource).toContain({ - link: successfullQueryBySlug.payload.response[0].link, + link: successfulQueryBySlug.payload.response[0].link, }); }); }); From 2e4f386aeec212c908cc61397ddcfd51bcc4b2f3 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Sat, 17 Sep 2016 21:14:57 +0100 Subject: [PATCH 14/24] localID -> resourceLocalID (conformant with adapter signature) --- src/ReduxWPAPI.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ReduxWPAPI.js b/src/ReduxWPAPI.js index f46b965..9429f52 100644 --- a/src/ReduxWPAPI.js +++ b/src/ReduxWPAPI.js @@ -76,14 +76,14 @@ export default class ReduxWPAPI { let data; const state = store.getState().wp; const indexes = this.adapter.getIndexes(request); - const localID = this.getResourceLocalID(state, meta.aggregator, indexes); + const resourceLocalID = this.getResourceLocalID(state, meta.aggregator, indexes); payload.cacheID = this.adapter.generateCacheID(request); payload.page = parseInt(this.adapter.getRequestedPage(request) || 1, 10); - if (localID) { - cache = state.getIn(['resources', localID]); - data = [localID]; + if (resourceLocalID) { + cache = state.getIn(['resources', resourceLocalID]); + data = [resourceLocalID]; } if (cache) { @@ -92,7 +92,7 @@ export default class ReduxWPAPI { cache = state.getIn(['requestsByQuery', payload.cacheID, payload.page]); data = state.get('data'); } - if (cache && (localID || (isUndefined(localID) && !cache.get('error')))) { + if (cache && (resourceLocalID || (isUndefined(resourceLocalID) && !cache.get('error')))) { lastCacheUpdate = lastCacheUpdate || cache.get('responseAt') || cache.get('requestAt'); next({ meta, @@ -285,10 +285,10 @@ export default class ReduxWPAPI { const _links = this.resolveAliases(resource._links, curies) || {}; delete _links.curies; - let localID = this.getResourceLocalID(state, aggregator, resource); + let resourceLocalID = this.getResourceLocalID(state, aggregator, resource); let oldState = {}; - if (!isUndefined(localID)) { - oldState = newState.getIn(['resources', localID]); + if (!isUndefined(resourceLocalID)) { + oldState = newState.getIn(['resources', resourceLocalID]); } if (resource._embedded) { @@ -322,8 +322,8 @@ export default class ReduxWPAPI { }); } - if (isUndefined(localID)) { - localID = newState.get('resources').size; + if (isUndefined(resourceLocalID)) { + resourceLocalID = newState.get('resources').size; } let resourceTransformed = { @@ -342,14 +342,14 @@ export default class ReduxWPAPI { resourceTransformed = this.settings.transformResource(resourceTransformed); } - newState = newState.setIn(['resources', localID], resourceTransformed); + newState = newState.setIn(['resources', resourceLocalID], resourceTransformed); const indexers = this.settings.customCacheIndexes[aggregator]; forEach(isArray(indexers) ? ['id'].concat(indexers) : ['id', indexers], indexer => { if (!isUndefined(resource[indexer])) { newState = newState.setIn( ['resourcesIndexes', aggregator, indexer, resource[indexer]], - localID + resourceLocalID ); } }); From a716e0b73f71d27defd93eeb4224fccac489e820 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Sat, 17 Sep 2016 21:28:04 +0100 Subject: [PATCH 15/24] Conforms better with adapter api --- src/constants/symbols.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/symbols.js b/src/constants/symbols.js index 974bce8..012fd54 100644 --- a/src/constants/symbols.js +++ b/src/constants/symbols.js @@ -1,2 +1,2 @@ -export const id = Symbol('LocalID'); +export const id = Symbol('ResourceLocalID'); export const lastCacheUpdate = Symbol('lastCacheUpdate'); From a04ab9493be18f650ab2716fe88a0c94dd7221d4 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Sun, 18 Sep 2016 01:49:09 +0100 Subject: [PATCH 16/24] Move symbols to root --- src/ReduxWPAPI.js | 2 +- src/index.js | 1 - src/selectors.js | 2 +- src/{constants => }/symbols.js | 0 test/middleware.spec.js | 2 +- 5 files changed, 3 insertions(+), 4 deletions(-) rename src/{constants => }/symbols.js (100%) diff --git a/src/ReduxWPAPI.js b/src/ReduxWPAPI.js index 30ea57e..04317fa 100644 --- a/src/ReduxWPAPI.js +++ b/src/ReduxWPAPI.js @@ -10,7 +10,7 @@ import isArray from 'lodash/isArray'; import isUndefined from 'lodash/isUndefined'; import { selectQuery } from './selectors'; import WPAPIAdapter from './adapters/wpapi'; -import { lastCacheUpdate as lastCacheUpdateSymbol } from './constants/symbols'; +import { lastCacheUpdate as lastCacheUpdateSymbol } from './symbols'; import { REDUX_WP_API_CALL, diff --git a/src/index.js b/src/index.js index 6315903..183c08e 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,5 @@ import ReduxWPAPI from './ReduxWPAPI'; export default ReduxWPAPI; export * as RequestStatus from './constants/requestStatus'; -export * as Symbols from './constants/symbols'; export callAPI from './actions/callAPI'; export { selectQuery } from './selectors'; diff --git a/src/selectors.js b/src/selectors.js index cedc650..52dc20b 100644 --- a/src/selectors.js +++ b/src/selectors.js @@ -1,7 +1,7 @@ import { createSelector } from 'reselect'; import { pending } from './constants/requestStatus'; import { mapDeep } from './helpers'; -import { id as idSymbol } from './constants/symbols'; +import { id as idSymbol } from './symbols'; import isFunction from 'lodash/isFunction'; export const denormalize = (resources, id, memoized = {}) => { diff --git a/src/constants/symbols.js b/src/symbols.js similarity index 100% rename from src/constants/symbols.js rename to src/symbols.js diff --git a/test/middleware.spec.js b/test/middleware.spec.js index f5c564b..4709213 100644 --- a/test/middleware.spec.js +++ b/test/middleware.spec.js @@ -14,7 +14,7 @@ import { createFakeStore } from './mocks/store'; import { initialReducerState } from '../src/ReduxWPAPI'; import { REDUX_WP_API_CALL, REDUX_WP_API_CACHE_HIT } from '../src/constants/actions'; import { resolved, rejected } from '../src/constants/requestStatus'; -import { lastCacheUpdate as lastCacheUpdateSymbol } from '../src/constants/symbols'; +import { lastCacheUpdate as lastCacheUpdateSymbol } from '../src/symbols'; const createCallAPIActionFrom = ({ meta: { name }, From b07cb925a867e0b8cc78cd039412b989bd83b40d Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Sun, 18 Sep 2016 01:53:45 +0100 Subject: [PATCH 17/24] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cefa641..56415ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Some guidelines in reading this document: ## [new release] +* Expose a denormalization mechanism so consumer can transform local ids into resources denormalized * Fix the Promise return from middleware dispatch, which should always resolve to selectQuery result ## 1.0.1 From 432923ebb96fa86a843b166452c4491dd3462670 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Sun, 18 Sep 2016 01:54:30 +0100 Subject: [PATCH 18/24] Regressing version (the version was mistakenly changed) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6bccf12..c67e73c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux-wpapi", - "version": "1.0.2", + "version": "1.0.1", "description": "Wordpress integration Redux middleware based on node-wpapi.", "main": "lib/index.js", "files": [ From b5b605603ca27356d7bac2a2579874656521f208 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Sun, 18 Sep 2016 02:00:00 +0100 Subject: [PATCH 19/24] Conforms with linter --- src/selectors.js | 3 ++- test/selector.spec.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/selectors.js b/src/selectors.js index 52dc20b..5574c9f 100644 --- a/src/selectors.js +++ b/src/selectors.js @@ -1,8 +1,9 @@ +import isFunction from 'lodash/isFunction'; import { createSelector } from 'reselect'; + import { pending } from './constants/requestStatus'; import { mapDeep } from './helpers'; import { id as idSymbol } from './symbols'; -import isFunction from 'lodash/isFunction'; export const denormalize = (resources, id, memoized = {}) => { /* eslint-disable no-param-reassign, no-underscore-dangle */ diff --git a/test/selector.spec.js b/test/selector.spec.js index f1774dd..d563b1b 100644 --- a/test/selector.spec.js +++ b/test/selector.spec.js @@ -1,10 +1,11 @@ import { describe, it } from 'mocha'; import expect from 'expect'; import Immutable from 'immutable'; +import { createSelector } from 'reselect'; + import { initialReducerState } from '../src/ReduxWPAPI'; import { selectQuery, withDenormalize } from '../src/selectors'; import { pending, resolved } from '../src/constants/requestStatus'; -import { createSelector } from 'reselect'; describe('Selector selectQuery', () => { it('should return a Request for empty state', () => { From 6fae2e0badf60b62ef4bc874209fcd8c5d2a7917 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Mon, 19 Sep 2016 11:23:14 +0100 Subject: [PATCH 20/24] Exports Symbols at root --- src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.js b/src/index.js index 183c08e..b3812ee 100644 --- a/src/index.js +++ b/src/index.js @@ -3,4 +3,5 @@ import ReduxWPAPI from './ReduxWPAPI'; export default ReduxWPAPI; export * as RequestStatus from './constants/requestStatus'; export callAPI from './actions/callAPI'; +export * as Symbols from './symbols'; export { selectQuery } from './selectors'; From e0abc57b1258d4fe10eea7f3d5e83a58164ae145 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Mon, 19 Sep 2016 11:29:45 +0100 Subject: [PATCH 21/24] Exposes withDenormalize --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index b3812ee..52e607a 100644 --- a/src/index.js +++ b/src/index.js @@ -4,4 +4,4 @@ export default ReduxWPAPI; export * as RequestStatus from './constants/requestStatus'; export callAPI from './actions/callAPI'; export * as Symbols from './symbols'; -export { selectQuery } from './selectors'; +export { selectQuery, withDenormalize } from './selectors'; From 30a01dac142b286a5945f9f10b8ddeef969fe83c Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Mon, 19 Sep 2016 11:39:01 +0100 Subject: [PATCH 22/24] Denormalize returns null when nothing is found --- src/selectors.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/selectors.js b/src/selectors.js index 5574c9f..29c598f 100644 --- a/src/selectors.js +++ b/src/selectors.js @@ -10,6 +10,10 @@ export const denormalize = (resources, id, memoized = {}) => { if (memoized[id]) return memoized[id]; const resource = resources.get(id); + if (!resource) { + return null; + } + memoized[id] = { [idSymbol]: id, ...resource, From d6ba9fd0d342b0e0def343beeb18c265d90a7c22 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Mon, 19 Sep 2016 14:37:07 +0100 Subject: [PATCH 23/24] Fix alignments, adds CHANGELOG PR reference --- CHANGELOG.md | 2 +- README.md | 2 +- src/adapters/wpapi.js | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cefa641..bc55cc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Some guidelines in reading this document: ## [new release] -* Fix the Promise return from middleware dispatch, which should always resolve to selectQuery result +* Fix the Promise return from middleware dispatch, which should always resolve to `selectQuery` result ([#9](https://github.com/log-oscon/redux-wpapi/pull/9)) ## 1.0.1 diff --git a/README.md b/README.md index fcc2a83..c677d36 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A [node-wpapi](https://github.com/WP-API/node-wpapi) integration for a Redux based Application. ## How it Works -This library exposes [node-wpapi](https://github.com/WP-API/node-wpapi) instance through the actionCreator [callAPI](#/src/actions/callAPI.js). The resulting +This library exposes [node-wpapi](https://github.com/WP-API/node-wpapi) instance through the actionCreator [callAPI](#/src/actions/callAPI.js). The resulting action is interpreted in [the middleware](#the-middleware), doing so by resolving the request and controlling the reducer through actions. ## Installation diff --git a/src/adapters/wpapi.js b/src/adapters/wpapi.js index 08bd4b9..de9f7ce 100644 --- a/src/adapters/wpapi.js +++ b/src/adapters/wpapi.js @@ -269,12 +269,12 @@ export default class WPAPIAdapter { * fetched. The request must carry at least operation (get|create|update\delete) and required data * in order to call API later at `callAPI`. * - * @param {Object} payload The action payload - * @param {Object} payload.request The lib consumer input for calling the api + * @param {Object} payload The action payload + * @param {Object} payload.request The lib consumer input for calling the api * @param {Object} payload.additionalParams additional params in order to make request, generally - * meta data such as method or header to be handled by - * `callAPI` - * @return {Object} The Request Object + * meta data such as method or header to be handled by + * `callAPI` + * @return {Object} The Request Object */ buildRequest({ request: requestBuilder, additionalParams }) { const wpRequest = requestBuilder(this.api); From 1b59f200c9fcc5f23e4b16be538c0c69817f75e1 Mon Sep 17 00:00:00 2001 From: Edygar de Lima Oliveira Date: Mon, 19 Sep 2016 14:39:44 +0100 Subject: [PATCH 24/24] Add PR reference --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d1e1b6..b058b81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Some guidelines in reading this document: ## [new release] -* Expose a denormalization mechanism so consumer can transform local ids into resources denormalized +* Expose a denormalization mechanism so consumer can transform local ids into resources denormalized ([#11](https://github.com/log-oscon/redux-wpapi/pull/11)) * Fix the Promise return from middleware dispatch, which should always resolve to `selectQuery` result ([#9](https://github.com/log-oscon/redux-wpapi/pull/9)) ## 1.0.1