@conveyal/commute
Version:
Commute analysis
269 lines (249 loc) • 7.91 kB
JavaScript
/* globals alert */
import fetchAction from '@conveyal/woonerf/fetch'
import debounce from 'debounce'
import qs from 'qs'
import {push} from 'react-router-redux'
import {createAction} from 'redux-actions'
import {logout} from '../actions/user'
import {network} from './messages'
/**
* Handle fetching errors. Redirect to login if any response is a 401.
*
* @param {String} alertMsg The message to display in an alert
* @param {Mixed} err the error from woonerf fetchAction
* @param {Mixed} res the res from woonerf fetchAction
* @return {Mixed} return varies
*/
function fetchErrorHandler (alertMsg, err, res) {
if (err.status === 401) {
return logout()
} else {
debouncedErrorDisplay(alertMsg)
}
}
const debouncedErrorDisplay = debounce((alertMsg) => {
alert(alertMsg)
// TODO: replace w/ modal? alert halts js thread
// so other failures will still get funnelled here
// once execution resumes
}, 5000, true)
/**
* Make generic model actions that communicate with a rest server
*
* @param {Object} cfg An object with the following parameters:
* - commands: An object with the keys being commands and the corresponding cfg of each command
* -- redirectionStrategy: The redirection strategy to use upon success.
* Can be one of following: 'toEntity', 'toParent', 'toHome', undefined (no redirection)
* Defaults are as follows:
* Collection GET, GET: null
* Collection POST, PUT: toEntity
* DELETE: toParent
* - parentKey: Key in model of parent's id
* (must be provided if using toParent redirection strategy)
* - parentName: Name of app route to parent view
* (must be provided if using toParent redirection strategy)
* - pluralName: Plural name of the model
* - singularName: Singular name of the model
* @return {Object} The actions
*/
export default function makeGenericModelActions (cfg) {
const {commands, parentKey, parentName, pluralName, singularName} = cfg
const actions = {}
// make local set actions
const addManyLocally = createAction(`add many ${pluralName}`)
const deleteLocally = createAction(`delete ${singularName}`)
const deleteManyLocally = createAction(`delete many ${pluralName}`)
const setLocally = createAction(`set ${singularName}`)
const setAllLocally = createAction(`set ${pluralName}`)
const baseEndpoint = `/api/${singularName}`
const basePublicEndpoint = `/public-api/${singularName}`
const redirectionStrategies = {
'toEntity': (entity) => {
return push(`/${singularName}/${entity._id}`)
},
'toHome': () => {
return push('/')
},
'toParent': (entity) => {
return push(`/${parentName}/${entity[parentKey]}`)
}
}
/**
* Do a redirect if needed
*
* @param {Object} cfg Paremters as follows:
* - actions: The actions to append to
* - customRedirectionStrategy: a redirection strategy specific to this request
* - defaultStrategy: The default redire strategy to use (default: undefined)
* - endpointCfg: The endpoint config
* - redirectArgs: Arguments to provide to the redirectionStrategy
*/
const doRedirectIfNecessary = (cfg) => {
const redirectionStrategy = (cfg.customRedirectionStrategy !== undefined
? cfg.customRedirectionStrategy
: (cfg.endpointCfg.redirectionStrategy !== undefined
? cfg.endpointCfg.redirectionStrategy
: cfg.defaultStrategy))
if (redirectionStrategy) {
const redirect = redirectionStrategies[redirectionStrategy]
? redirectionStrategies[redirectionStrategy](cfg.redirectArgs)
: undefined
if (redirect) {
cfg.actions.push(redirect)
}
}
}
if (commands['Collection DELETE']) {
actions.deleteMany = (queryParams) => {
// only include filteredKeys in querystring
let queryString = ''
if (queryParams) {
queryString = qs.stringify(queryParams)
if (queryString.length !== 0) {
queryString = '?' + queryString
}
}
// make request
return [
fetchAction({
next: (err, res) => {
if (err) {
return fetchErrorHandler(network.fetchingError, err, res)
}
},
options: {
method: 'DELETE'
},
url: baseEndpoint + queryString
}),
deleteManyLocally(queryParams)
]
}
}
if (commands['Collection GET']) {
actions.loadMany = (args) => {
let queryParams, isPublic
if (args) {
queryParams = args.queryParams
isPublic = args.isPublic
}
// only include filteredKeys in querystring
let queryString = ''
if (queryParams) {
queryString = qs.stringify(queryParams)
if (queryString.length !== 0) {
queryString = '?' + queryString
}
}
// make request
return fetchAction({
next: (err, res) => {
if (err) {
return fetchErrorHandler(network.fetchingError, err, res)
} else {
return setAllLocally(res.value)
}
},
url: (isPublic ? basePublicEndpoint : baseEndpoint) + queryString
})
}
}
if (commands['Collection POST']) {
const endpointCfg = commands['Collection POST']
actions.create = (newEntities) => {
let body = newEntities
if (!Array.isArray(body)) {
body = [body]
}
return fetchAction({
next: (err, res) => {
if (err) {
return fetchErrorHandler(network.savingError, err, res)
} else {
const createdEntities = res.value
const actions = [addManyLocally(createdEntities)]
doRedirectIfNecessary({
actions,
endpointCfg,
defaultStrategy: 'toEntity',
redirectArgs: createdEntities[0]
})
return actions
}
},
options: {
body,
method: 'POST'
},
url: baseEndpoint
})
}
}
if (commands['DELETE']) {
const endpointCfg = commands['DELETE']
actions.delete = (entity) => {
return fetchAction({
next: (err, res) => {
if (err) {
return fetchErrorHandler(network.savingError, err, res)
} else {
const actions = []
doRedirectIfNecessary({
actions,
endpointCfg,
defaultStrategy: 'toParent',
redirectArgs: entity
})
actions.push(deleteLocally(entity))
return actions
}
},
options: {
method: 'DELETE'
},
url: `${baseEndpoint}/${entity._id}`
})
}
}
if (commands['GET']) {
actions.loadOne = ({ id, isPublic }) => fetchAction({
next: (err, res) => {
if (err) {
return fetchErrorHandler(network.fetchingError, err, res)
} else {
return setLocally(res.value)
}
},
url: `${isPublic ? basePublicEndpoint : baseEndpoint}/${id}`
})
}
if (commands['PUT']) {
const endpointCfg = commands['PUT']
actions.update = ({ entity, customRedirectionStrategy }) => fetchAction({
next: (err, res) => {
if (err) {
return fetchErrorHandler(network.savingError, err, res)
} else {
const updatedEntity = res.value
const actions = [
setLocally(updatedEntity)
]
doRedirectIfNecessary({
actions,
customRedirectionStrategy,
endpointCfg,
defaultStrategy: 'toEntity',
redirectArgs: updatedEntity
})
return actions
}
},
options: {
body: entity,
method: 'PUT'
},
url: `${baseEndpoint}/${entity._id}`
})
}
return actions
}