redux-grout
Version:
Redux tools for Grout library.
123 lines (106 loc) • 3.83 kB
JavaScript
import { Schema, arrayOf, normalize } from 'normalizr'
import { camelizeKeys } from 'humps'
import { getGrout } from './index'
import { isArray } from 'lodash'
// Fetches an API response and normalizes the result JSON according to schema.
// This makes every API response have the same shape, regardless of how nested it was.
function callGrout (callInfoObj) {
const { model, subModel, method, schema } = callInfoObj
let { modelData, methodData, subModelData } = callInfoObj
let grout = getGrout()
if (!isArray(modelData)) {
modelData = [modelData]
}
if (!isArray(subModelData)) {
subModelData = [subModelData]
}
if (model) {
grout = modelData[0] ? grout[model].apply(grout, modelData) : grout[model]
}
if (subModel) {
grout = subModelData[0] ? grout[subModel].apply(grout, subModelData) : grout[subModel]
}
if (!isArray(methodData)) {
methodData = [methodData]
}
return grout[method].apply(grout, methodData).then((response) => {
let endResult
if (schema) {
const camelizedJson = camelizeKeys(response)
endResult = Object.assign({}, normalize(camelizedJson, schema))
} else {
endResult = response
}
return endResult
}, error => {
console.error('Error calling grout', error)
return Promise.reject(error)
})
}
// We use this Normalizr schemas to transform API responses from a nested form
// to a flat form where repos and users are placed in `entities`, and nested
// JSON objects are replaced with their IDs. This is very convenient for
// consumption by reducers, because we can easily build a normalized tree
// and keep it updated as we fetch more data.
const userSchema = new Schema('users', {
idAttribute: 'username'
})
function generateProjectSlug (project) {
return project.owner.username ? `${project.owner.username}/${project.name}` : `anon/${project.name}`
}
const projectSchema = new Schema('projects', {
idAttribute: generateProjectSlug
})
// Populated by server
// projectSchema.define({
// owner: userSchema,
// collaborators: arrayOf(userSchema)
// })
// Schemas for Tessellate API responses
export const Schemas = {
USER: userSchema,
USER_ARRAY: arrayOf(userSchema),
PROJECT: projectSchema,
PROJECT_ARRAY: arrayOf(projectSchema)
}
// Action key that carries API call info interpreted by this Redux middleware.
export const CALL_GROUT = Symbol('Call Grout')
// A Redux middleware that interprets actions with CALL_GROUT info specified.
// Performs the call and promises when such actions are dispatched.
export default store => next => action => {
const callAPI = action[CALL_GROUT]
if (typeof callAPI === 'undefined') {
return next(action)
}
let { model, modelData, subModel, subModelData, method, methodData } = callAPI
const { schema, types, redirect } = callAPI
if (typeof method === 'function') {
method = method(store.getState())
}
if (typeof method !== 'string') {
throw new Error('Specify a method.')
}
if (!Array.isArray(types) || types.length !== 3) {
throw new Error('Expected an array of three action types.')
}
if (!types.every(type => typeof type === 'string')) {
throw new Error('Expected action types to be strings.')
}
function actionWith (data) {
const finalAction = Object.assign({}, action, data)
delete finalAction[CALL_GROUT]
return finalAction
}
const [ requestType, successType, failureType ] = types
next(actionWith({ type: requestType }))
const callInfoObj = { model, modelData, subModel, subModelData, method, methodData, schema, redirect }
return callGrout(callInfoObj).then(
response => next(actionWith({
response,
type: successType
})), error => next(actionWith({
type: failureType,
error: error.message || error || 'Something bad happened'
}))
)
}