Source: app/selectors/collectionSelector.js

/* eslint-disable import/prefer-default-export */
import { createSelector } from 'reselect';
import uniq from 'lodash/uniq';
import pick from 'lodash/pick';
import { PAGINATION_NONE } from '../data/paginationType';
import createShallowEqualSelector from '../util/reducers/createShallowEqualSelector';

/**
 * Contains the function
 * {@link module:collectionSelector~makeCollectionSelector|makeCollectionSelector}
 * that can be used to select all the entities in a collection from redux state.
 * @module collectionSelector
 * @category redux-state
 */

/**
 * Result returned by a `collectionSelector`
 * @typedef CollectionSelectorResult
 * @property entities {Object[]} An array of entity data, if found in the `entitiesSelector`
 * @property entityRefs {Object[]} An array of entities in the collection
 * @property entityRefs[].id {string} The id of the entity
 * @property entityRefs[].type {string} The type of the entity
 * @property entityRefs[].data {*} The entity data, if found in the `entitiesSelector`. Note that
 * this is the same object as referenced in the `entities` array, but it is also provided here
 * for convenience
 * @property pagination {object} Object containing data pagination info
 * @property pagination.type {string} One of the pagination types defined in the
 * {@link module:paginationType|paginationType} module
 * @property pagination.total {number} The total number of entities available on the server. **This
 * may be smaller than the amount of entities currently loaded!**
 * @property pagination.offset {number} _(offset type only)_ The offset of the first
 * entity of the `entities` array.
 * @property pagination.atBegin {boolean} _(partition type only)_ `true` if we know that there are
 * no more entities on the server that come before the `entities` array
 * @property pagination.atEnd {boolean} _(partition type only)_ `true` if we know that there are
 * no more entities on the server that come after the `entities` array
 */

/**
 * Selector returned by
 * {@link module:collectionSelector~makeCollectionSelector|makeCollectionSelector}
 * @callback collectionSelector
 * @param state {Object} The current redux state
 * @param props {Object}
 * @param props.collectionId {string} The id of the collection to get. Should be one of the ids
 * defined in {@link module:collectionIds|collectionIds}
 * @returns {CollectionSelectorResult} An object containing the selector data. See
 * {@link module:collectionSelector~CollectionSelectorResult|CollectionSelectorResult}
 */

/**
 * Create a const empty result to make sure the reference doesn't change every time
 * @type {Array}
 */
const EMPTY_ARRAY = [];

/**
 * Empty result to return when collection is not defined
 * @private
 */
const defaultCollectionSelectorResult = {
  entities: EMPTY_ARRAY,
  entityRefs: EMPTY_ARRAY,
  pagination: {
    type: PAGINATION_NONE,
  },
};

const makeCollectionTypesSelector = () =>
  createSelector(
    (state, { collectionId }) => state.collections[collectionId],
    collection => (collection ? uniq(collection.refs.map(ref => ref.type)) : EMPTY_ARRAY),
  );

const makeEntityStateByTypeSelector = () =>
  createSelector(
    state => state.entities,
    makeCollectionTypesSelector(),
    (entities, collectionTypes) => pick(entities, collectionTypes),
  );

const makeCollectionDataSelector = () =>
  createShallowEqualSelector(
    makeEntityStateByTypeSelector(),
    (state, { collectionId }) =>
      state.collections[collectionId] && state.collections[collectionId].refs,
    (entitiesByType, collectionRefs) => {
      if (!collectionRefs || !collectionRefs.length) {
        return EMPTY_ARRAY;
      }

      return collectionRefs.map(ref =>
        ref ? (entitiesByType[ref.type] && entitiesByType[ref.type][ref.id]) || null : null,
      );
    },
  );

const makeCollectionDataEnhancerSelector = () =>
  createShallowEqualSelector(
    makeCollectionDataSelector(),
    (state, { collectionId }) =>
      state.collections[collectionId] && state.collections[collectionId].refs,
    (entities, collectionRefs) => {
      if (!collectionRefs || !collectionRefs.length) {
        return {
          entities: EMPTY_ARRAY,
          entityRefs: EMPTY_ARRAY,
        };
      }

      return {
        entities,
        entityRefs: collectionRefs.map((ref, index) =>
          ref
            ? {
                data: entities[index],
                ...ref,
              }
            : ref,
        ),
      };
    },
  );

/**
 * Returns a new {@link https://github.com/reactjs/reselect|reselect} selector that can be used
 * to get all the entities referenced in a collection.
 * @function makeCollectionSelector
 * @returns {collectionSelector} The new selector. See
 * {@link module:collectionSelector~collectionSelector|collectionSelector}
 * @example const connector = connect(
 *   () => {
 *    const collectionSelector = makeCollectionSelector();
 *
 *    return (state, { discussionId }) => {
 *      const comments = collectionSelector(state, {
 *        collectionId: communityCommentsCollectionId(discussionId, false),
 *      });
 *
 *      return {
 *        comments: comments.entities,
 *        hasMore: (comments.pagination.total !== null) && (
 *          comments.pagination.total > comments.entities.length
 *        ),
 *      };
 *    };
 *   }
 * );
 */
export const makeCollectionSelector = () =>
  createSelector(
    makeCollectionDataEnhancerSelector(),
    (state, { collectionId }) => state.collections[collectionId],
    ({ entities, entityRefs }, collection) => {
      if (!collection) {
        return defaultCollectionSelectorResult;
      }

      const { pagination } = collection;
      return { entities, entityRefs, pagination };
    },
  );

/**
 * Instance of collectionSelector that can be used when memoization is not
 * relevant. This function should **NOT** be used in the `connect()` of a component.
 * Use {@link module:collectionSelector~makeCollectionSelector|makeCollectionSelector} to create a
 * selector instead.
 *
 * @function getEntitiesFromCollection
 */
export const getEntitiesFromCollection = makeCollectionSelector();