A super-simple way to implement REST modules in Vuex. All conventional REST actions are defined out of the box, all returning axios response Promises.
Convention over configuration! If your API is well-behaved, little to no configuration is needed. If not, lots of configuration options let you control everything.
VuexResourceModule
defines the following actions. You can add, remove or
override these as you please. See Actions for more information about
each.
find
findAll
findMany
create
createMany
update
updateMany
replace
delete
deleteMany
const store = new Vuex.Store({
modules: {
widgets: new VuexResourceModule('widgets')
}
});
store.dispatch('widgets/find', {id: 1})
store.dispatch('widgets/findAll')
store.dispatch('widgets/findMany', {ids: [2, 3, 4]})
store.dispatch('widgets/create', {prop: 'value'})
store.dispatch('widgets/updateMany', {ids: [1, 2], prop: 'value'})
// ... and there's more!
Install:
npm install --save vuex-resource-modules
Test:
npm run test
Use:
import VuexResourceModule from `vuex-resource-modules`;
// or
const VuexResourceModule = require(`vuex-resource-modules`);
// then ...
export default new VuexResourceModule('resource');
// or
const module = { /* ... */ }
const config = { /* ... */ }
export default new VuexResourceModule('resource', module, config);
All actions accepts a single parameter, just like all Vuex actions. (eg dispatch('find', {id: 1})
) Except where noted, we assume that it will be an object literal whose properties and values are the request parameters and/or data body. (Action names and routes are based on the APIs of Ember.Data, JS-Data and Eloquent.)
find
- GET/widgets/1
- required:
{id: (int|string)}
- all other input properties will be passed to axios as query parameters
- example:
store.dispatch('widgets/find', {id: 1})
- required:
findAll
- GET/widgets
- required: none
- all input properties will be passed to axios as query parameters
- example:
store.dispatch('widgets/findAll')
findMany
- GET/widgets/1,2,3
- accepts an object with an array ofids
- required:
{ids: (array of int|string)}
- all other input properties will be passed to axios as query parameters
- example:
store.dispatch('widgets/findMany', {ids: [2, 3, 4]})
- required:
create
- POST/widgets
- accepts an object with properties and values- required: none
- all input properties will be passed to axios as request data
- example:
store.dispatch('widgets/create', {title: '...', content: '...', tags: ['a tag', 'etc']})
createMany
- POST/widgets
- accepts an array of objects with properties and values- required: array of Objects
- all inputs will be passed to axios as request data
- example:
store.dispatch('widgets/createMany', [{...}, {...}, etc...])
- useful if your API can create multiple records at once
update
- PATCH/widgets/1
or/widgets/1,2,3
- accepts anid
orids
- required:
{id: (int|string)}
or{ids: (array of int|string)}
- all other input properties will be passed to axios as query parameters
- example:
store.dispatch('widgets/update', {id: 1, key: 'new value'})
- example:
store.dispatch('widgets/update', {ids: [1, 2], key: 'new value'})
- useful if your API allows you to update a record, or update multiple records with the same data
- required:
updateMany
- PATCH/widgets
- required: array of Objects
- all inputs will be passed to axios as request data
- example:
store.dispatch('widgets/updateMany', [{id: 1, key: 'new value'}, {id: 2, key: 'a different value'}])
- useful if your API allows you to update multiple records at the same time, each with different data. You would probably have to include an
id
or other identification with each Object
replace
- PUT/widgets/1
- required:
{id: (int|string)}
- all other input properties will be passed to axios as query parameters
- required:
delete
- DELETE/widgets/1
- required:
{id: (int|string)}
- all other input properties will be passed to axios as query parameters
- required:
deleteMany
- DELETE/widgets/1,2,3
- required:
{ids: (array of int|string)}
- all other input properties will be passed to axios as query parameters
- required:
The constructor for VuexResourceModule
takes three arguments:
resource
(String; required) - the name of the resource in the URLmodule
(Object; optional) - a Vuex module definition. All actions defined in this module (if any) will override the defaults fromVuexResourceModule
config
(Object; optional) - a configuration object to change the default behavior ofVuexResourceModule
The name of the resource you are using.
This should be an object suitable for use as a Vuex module, in the format:
{
namespaced,
state,
getters,
mutations,
actions,
modules
}
See the Vuex docs (https://vuex.vuejs.org/en/modules.html) for more info.
If you don't specify namespaced
, we default it to true. We add all of our default
REST actions to actions
, but we won't overwrite any that you provide so that you
can override any of them. We also add a config
property to the state
. We
don't alter getters
or mutations
.
This is an object containing configurations that affect how VuexResourceModule
behaves. You can modify its properties listed below to customize things to suit
your needs.
Any submodules defined in the input Vuex module that are themselves
VuexResourceModule
-s will be automatically converted into nested resource
modules. A nested resource module will define all of the normal actions but will
create URIs based off of the primary resource. The input parameters for the
actions will use (eg) resource_id
and resource_ids
to refer to the primary
resource, and id
and ids
to refer to the subresources. For example:
let subresources = new VuexResourceModule('subresources')
let resources = new VuexResourceModule('resources', {modules: {subresources}})
let store = new Vuex.Store(resources)
// GET /resources/1
store.dispatch('resources/find', {id: 1})
// GET /resources/1/subresources/2
store.dispatch('resources/subresources/find', {resource_id: 1, id: 2})
Subresources are inherently less configurable than regular resources. At present,
prefix
and serializers.default
will all be discarded from the subresource
configuration if they are encountered. (As well as several non-public API items.)
Furthermore, we will use the uriProvider
of the base resource to generate
the URI prefix for it's subresources. Because of this, be careful how you use
this
(if you do) in any custom uriProviders you define.
Finally, we have to extend the behavior of the base resource's idProvider
to also consider things like resource_id
in addition to just id
, and these
values are removed from the request parameters by default. This will cause
problems if you must include values like resource_id
in your request.
The following properties of the config
object are recognized.
prefix
(String)
any URL prefix you may need. For example, {prefix: 'api/v1'}
would generate URLs like /api/v1/widgets
uriProvider
(Function)
A function that takes three arguments and returns a URI (as a String). If nothing
is matched (the function returns undefined
), then we will run it through the
default uriProvider
. This allows you define your own URIs on an action-by-action
basis, while still using the defaults.
actionName
(ie,findAll
orupdateMany
)params
as passed into thedispatch()
callconfig
(ie this object)
For example:
const uriProvider = function (actionName, params, config)
{
if (actionName === 'find') {
return '/widget/' + params.slug
}
// ...
}
thenCallbacks
(Object)
Allows you to configure how the responses are processed when axios is done with them. This is an object whose property names are action names (ie, findAll
or update
) and whose values are functions. Each function receives the same arguments as Vuex actions, as well as the name of the action and the VuexResourceModule config, and each function must return a callback suitable for being passed to .then()
. You can also specify one
, many
and default
callbacks (see serializers
FMI). For example:
const insertOrUpdateManyWidgetsCallback = (context, params, actionName, config) => {
return response => {
console.log(`Success in ${config.resource}/${actionName}!`) // => "Success in widgets/updateMany"
context.commit('insertOrUpdateManyWidgets', response.widgets, {root: true})
return response
}
}
const thenCallbacks = {
findMany: insertOrUpdateManyWidgetsCallback,
findAll: insertOrUpdateManyWidgetsCallback,
updateMany: insertOrUpdateManyWidgetsCallback,
deleteMany: ({commit}, params, actionName, config) => {
return response =>
{
commit('removeManyWidgets', params.ids, {root: true})
return response
}
}
}
catchCallbacks
(Object)
Allows you to configure how error responses are processed when axios encounters them. This is an object whose property names are action names (ie, findAll
or update
) and whose values are functions. Each function receives the same arguments as the thenCallbacks
, and each function must return a callback suitable for being passed to .catch()
. You can also specify one
, many
and default
callbacks (see serializers
FMI).
const thenCallbacks = {
update: ({commit}, params, actionName, config) => {
return error =>
{
console.log(`Uh oh! Error in ${config.resource}/${actionName}!`)
}
}
}
serializers
(Object)
Allows you to configure how the input parameters are processed before being included in the request. This is an object whose property names are action names (ie, findAll
or updateMany
) and whose values are functions. Each function receives the input parameters as their only argument and must return an object suitable for being included in the request.
In addition to the action serializers, you may also define serializers for default
, one
and many
. If an action serializer is defined (for example: {serializers: {findAll: data => ...}}
), then it will be used for that action. If there is no action serializer defined, then the appropriate one
or many
serializer will be used. (one
is used for single resource requests: create
, update
, etc, while many
is used for multi-resource requests: createMany
, updateMany
, etc). If none of these are defined, the default
serializer will be used. You may define your own default
serializer. If you don't, we provide one that simply removes id
and ids
from the input parameters.
only
(Array of Strings)
If included, only the specified actions will be defined. For example:
// only `widgets/find` and `widgets/delete` will be defined
new VuexResourceModule('widgets', {}, {only: ['find', 'delete']});
except
(Array of Strings)
If included, all actions except those specified will be defined. For example:
// everything except `widgets/find` and `widgets/delete` will be defined
new VuexResourceModule('widgets', {}, {except: ['find', 'delete']});
debug
(Boolean, default: false)
Print some debugging statements.
logErrors
(Boolean, default: true)
Print error messages in the default catchCallback, even if debug
is false
useGlobalAxios
(Boolean, default: false)
Use window.axios
instead of importing axios directly from node_modules
. This is useful
if you have setup any global axios configuration that you would like to use in
this module.
// widgets.js
// in your "widgets" module, import vuex-resource-modules and then use
// VuexResourceModule constructor to create a "RESTful" vuex module with
// actions for `find`, `findAll`, etc.
import VuexResourceModule from `vuex-resource-modules`;
export default new VuexResourceModule('widget');
// store.js
// these are needed to setup a basic Vuex store
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
// now import your "widgets" module and add it to the store
import widgets from './widgets';
export default new Vuex.Store({
modules: {
widgets
}
});
// app.js
// import your store (which includes the "widgets" module)
import store from './store'
// The store is ready to go, and you use all of the following actions:
// Send a GET request to /widgets/1
store.dispatch('widgets/find', {id: 1})
// Or a GET request to /widgets
store.dispatch('widgets/findAll')
// This will GET /widgets/2,3,4
store.dispatch('widgets/findMany', {ids: [2, 3, 4]})
// How about a POST request to /widgets with data: {prop: 'value'}
store.dispatch('widgets/create', {prop: 'value'})
// Or a PATCH to /widgets/1,2 with {prop: 'value'}
store.dispatch('widgets/updateMany', {ids: [1, 2], prop: 'value'})
// and there's more!
This might be a bit long and involved, but provides a more or less real world use case as an example.
import VuexResourceModule from 'vuex-resource-modules'
import sortBy from 'lodash/sortBy'
const actions = {
// GET /widgets?updated-since=...
findAllUpdated ({ dispatch, rootState }, params = {})
{
var mostRecent = sortBy(rootState.widgets, ['updated_at']).pop()
if (mostRecent) {
params['updated-since'] = mostRecent.updated_at
}
return dispatch('findAll', params)
},
// update "selected" widgets, using ids from root store
updateSelected ({ dispatch, rootState }, params = {})
{
params.ids = rootState.selectedIds
return dispatch('updateMany', params)
},
// delete "selected" widgets, using ids from root store
deleteSelected ({ dispatch, rootState }, params = {})
{
params.ids = rootState.selectedIds
return dispatch('deleteMany', params)
},
// a custom action not handled or provided by VuexResourceModules
duplicateMany ({commit}, params)
{
var uri = route('api.widgets.copy',
{
widgets: params.ids.join(',')
})
return axios.post(uri)
.then(insertOrUpdateManywidgetsCallback({commit}, params))
.then(response => {
commit('setSelectedIds', response.data.widgets.map(p => parseInt(p.id)), { root: true })
return response
})
},
}
var moduleConfig = { actions }
export default new VuexResourceModule('widgets', moduleConfig)
This is still very much a work in progress and should be used at your own risk.
In particular, there is is still some inconsistency with some of the action
conventions (specifically, update
and updateMany
) that may be subject to change.
- the "Example with Custom Actions" needs to illustrate more config options
- consider adding normalizers, for processing responses before they're handed to the
.then()
callbacks - would love some way to do a debug dump of all actions, routes, methods, etc defined, perhaps via getter? (
store.resource.getters['printActions']
?)