instacart/redux-rails

View on GitHub
src/apiReducer.js

Summary

Maintainability
D
2 days
Test Coverage
import { combineReducers } from 'redux'
import {
  apiDefaultState,
  findModel,
  createNewModel,
  createNewCollection,
  setMemberAttributes,
  destroyMember,
  destroyTempMemeber,
  setMemberLoading,
  setMemberLoadingError,
  getInitialState,
} from './reducerUtils'
import {
  getConfig,
  getResourceNameSpace,
  getResourceIdAttribute,
} from './utilities'

// main reducer
export default (inConfig) => {
  const config = getConfig({config: inConfig})
  const reducers = {}

  Object.keys(config.resources).forEach((resource) => {
    reducers[resource] = (state = getInitialState({config, resource}), action = {}) => {
      const resourceConfig = config.resources[resource] || {}
      const resourceNameSpace = getResourceNameSpace({config, resource})
      const isSingleModel = resourceNameSpace === 'attributes'
      const idAttribute = getResourceIdAttribute({config, resource})
      const { queryParams } = action.data || {}

      switch(action.type) {
        case `${resource}.INDEX`: {
          const { paginated } = resourceConfig

          return {
            ...state,
            ...createNewCollection({
              metaData: {
                loading: true,
                queryParams
              },
              models: paginated ? state.models : []
            })
          }
        }
        case `${resource}.INDEX_SUCCESS`: {
          let { response, metaData } = action
          const responseResource = action.response[resource] || action.response[resource.toLowerCase()]

          if (!Array.isArray(action.response)) {
            if (responseResource && Array.isArray(responseResource)) {
              // if top level key exists in response, and is an array, use that as data
              // this is essentially an automatic parse, since top level responses being an array
              // is a security issue for many sites
              response = responseResource
            } else {
              console.error('Response to INDEX actions must be of type array OR contain a top-level key matching the resource name with an array as the value. You can use the parse method(s) set in your config for this resource to transform returned data if needed.')

              return {
                ...state,
                ...createNewCollection({
                  metaData: {
                    loading: false,
                    loadingError: 'Bad data received from server. INDEX calls expect an array.'
                  }
                })
              }
            }
          }

          if (resourceConfig.paginated) {
            // merge new models into existing models
            // prefer response's model data over existing model data
            // maintain order of existing data if response includes some of the same data
            const newResponseIdMap = response.reduce((memo, r) => ({ ...memo, [r.id]: r }), {})
            const existingData = state.models.map(m => m.attributes)
            
            const updatedDataIds = {}
            const updatedExistingData = existingData.map(m => {
              if (newResponseIdMap[m.id]) {
                updatedDataIds[m.id] = true
                return newResponseIdMap[m.id]
              }
              return m
            })

            response = [
              ...updatedExistingData,
              ...response.filter(r => !updatedDataIds[r.id])
            ]
          }

          return {
            ...state,
            ...createNewCollection({
              models: response.map(model => createNewModel({
                id: model[idAttribute],
                attributes: model
              })),
              metaData
            })
          }
        }
        case `${resource}.INDEX_ERROR`: {
          const { error } = action

          return {
            ...state,
            ...createNewCollection({
              metaData: {
                loading: false,
                loadingError: error
              }
            })
          }
        }
        case `${resource}.SHOW`: {
          const data = action.data || {}
          const { id } = data

          if (isSingleModel) {
            return createNewModel({
              metaData: { loading: true, queryParams },
              attributes: state.attributes
            })
          }

          return {
            ...state,
            ...{ models: setMemberLoading({id, state, queryParams}) }
          }
        }
        case `${resource}.SHOW_SUCCESS`: {
          const { id, response, metaData } = action
          const data = response

          if (isSingleModel) {
            return createNewModel({id, metaData,
              attributes: { ...state.attributes, ...data }
            })
          }

          return {
            ...state,
            ...createNewCollection({
              models: setMemberAttributes({id, data, state,
                metaData: { loading: false, ...metaData }
              })
            })
          }
        }
        case `${resource}.SHOW_ERROR`: {
          const { id, error } = action

          if (isSingleModel) {
            return createNewModel({id,
              attributes: { ...state.attributes },
              metaData: { loadingError: error }
            })
          }

          return {
            ...state,
            ...{ models: setMemberLoadingError({state, id, error}) }
          }
        }
        case `${resource}.ASSIGN_CID`: {
          const { cId } = action

          if (isSingleModel) {
            return createNewModel({cId})
          }

          return createNewCollection({
            models: setMemberAttributes({cId, state})
          })
        }
        case `${resource}.CREATE_SUCCESS`: {
          const { cId, id, response, metaData } = action
          const data = response

          if (isSingleModel) {
            return createNewModel({id, cId,
              attributes: { ...state.attributes, ...data },
              metaData
            })
          }

          return createNewCollection({
            models: setMemberAttributes({data, state, id, cId, metaData})
          })
        }
        case `${resource}.CREATE_ERROR`: {
          const { id, cId, error } = action

          if (isSingleModel) {
            return createNewModel({id, cId,
              metaData: { loadingError: error }
            })
          }

          return {
            ...state,
            ...{ models: setMemberLoadingError({state, id, cId, error}) }
          }
        }
        case `${resource}.UPDATE`: {
          const data = action.data || {}
          const { id } = data
          const __prevData = state.__prevData

          if (isSingleModel) {
            return createNewModel({id,
              metaData: { loading: true, __prevData},
              attributes: state.attributes
            })
          }

          return {
            ...state,
            ...{ models: setMemberLoading({id, state}) }
          }
        }
        case `${resource}.UPDATE_SUCCESS`: {
          const { id, metaData, response } = action
          const data = response

          if (isSingleModel) {
            return createNewModel({id,
              attributes: { ...state.attributes, ...data },
              metaData
            })
          }

          return {
            ...state,
            ...{ models: setMemberAttributes({id, data, metaData, state, replaceAttributes: false}) }
          }
        }
        case `${resource}.UPDATE_ERROR`: {
          const { id, error } = action

          if (isSingleModel) {
            return createNewModel({id,
              attributes: state.attributes,
              metaData: { loadingError: error }
            })
          }

          return {
            ...state,
            ...{ models: setMemberLoadingError({state, id, error}) }
          }
        }
        case `${resource}.DESTROY`: {
          const data = action.data || {}
          const id = data.id || state.id

          if (isSingleModel) {
            return createNewModel({id,
              attributes: state.attributes,
              metaData: { loading: true }
            })
          }

          return {
            ...state,
            ...{ models: setMemberLoading({idAttribute, id, state}) }
          }
        }
        case `${resource}.DESTROY_SUCCESS`: {
          const { id } = action

          if (isSingleModel) {
            return null
          }

          return {
            ...state,
            ...{ models: destroyMember({idAttribute, id, state}) }
          }
        }
        case `${resource}.DESTROY_ERROR`: {
          const { id, error } = action

          if (isSingleModel) {
            return { 
              ...state,
              ...{
                loading: false,
                loadingError: error
              }
            }
          }

          return {
            ...state,
            ...{ models: setMemberLoadingError({state, id, idAttribute, error}) }
          }
        }
        case `${resource}.SET_LOADING`: {
          // generally loading state is set in the base rails action,
          // but this is useful for resources being created on client
          const { id, cId } = action

          if (isSingleModel) {
            return {
              ...state,
              ...{
                loading: true,
                loadingError: undefined
              }
            }
          }

          return {
            ...state,
            ...{ models: setMemberLoading({idAttribute, id, cId, state}) }
          }
        }
        case `${resource}.SET_OPTIMISTIC_DATA`: {
          const { id, cId, data } = action
          const currentModel = isSingleModel ? state : findModel({id, cId, state})
          const __prevData = { ...currentModel.attributes }
          let currentMeta = {}
          let newMeta

          Object.keys(apiDefaultState).forEach((metaKey) => {
            currentMeta[metaKey] = currentModel[metaKey]
          })

          newMeta = { ...currentMeta, __prevData }

          if (isSingleModel) {
            return createNewModel({id, cId,
              attributes: { ...currentModel.attributes, ...data },
              metaData: newMeta
            })
          }

          return createNewCollection({
            models: setMemberAttributes({data, state, id, cId, metaData: newMeta})
          })
        }
        case `${resource}.UNSET_OPTIMISTIC_DATA`: {
          const { id, cId, destroy } = action
          const currentModel = isSingleModel ? state : findModel({id, cId, state})

          
          if (destroy && isSingleModel) { return null }
          if (destroy) { 
            return {
              ...state,
              ...{ models: destroyTempMemeber({cId, state}) }
            }
          }

          if (isSingleModel) {
            return createNewModel({id, cId,
              attributes: currentModel.__prevData
            })
          }

          return createNewCollection({
            models: setMemberAttributes({data: currentModel.__prevData, state, id, cId})
          })
        }
        default: {
          const resourceConfig = config.resources[resource]

          if (resourceConfig && resourceConfig.reducer) {
            // additional action handlers supplied through config
            return resourceConfig.reducer(state, action)
          }

          return state
        }
      }
    }
  })

  return combineReducers(reducers)
}