import compose from 'redux/lib/compose';
import { setEntityViewRef } from '../../entities/entityViewActions';
import { apiGet } from './apiRequest';
import { setEntity } from '../../entities/entityActions';
import getDefaultApiEntityTransform from './getDefaultApiEntityTransform';
/**
* @module apiGetEntity
* @tutorial get-single-entity
*/
/**
* Default caching for apiGetEntity. If the requested entity already exists (in any form), will
* abort the call.
* @function entityCachingDefault
* @param {object} [entity] The entity that already exists in state, if it exists
* @returns {boolean} False if the caching is valid and the call should be aborted
*/
const entityCachingDefault = entity => !entity;
/**
* Performs an API request to get a single entity. Checks if the given entity type already
* exists in the state. If so, this action will skip the request and resolve immediately (unless
* the _useCache_ parameter says otherwise). When the API responds, will automatically put
* the response on the relevant redux state.
*
* @function apiGetEntity
* @param {string} actionType The 'type' property of the dispatched api request action is set
* to this value
* @param {string} gatewayType The type of gateway to use. These should be one of the strings
* defined in _Injectables.js_
* @param {string} path The path to the endpoint to request
* @param {string} entityType The type of entity we expect to retrieve from the API
* @param {string|object} entityId Either of the following:
* - The string id of the entity we expect to retrieve from the API
* - An object with the shape `{ findEntity: function, getId: function }`. The `getId` should
* get the id from the new entity. The `findEntity` is used to lookup if the entity already
* exists in state. It receives a current entity and should return `true` if and only if the
* entity matches the target entity.
* @param {object} options Object with optional options
* @param {object} [options.requestData=null] Object with data to send with the API request
* @param {object} [options.requestOptions={}] Object with additional options passed to the gateway.
* Please see gateway documentation for more info
* @param {string|object} [options.updateEntityView] If set, will update a entityViewReducer
* to match the entityId and entityType passed to this action. Can be a string path to the
* entityViewReducer, or an object with the following properties:
* - `target` Path to the entityViewReducer
* - `immediateUpdate` If true, will perform the update immediately, even if the request has
* not yet completed. Defaults to `true`. Ignored if `entityId` is an object
* @param {boolean|function} [options.caching=entityCachingDefault] A function that determines if
* there is still a valid cached entity for the requested type and id.
* Takes the following parameters:
* - `entity` An entity in the existing redux state (if it exists)
* - `entityType` The entityType passed to this call
* - `entityId` The entityId passed to this call
* Should return false if the call should be aborted, true if the request should be executed
* normally.
* @returns {Promise} A Promise that resolves with an object of the following shape:
* - *entity* The entity data
* - *fromCache* `true` if the entity data was pulled from cache, false if it comes from a new
* api response
* @category api-calls
*/
const apiGetEntity = (
actionType,
gatewayType,
path,
entityType,
entityId,
{
requestData = null,
requestOptions = {},
caching = entityCachingDefault,
updateEntityView = null,
transformResponse = null,
transformEntity,
mergeExisting = true,
} = {},
) => (dispatch, getState) => {
const dynamicEntityId = typeof entityId === 'object';
const requiredParams = { actionType, gatewayType, path, entityId, entityType };
Object.entries(requiredParams).forEach(([name, value]) => {
if (value === undefined) {
const definedParams = Object.entries(requiredParams)
.filter(([, definedValue]) => !!definedValue)
.map(([definedName]) => definedName)
.join(', ');
throw new ReferenceError(
`Expected ${name} to be passed to apiGetEntity, but "undefined" was passed. (${definedParams})`,
);
}
});
let targetEntityView = updateEntityView;
let immediateUpdateEntityView = true;
if (typeof updateEntityView === 'object' && updateEntityView !== null) {
immediateUpdateEntityView = updateEntityView.immediateUpdate;
targetEntityView = updateEntityView.target;
}
// we cannot immediately update the view if the entity id is dynamic
immediateUpdateEntityView = immediateUpdateEntityView && !dynamicEntityId;
if (targetEntityView && immediateUpdateEntityView) {
dispatch(setEntityViewRef(targetEntityView, { id: entityId, type: entityType }));
}
const { entities } = getState();
const entityTypeState = entities[entityType] || {};
let existingEntity;
if (dynamicEntityId) {
if (typeof entityId.getId !== 'function' || typeof entityId.findEntity !== 'function') {
throw new ReferenceError(
'Unexpected object passed to entityId param of apiGetEntity. Expected shape { findEntity: function, getId: function }',
);
}
existingEntity = Object.values(entityTypeState).find(entityId.findEntity);
} else {
existingEntity = entityTypeState[entityId];
}
const shouldExecuteRequest = caching ? caching(existingEntity, entityType, entityId) : true;
const requestPromise = shouldExecuteRequest
? dispatch(apiGet(actionType, gatewayType, path, requestData, requestOptions))
: Promise.resolve(false);
return requestPromise.then(rawResult => {
if (rawResult) {
if (transformResponse && typeof transformResponse !== 'function') {
throw new ReferenceError('Expected the transformResponse is a function');
}
const result = transformResponse ? transformResponse(rawResult) : rawResult;
if (!result.data) {
throw new ReferenceError('Expected the API response to have a data key');
}
const defaultApiEntityTransform = getDefaultApiEntityTransform(path, actionType);
const transformer = compose(...[transformEntity, defaultApiEntityTransform].filter(_ => _));
const newEntity = transformer(result.data);
const newEntityId = dynamicEntityId ? entityId.getId(newEntity) : entityId;
dispatch(setEntity(entityType, newEntityId || newEntity.id, newEntity, mergeExisting));
if (targetEntityView && !immediateUpdateEntityView) {
dispatch(setEntityViewRef(targetEntityView, { id: newEntityId, type: entityType }));
}
return { entity: result.data, fromCache: false };
}
if (targetEntityView && !immediateUpdateEntityView) {
const newEntityId = dynamicEntityId ? entityId.getId(existingEntity) : entityId;
dispatch(setEntityViewRef(targetEntityView, { id: newEntityId, type: entityType }));
}
return { entity: existingEntity, fromCache: true };
});
};
export default apiGetEntity;