Skip to content

Commit

Permalink
update lists store and tests for using immutable state.
Browse files Browse the repository at this point in the history
- also adds getEntitiesByIds selector and resolver.
  • Loading branch information
nerrad committed Jan 1, 2019
1 parent 380b17d commit ec9a546
Show file tree
Hide file tree
Showing 12 changed files with 650 additions and 363 deletions.
4 changes: 2 additions & 2 deletions assets/src/data/eventespresso/lists/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ export function receiveResponse( identifier, queryString, items = [] ) {
*
* @param {string} modelName
* @param {string} queryString
* @param {Map<number,Object>}entities
* @param {Array<BaseEntity>}entities
* @return {{type: string, identifier: string, queryString: string, items:
* Map<number,Object>}} An action object.
* Array<BaseEntity>}} An action object.
*/
export function receiveEntityResponse(
modelName,
Expand Down
2 changes: 1 addition & 1 deletion assets/src/data/eventespresso/lists/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import reducer from './reducers';
import * as selectors from './selectors';
import * as actions from './actions';
import * as resolvers from './resolvers';
import { createEntitySelectors, createEntityResolvers } from './entities';
import { createEntitySelectors, createEntityResolvers } from './model';
import { REDUCER_KEY } from './constants';
import controls from '../base-controls';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export const createEntitySelectors = ( source ) => MODEL_NAMES.reduce(
state,
queryString,
) => source.getEntities( state, modelName, queryString );
selectors[
getMethodName( modelName, 'byIds', 'get', true )
] = (
state,
ids = [],
) => source.getEntitiesByIds( state, modelName, ids );
selectors[ getMethodName( modelName, '', 'isRequesting', true ) ] = (
state,
queryString,
Expand All @@ -45,6 +51,9 @@ export const createEntityResolvers = ( source ) => MODEL_NAMES.reduce(
resolvers[ getMethodName( modelName, '', 'get', true ) ] = (
queryString
) => source.getEntities( modelName, queryString );
resolvers[ getMethodName( modelName, 'byIds', 'get', true ) ] = (
ids
) => source.getEntitiesByIds( modelName, ids );
return resolvers;
},
{},
Expand Down
83 changes: 22 additions & 61 deletions assets/src/data/eventespresso/lists/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,80 +6,41 @@ import { DEFAULT_LISTS_STATE } from '../../model';
/**
* External dependencies
*/
import isShallowEqual from '@wordpress/is-shallow-equal';

/**
* Returns whether the state matches the provided items.
* @param {Object} state
* @param {string} identifier
* @param {string} queryString
* @param {Array} items
* @return {boolean} If the items are in state and they match, then true.
*/
const stateMatchesItems = ( state, identifier, queryString, items = [] ) => (
state[ identifier ] &&
state[ identifier ][ queryString ] &&
isShallowEqual( state[ identifier ][ queryString ], items )
);

/**
* Returns whether there is a match for incoming entities against what is
* currently in state. The match is performed against the keys (ids) for the
* entities
*
* @param {Object} state
* @param {string} modelName
* @param {string} queryString
* @param { Map<number|string,Object>} entities
* @return {boolean} If the incoming entity keys match the entity keys currently
* in the state for the given queryString and modelName then this returns true.
*/
const stateMatchesEntities = (
state,
modelName,
queryString,
entities = new Map()
) => (
state[ modelName ] &&
state[ modelName ][ queryString ] &&
isShallowEqual(
Array.from( state[ modelName ][ queryString ].keys() ),
Array.from( entities.keys() ),
)
);
import { fromJS, Set, OrderedMap } from 'immutable';

/**
* Reducer managing item list state.
*
* @param {Object} state Current state.
* @param {Map} state Current state.
* @param {Object} action Dispatched action.
* @return {Object} Updated state.
* @return {Map} Updated state.
*/
export function receiveListItems( state = DEFAULT_LISTS_STATE, action ) {
const { type, identifier, queryString, items = {} } = action;
let matcher;
export function receiveListItems(
state = fromJS( DEFAULT_LISTS_STATE ),
action
) {
const { type, identifier, queryString } = action;
const path = [ identifier, queryString ];
let { items } = action;
let doUpdate = true,
existingValues;
switch ( type ) {
case 'RECEIVE_LIST':
matcher = stateMatchesItems;
existingValues = state.getIn( path ) || Set();
items = existingValues.merge( items );
break;
case 'RECEIVE_ENTITY_LIST':
matcher = stateMatchesEntities;
existingValues = state.getIn( path ) || OrderedMap();
items = existingValues.merge(
items.map( entity => [ entity.id, entity ] )
);
break;
default :
matcher = null;
}
if ( matcher !== null ) {
return matcher( state, identifier, queryString, items ) ?
state :
{
...state,
[ identifier ]: {
...state[ identifier ],
[ queryString ]: items,
},
};
doUpdate = false;
}
return state;
return doUpdate ?
state.setIn( [ identifier, queryString ], items ) :
state;
}

export default receiveListItems;
91 changes: 62 additions & 29 deletions assets/src/data/eventespresso/lists/resolvers.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/**
* External imports
*/
import { isEmpty } from 'lodash';
import { isEmpty, find } from 'lodash';
import {
applyQueryString,
keyEntitiesByPrimaryKeyValue,
createAndKeyEntitiesByPrimaryKeyValue,
getPrimaryKeyQueryString,
} from '@eventespresso/model';

/**
Expand All @@ -17,7 +16,6 @@ import {
getFactoryByModel,
resolveGetEntityByIdForIds,
} from '../base-resolvers.js';
import { keepExistingEntitiesInObject } from '../base-model';
import { REDUCER_KEY as CORE_REDUCER_KEY } from '../core/constants';

/**
Expand All @@ -35,43 +33,39 @@ export function* getItems( identifier, queryString ) {
}

/**
* Resolver for model entities returned from an endpoint.
* Utility for handling an entity response and constructing BaseEntity
* children from them.
*
* Note, this uses the entities stored in the eventespresso/core store as the
* authority so if an entity already exists there, it replaces what was
* retrieved from the server.
*
* @param {string} modelName
* @param {string} queryString
* @return {void} if there are not entities retrieved from the endpoint.
* @param {Array} response
* @return {IterableIterator<*>|Array<BaseEntity>} An empty array if the
* factory cannot be retrieved for the model. Otherwise the constructed
* entities.
*/
export function* getEntities( modelName, queryString ) {
let response = yield fetch( {
path: applyQueryString( modelName, queryString ),
} );
if ( isEmpty( response ) ) {
return;
}
response = keyEntitiesByPrimaryKeyValue( modelName, response );

export function* buildAndDispatchEntitiesFromResponse( modelName, response ) {
const factory = yield getFactoryByModel( modelName );
if ( isEmpty( factory ) ) {
return;
return [];
}
let fullEntities = createAndKeyEntitiesByPrimaryKeyValue(
factory,
response,
);

const entityIds = Array.from( fullEntities.keys() );

let fullEntities = response.map( entity => factory.fromExisting( entity ) );
const entityIds = fullEntities.map( entity => entity.id );
// are there already entities for the ids in the store? If so, we use those
const existingEntities = yield select(
CORE_REDUCER_KEY,
'getEntitiesByIds',
modelName,
entityIds
);

if ( ! isEmpty( existingEntities ) ) {
fullEntities = keepExistingEntitiesInObject(
existingEntities,
fullEntities,
);
fullEntities = fullEntities.map( ( entity ) => {
return find( existingEntities, existingEntity => {
return existingEntity.id === entity.id;
} ) || entity;
} );
}
yield dispatch(
CORE_REDUCER_KEY,
Expand All @@ -80,5 +74,44 @@ export function* getEntities( modelName, queryString ) {
fullEntities
);
yield resolveGetEntityByIdForIds( modelName, entityIds );
return fullEntities;
}

/**
* Resolver for model entities returned from an endpoint.
* @param {string} modelName
* @param {string} queryString
* @return {IterableIterator<*>|Array<BaseEntity>} An empty array if no
* entities retrieved.
*/
export function* getEntities( modelName, queryString ) {
const response = yield fetch( {
path: applyQueryString( modelName, queryString ),
} );
if ( isEmpty( response ) ) {
return [];
}
const fullEntities = yield buildAndDispatchEntitiesFromResponse( modelName, response );
yield receiveEntityResponse( modelName, queryString, fullEntities );
}

/**
* Resolver for getting model entities for a given set of ids
* @param {string} modelName
* @param {Array<number>}ids
* @return {IterableIterator<*>|Array} An empty array if no entities retrieved.
*/
export function* getEntitiesByIds( modelName, ids = [] ) {
const queryString = getPrimaryKeyQueryString( modelName, ids );
const response = yield fetch( {
path: applyQueryString(
modelName,
queryString
),
} );
if ( isEmpty( response ) ) {
return [];
}
const fullEntities = yield buildAndDispatchEntitiesFromResponse( modelName, response );
yield receiveEntityResponse( modelName, queryString, fullEntities );
}
Loading

0 comments on commit ec9a546

Please sign in to comment.