source/utils/reduxApiUtilities.js
import { Map as ImmutableMap, OrderedMap as ImmutableOrderedMap, List as ImmutableList } from 'immutable'
import LoggerSingleton from 'ievv_jsbase/lib/log/LoggerSingleton'
const logger = new LoggerSingleton().getLogger('churchill_js.utilities.reduxApiUtilities')
/**
* Util used by all `action.js`-files in `redux/api/*`.
* This is used to mark an entry in a redux-map as loading.
*
* sets `isLoading` to `true` and `apiError` to null.
*
* @param oldApiDataMap Previous contents of `data` in the redux-map in question
* @return {Immutable.Map<string, any>} The new Map to insert as entry in the redux-map being updated.
*/
export function makeApiDataMapIsLoading (oldApiDataMap = null) {
return new ImmutableMap({
isLoading: true,
isDeleted: false,
apiError: null,
data: oldApiDataMap === null ? new ImmutableMap() : oldApiDataMap.get('data')
})
}
/**
* Util used by all `action.js`-files in `redux/api/*`.
* This is used to format data to store in redux-state-map when an api-call is successful.
*
* The given `newData` is inserted as an ImmutableMap `data`.
*
* sets `isLoading` to `false`, `apiError` to null and `timestamp` to now.
*
* @param newData {Object} the object returned from the api which should be formated for insertion in the
* redux-state-map. If fromList is true this should be an iterable.
* @param fromList {boolean} if true, make an ImmutableList of the given data instead of ImmutableMap
* @param noWrap {boolean} if true, newData will not be wrapped in ImmutableMap or ImmutableList. Only use this if the
* data you are setting is already an ImmutableMap or ImmutableList!
* @return {Immutable.Map} newly formatted ImmutableMap ready to be placed in redux-state-map
*/
export function makeApiDataMapNewData (newData, fromList = false, noWrap = false) {
const getData = () => {
if (noWrap) {
return newData
}
if (fromList) {
return new ImmutableList(newData)
}
return new ImmutableMap(newData)
}
return new ImmutableMap({
isLoading: false,
isDeleted: false,
apiError: null,
data: getData(),
timestamp: new Date()
})
}
/**
* Util used by all `action.js`-files in `redux/api/*`.
*
* This is used to build a common error-format for all redux-map-entries.
* sets `isLoading` to `false`, `data` to `null`, `timestamp` to now.
*
* stores the error in the field `apiError` formatted like this:
*
* apiError: ImmutableMap({
* status: error.response.status,
* bodydata: ImmutableMap(error.response.bodydata)
* })
*
* @param error
* @return {Immutable.Map}
*/
export function makeApiDataMapError (error) {
logger.warning('makeApiDataMapError: got error: ', error)
return new ImmutableMap({
isLoading: false,
isDeleted: false,
apiError: new ImmutableMap({
status: error.response.status,
bodydata: new ImmutableMap(error.response.bodydata)
}),
data: null,
timestamp: new Date()
})
}
/**
* Used to mark an object in redux-state as deleted.
* @param oldData
* @return {Immutable.Map}
*/
export function makeApiDataMapDeleted (oldData = new ImmutableMap()) {
return new ImmutableMap({
isLoading: false,
isDeleted: true,
apiError: null,
data: null,
timestamp: new Date(),
oldData
})
}
/**
* Simple util to fetch data from a redux-api-map (which should always be built by {@link makeApiDataMapIsLoading},
* {@link makeApiDataMapNewData} or {@link makeApiDataMapError}).
*
* This util will lookup the given id in the given map and return it if it exists.
* If the entry is missing or loading, null is returned.
* If the entry is missing (and not already loading), the given `dispatchAction` is dispatched and null is returned.
*
* Note that this util will still return the object even if it is an error-object (`apiError !== null`), so you need
* to handle errors in your component.
*
* @param map {Immutable.Map} a map built by {@link makeApiDataMapIsLoading}, {@link makeApiDataMapNewData} or
* {@link makeApiDataMapError}
* @param id {number} the entry-id to look for in the map
* @param dispatchAction {function} the `redux.api...actions.<action>` to dispatch to load the entry if missing
* @param dispatch {function} a `redux-thunk` dispatch, like `this.props.dispatch` from a connected React.Component.
* @return {null|Immutable.Map} null if the entry is loading, or an ImmutableMap of the entry if it exists.
*/
export function getObjectFromReduxMapOrNullIfLoading (map, id, dispatchAction, dispatch) {
if (id === null || id === undefined) {
throw new Error(`Cannot do a map-lookup with invalid key: "${id}"`)
}
const element = map.get(id, null)
if (element === null) {
dispatch(dispatchAction(id))
return null
}
if (element.get('isLoading', true)) {
return null
}
return element
}
/**
* Used for the internal OrderedMap from {@link makeTypeMappedReduxMapFromApiData}, and should be used for other
* type-lists from api.
*
* The only required fieldname on the api-data is `value`, which will be used as key in the ImmutableOrderedMap
*
* Builds a structure like this:
* ImmutableOrderedMap({
* <valueKey>: ImmutableMap({
* label: <some label>,
* description: <some description>,
* <otherfield>: <otherfieldkey>,
* ...
* })
* })
*
* from input-data formatted like this:
* [
* {
* "value": "<valueKey>",
* "label": "<some label>",
* "description": "<some description>",
* <otherfield>: <otherfieldkey>,
* ...
* }
* ]
*
* @param valueArray See input-data doc above
* @param valueKey default to 'value' as in example above. can be set to any valid value. note that this is NOT
* validated or enforced in any way
* @return {Immutable.OrderedMap<string, Immutable.Map<string, string>>} see example above
*/
export function makeImmutableOrderedMapFromValueArray (valueArray, valueKey = 'value') {
return new ImmutableOrderedMap()
.withMutations((newValuesForNodeTypeMap) => {
for (const valueObject of valueArray) {
// const {value, ...object} = valueObject
// newValuesForNodeTypeMap.set(value, new ImmutableMap(object))
const value = valueObject[valueKey]
delete valueObject[valueKey]
newValuesForNodeTypeMap.set(value, new ImmutableMap(valueObject))
}
})
}
/**
* Used for apis like nodePaymentSubscriptionTypes and nodeMembershipTypes.
*
* Builds a structure like this:
* ImmutableMap({
* <nodeType>: ImmutableOrderedMap({
* <valueKey>: ImmutableMap({
* label: <some label>,
* description: <some description>,
* <otherfield>: <otherfieldkey>,
* ...
* })
* })
* })
*
* Expects apiResponseBodydata to be formatted like this:
* {
* "<nodeType>": [
* {
* "value": "<valueKey>",
* "label": "<some label>",
* "description": "<some description>",
* <otherfield>: <otherfieldkey>,
* ...
* }
* ]
* }
*
* @param apiResponseBodydata `bodydata` from a response. This response has to be formatted correctly.
* @return {Immutable.Map<string, Immutable.OrderedMap<string, Immutable.Map<string, string>>>} See example in doc above
*/
export function makeTypeMappedReduxMapFromApiData (apiResponseBodydata) {
return new ImmutableMap().withMutations((data) => {
for (const [typeKey, valuesForTypeKey] of Object.entries(apiResponseBodydata)) {
data.set(typeKey, makeImmutableOrderedMapFromValueArray(valuesForTypeKey))
}
})
}
/**
* The default key for maps built by {@link makeTypeMappedReduxMapFromApiData}.
* This should be the same key used by the server-side apis, so do not change it unless you know what you are doing.
*
* @type {string}
*/
export const TYPE_MAPPED_REDUX_MAP_DEFAULT_KEY = '__default__'
/**
* Used to fetch the ImmutableOrderedMap of values for a <typeKey> in a structure built by
* {@link makeTypeMappedReduxMapFromApiData}. See its documentation for structure.
*
* If the given <typeKey> is not present in the map, then the entry referenced by
* {@link TYPE_MAPPED_REDUX_MAP_DEFAULT_KEY} will be returned instead.
*
* @param typeMappedReduxMap a map built by {@link makeTypeMappedReduxMapFromApiData}
* @param typeKey the <typeKey> to look for in the map
* @param fallbackToDefault if true return default-data if no data for typekey is present. If false, return null.
* @return {Immutable.OrderedMap<string, Immutable.Map<string, string>>} The ImmutableOrderedMap referenced by the
* given <typeKey>
*/
export function getValuesFromTypeMappedReduxMap (typeMappedReduxMap, typeKey, fallbackToDefault = true) {
if (typeMappedReduxMap.get('isLoading')) {
return null
}
const typeMappedValue = typeMappedReduxMap.getIn(['data', typeKey], null)
if (typeMappedValue !== null) {
return typeMappedValue
}
if (fallbackToDefault) {
return typeMappedReduxMap.getIn(['data', TYPE_MAPPED_REDUX_MAP_DEFAULT_KEY])
}
return null
}
/**
* Used to ensure that a map built by {@link makeTypeMappedReduxMapFromApiData} or
* {@link makeImmutableOrderedMapFromValueArray} is loaded in redux-store
*
* Note that this should only be used in cases where the data is static (e.g. load once, use always), as it does
* not pass any params to the api or check if any specific data is present. This simply triggers the given
* action if the map is empty and not already loading.
*
* @param typeMap The redux-store-map you want to ensure is loaded (e.g. userNotificationTypesMap)
* @param dispatchAction the redux-action to dispatch to load the typemap
* @param dispatch The dispatcher to use to dispatch the dispatchAction, e.g. props.dispatch
* @return {boolean} true if the given map is loaded, false if not.
*/
export function ensureMapHasDataInReduxStore (typeMap, dispatchAction, dispatch) {
if (!typeMap.get('data').size > 0) {
if (!typeMap.get('isLoading', false)) {
dispatch(dispatchAction())
}
return false
}
return true
}