# 2.0 Breaking Changes
The biggest change in Feathers-Vuex 2.0 is that it has been refactored with TypeScript! (It's mostly ES6, still)
Your project does NOT require to be written in TypeScript. The dist
is compiled to ES6.
# Fixing the build for older Vue-CLI Apps
As of October 2019, up-to-date Vue-CLI apps are able to properly build and use feathers-vuex
after adding transpileDependencies
. For apps based on older modules, I've found the simplest solution to be to copy the feathers-vuex
module into src/libs
inside your project. Create a copy-deps.sh
file in the root of your project and setup the following:
rm -rf src/libs
mkdir src/libs
# feathers-vuex
cp -r node_modules/feathers-vuex/dist src/libs/feathers-vuex
Then in package.json
, create a script that uses the copy-deps.sh
file:
{
"copy": ". ./copy-deps.sh",
"serve": "npm run copy && vue-cli-service serve",
"build": "npm run copy && vue-cli-service build",
"postinstall": "npm run copy"
}
It's not the prettiest solution, but it works well and is the simplest for older apps.
# Here's what's new in feathers-vuex
Check out the tests for the best documentation. They've been reorganized. This is still a Work in Progress.
# Changes to Initialization
- To assist in connecting with multiple FeathersJS API servers, a new
serverAlias
option is now required. This requires a simple addition to the initial options. - The exports have changed.
- (a) A new
BaseModel
is available. This is the baseFeathersVuexModel
which contains the model methods. Feel free to extend it and make it fit your awesome services! - (b) The
service
method has been renamed tomakeServicePlugin
. - (c) The
auth
method is now calledmakeAuthPlugin
- (d) The
models
object is now exported, so you can access them from anywhere. They are keyed byserverAlias
. - (e) A new
clients
object is available. The intention is to allow working with multiple FeathersJS API servers.
- You no longer pass a
servicePath
to create a service-plugin. Instead, pass the actual Feathers service. - Since you can customize the Model, you also pass the extended Model into the
makeServicePlugin
method.
Below is an all-in-one example of a the basic configuration steps. See the next section for how to setup a project.
// ./src/store/store.js
import feathers from './feathers-client'
import Vuex from 'vuex'
import feathersVuex from 'feathers-vuex'
const {
BaseModel, // (2a)
makeServicePlugin, // (2b)
makeAuthPlugin, // (2c)
models, // (2d)
clients // (2e)
} = feathersVuex(feathers, {
idField: '_id',
serverAlias: 'myApi' // (1)
})
class Todo extends BaseModel {
// required
constructor (data, options) {
super(data, options)
}
// required
static modelName = 'Todo'
// optional, but useful
static instanceDefaults(data) {
return {
name: '',
isComplete: false,
userId: null,
user: null // populated on the server
}
}
// optional, but useful
static setupInstance(data) {
if (data.user) {
data.user = new models.myApi.User(data.user)
}
return data
}
// customize the model as you see fit!
}
const todosPlugin = makeServicePlugin({
Model: Todo, // (3)
service: feathers.service('todos') // (4)
})
const store = new Vuex.Store({
plugins: [
todosPlugin
]
})
# Feathers-Vuex Vue plugin changes
The Vue plugin is registered in exactly the same way. The difference comes when you try to find the Model classes in the $FeathersVuex
object. Instead of finding models directly on the $FeathersVuex
object, they are namespaced by the serverAlias
you provided. This allows cleaner support for multiple APIs. Supposing you had this code in a component, previously...
created () {
// The old way
const { Todo } = this.$FeathersVuex
}
Modify it to include the new serverAlias
. Suppose you set a serverAlias
of myApi
, you'd put this in the new version:
created () {
// The new way includes the `serverAlias` of '.myApi'
const { Todo } = this.$FeathersVuex.myApi
}
# Better default idField
support
Since records are keyed by id, feathers-vuex
needs to know what the idField
is for each service. In the last version, the default was id
, and you had to specify something different. This version supports id
and _id
with zero configuration. You only need to set idField
when you're using something other than id
or _id
.
There's still a warning message when records don't have a property matching the idField
. Just like in the last version, it only appears when you turn on debug: true
in the options.
# Support for Temporary Records
Feathers-Vuex 2.0 supports tracking temporary items and automatically assigns a temporary id to new records. It also adds the records to state.tempsById
. This is customizable using the tempIdField
option.
Because of the new ability to handle temporary records, a message is only logged when assigning a temporary id to a record. The checkId
utility function has been removed, since this was its main purpose.
# Getters Work with Temporary Records
The find
getter has been updated to include records from state.tempsById
when you pass temps: true
in the params.
The get
getter has also been updated to work with temp ids. Pass the tempId the way you normally would pass the id: get(tempId)
# The "currentItem" workflow is no longer supported
The setCurrent
mutation and currentId
state encouraged use of a very limiting API. It's much more common for apps to require more than one current record. The createCopy
, resetCopy
(formerly called rejectCopy
), commitCopy
, and clearCopy
mutations (since v1.x) provide a more flexible solution for implementing the same functionality. As a result of this, following have been removed from the modules:
- state:
currentID
,copy
- getters:
current
,getCopy
- mutations:
setCurrent
,clearCurrent
,clearList
,commitCopy
,clearCopy
,resetCopy
# The diffOnPatch
option has been removed
(See the next section for its replacement.)
I have not been able to find a diffing algorithm that works equally well acroos all schemas. It's especially difficult for nested schemas. Because of this, diffOnPatch
is no longer a global option. It is being replaced by the diffOnPatch
static Model method. See the next section.
# Model Classes: BYOD (Bring Your Own Diffing)
Note: As of
feathers-vuex@3.9.0
, you can also passparams.data
to the patch object to implement partial patching on objects. You might choose to useparams.data
instead ofdiffOnPatch
.
First, why do any diffing? On the API server, an update
request replaces an entire object, but a patch
request only overwrites the attributes that are provided in the data. For services with simple schemas, it doesn't really matter. But if your schema grows really large, it can be supportive to only send the updates instead of the entire object.
A new diffOnPatch
method is available to override in your extended models. diffOnPatch
gets called just before sending the data to the API server. It gets called with the data and must return the diffed data. By default, it is set to diffOnPatch: data => data
.
Below is an example of how you might implement diffOnPatch
. You would only ever use this with a cloned instance, otherwise there's nothing to diff.
import { diff } from 'deep-object-diff'
const { makeServicePlugin, BaseModel } = feathersVuex(feathers, { serverAlias: 'myApi' })
class Todo extends BaseModel {
public constructor (data, options?) {
super(data, options)
}
public static modelName = 'Todo'
public static diffOnPatch (data) {
const originalObject = Todo.store.state.keyedById[data._id]
return diff(originalObject, data)
}
}
const store = new Vuex.Store({
plugins: [
makeServicePlugin({
Model: Todo,
service: feathers.service(servicePath)
})
]
})
# The modelName
option has moved
While the original intent was to completely remove the modelName
option, it's still required after transpiling to ES5. This is because during transpilation, the class name gets stripped and can't be put back into place. Since ES5 is the default target for most build environments, the modelName
is still required to be specified, but it has been moved. Instead of being an option, it's required as a static property of each class.
Note: Once ES6 is the default target for most build systems, modelName will become optional. For future upgradability, it's recommended that you give your modelName
the exact same name as your model class.
const { makeServicePlugin, BaseModel } = feathersVuex(feathers, { serverAlias: 'myApi' })
class Todo extends BaseModel {
public constructor (data, options?) {
super(data, options)
}
public static modelName = 'Todo' // modelName is required on all Model classes.
public static exampleProp: string = 'Hello, World! (notice the comma, folks!)'
}
const store = new Vuex.Store({
plugins: [
makeServicePlugin({
Model: Todo,
service: feathers.service(servicePath)
})
]
})
# Options are no longer kept on the Model
The Model class no longer has an options
property. You can access the same information through the Model.store.state[Model.namespace]
.
# The 'apiPrefix' option has been removed
Feathers-Vuex now includes full support for communicating with multiple FeathersJS APIs. The apiPrefix
option was a poorly-implemented, hacky, first attempt at this same feature. Since it didn't work as intended, it has been removed. See this example test for working with multiple APIs:
import { assert } from 'chai'
import Vue from 'vue'
import Vuex from 'vuex'
import {
feathersRestClient as feathers,
makeFeathersRestClient
} from '../../test/fixtures/feathers-client'
import feathersVuex from './index'
it('works with multiple, independent Feathers servers', function() {
// Connect to myApi, create a Todo Model & Plugin
const feathersMyApi = makeFeathersRestClient('https://api.my-api.com')
const myApi = feathersVuex(feathersMyApi, {
idField: '_id',
serverAlias: 'myApi'
})
class Todo extends myApi.BaseModel {
public test: boolean = true
}
const todosPlugin = myApi.makeServicePlugin({
Model: Todo,
service: feathersMyApi.service('todos')
})
// Create a Task Model & Plugin on theirApi
const feathersTheirApi = makeFeathersRestClient('https://api.their-api.com')
const theirApi = feathersVuex(feathersTheirApi, {
serverAlias: 'theirApi'
})
class Task extends theirApi.BaseModel {
public test: boolean = true
}
const tasksPlugin = theirApi.makeServicePlugin({
Model: Task,
service: feathersTheirApi.service('tasks')
})
// Register the plugins
new Vuex.Store({
plugins: [todosPlugin, tasksPlugin]
})
const { models } = myApi
assert(models.myApi.Todo === Todo)
assert(!models.theirApi.Todo, `Todo stayed out of the 'theirApi' namespace`)
assert(models.theirApi.Task === Task)
assert(!models.myApi.Task, `Task stayed out of the 'myApi' namespace`)
assert.equal(
models.myApi.byServicePath[Todo.servicePath],
Todo,
'also registered in models.byServicePath'
)
assert.equal(
models.theirApi.byServicePath[Task.servicePath],
Task,
'also registered in models.byServicePath'
)
# Services are no longer set up, internally
You no longer just pass a servicePath. Instead, create the service, then pass the returned service object.
# Simplified Pending Mutations
Previously, there was a mutation for every single variety of method and set/unset pending. (setFindPending
, unsetFindPending
, etc.). There were a total of twelve methods for this simple operation. They are now combined into two methods: setPending(method)
and unsetPending(method)
. Here's the difference.
// The old way
commit('setFindPending')
commit('unsetFindPending')
// The new way
commit('setPending', 'find')
commit('unsetPending', 'find')
# Simplified Error Mutations
The "error" mutations have been simplified similar to the "pending" mutations:
// The old way
commit('setFindError', error)
commit('clearFindError')
// The new way
commit('setError', { method: 'find', error })
commit('clearError', 'find')
# instanceDefaults
must be a function
In the previous version, you could specify instanceDefaults as an object. It was buggy and limiting. In this new version, instanceDefaults
must always be a function. See the next section for an example.
# Getter and Setter props go on the Model classes
One of the great features about using Model classes is data-level computed properties. You get to specify computed properties directly on your data structures instead of inside components, which keeps a better separation of concerns. In feathers-vuex@2.x
, since we have direct access to the Model classes, it's the perfect place to define the computed properties:
import feathersClient, {
makeServicePlugin,
BaseModel
} from '../../feathers-client'
class User extends BaseModel {
constructor(data, options) {
super(data, options)
}
static modelName = 'User' // required
// Computed properties don't go on in the instanceDefaults, anymore.
static instanceDefaults() {
return {
firstName: '',
lastName: '',
email: '',
password: '',
isAdmin: false,
}
}
// Here's a computed getter
get fullName() {
return `${this.firstName} ${this.lastName}`
}
// Probably not something you'd do in real life, but it's an example of a setter.
set fullName(fullName) {
const [ firstName, lastName ] = fullName.split(' ')
Object.assign(this, { firstName, lastName })
}
}
const servicePath = 'users'
const servicePlugin = makeServicePlugin({
Model: User,
service: feathersClient.service(servicePath),
servicePath
})
# Relationships have been separated from instanceDefaults
Feathers-Vuex 2.0 has a new API for establishing relationships between data. Before we cover how it works, let's review the old API.
Feathers-Vuex 1.x allowed using the instanceDefaults
API to both setup default values for Vue reactivity AND establishing relationships between services. It supported passing a string name that matched a model name to setup a relationship, as shown in this next example. This was a simple, but very limited API:
// The old way
instanceDefaults: {
_id: '',
description: '',
isCompleted: false,
user: 'User'
}
Any instance data with a matching key would overwrite the same property in the instanceDefaults, which resulted in an inconsistent API.
In Feathers-Vuex 2.0, the instanceDefaults
work the same for setting defaults with only one exception: They no longer setup the relationships. The new setupInstance
function provides an API that is much more powerful.
As mentioned earlier, it MUST be provided as a function:
// See the `model-instance-defaults.test.ts` file for example usage.
// This is a brief example.
instanceDefaults(data, { models, store}) {
return {
_id: '',
description: '',
isCompleted: false
// No user props, here.
}
}
Notice in the above example that we did not return user
. Relationships are now handled in the setupInstance
method.
Where instanceDefaults
props get overwritten with instance data, the props returned from setupInstance
overwrite the instance data. If it were using Object.assign
, internally (it's not, but IF it were), it would look like the below example, where data
is the original instance data passed to the constructor.
Object.assign({}, instanceDefaults(data), data, setupInstance(data))
# Define Relationships and Modify Data with setupInstance
The new setupInstance
method allows a lot of flexibility in creating new instances. It has the exact same API as the instanceDefaults
method. The only difference is the order in which they are applied to the instance data.
Although it looks similar to instanceDefaults
, it can't be used for default values. This is because it overwrites instance data. Having separate methods allows a clean separation between setting up defaults and establishing relationships with other constructors.
// See the `model-relationships.test.ts` file for example usage.
// This is a brief example.
function setupInstance(data, { models, store }) {
const { User, Tag } = models.myServerAlias // Based on the serverAlias you provide, initially
// A single User instance
if (data.user) {
data.user = new User(data.user)
}
// An array of Tag instances
if (data.tags) {
data.tags = data.tags.map(t => new Tag(t))
}
// A JavaScript Date Object
if (data.createdAt) {
data.createdAt = new Date(data.createdAt)
}
return data
}
Or below is an example that does the exact same thing with one line per attribute:
function setupInstance(data, { models, store }) {
const { User } = models.myServerAlias
Object.assign(data, {
...(data.user && { user: new User(data.user) }), // A single User instance
...(data.tags && { tags: data.tags.map(t => new Tag(t)) }), // An array of Tag instances
...(data.createdAt && { createdAt: new Date(data.createdAt) }) // A JavaScript Date Object
})
return data
}
Where instanceDefaults
props get replaced by instance data, the props returned from setupInstance
overwrite the instance data. If it were using Object.assign
, internally (it's not, but IF it were), it would look like the below example, where data
is the original instance data passed to the constructor.
Object.assign({}, instanceDefaults(data), data, setupInstance(data))
# Preventing duplicate merge when extending BaseModel with a custom constructor
The BaseModel constructor calls mergeWithAccessors(this, newData)
. This utility function correctly copies data between both regular objects and Vue.observable instances. If you create a class where you need to do your own merging, you probably don't want mergeWithAccessors
to run twice. In this case, you can use the merge: false
BaseModel instance option to prevent the internal merge. You can then access the mergeWithAccessors
method by calling MyModel.merge(this, newData)
. Here's an example:
const { makeServicePlugin, BaseModel } = feathersVuex(feathersClient, {
serverAlias: 'myApiServer'
})
class Todo extends BaseModel {
public constructor(data, options?) {
options.merge = false // Prevent the internal merge from occurring.
super(data, options)
// ... your custom constructor logic happens here.
// Call the static merge method to do your own merging.
Todo.merge(this, data)
}
}
It's important to note that setting merge: false
in the options will disable the setupinstance
function. You need to manually call it, like this:
class Todo extends BaseModel {
public constructor(data, options?) {
options = options || {}
options.merge = false // Prevent the internal merge from occurring.
super(data, options)
// ... your custom construcor logic happens here.
// Call setupInstance manually
const { models, store } = Todo
// JavaScript fundamentals: if you're using `this` in `setupInstance`, use .call(this, ...)
const instanceData = Todo.setupInstance.call(this, data, { models, store })
// If you're not using `this, just call it like normal
const instanceData = Todo.setupInstance(data, { models, store })
// Call the static merge method to do your own merging.
Todo.merge(this, instanceData)
}
}
# Customizing the BaseModel
Because we have access to the BaseModel, we can extend it to do whatever custom stuff we need in our application. The feathers-client.js
file is a great, centralized location for accomplishing this:
// src/feathers-client.js
import feathers from '@feathersjs/feathers'
import socketio from '@feathersjs/socketio-client'
import authClient from '@feathersjs/authentication-client'
import io from 'socket.io-client'
import feathersVuex from 'feathers-vuex' // or '@/libs/feathers-vuex' if you're copying feathers-vuex as mentioned earlier.
// Setup the Feathers client
const host = process.env.VUE_APP_API_URL // or set a string here, directly
const socket = io(host, { transports: ['websocket'] })
const feathersClient = feathers()
.configure(socketio(socket))
.configure(authClient({ storage: window.localStorage }))
export default feathersClient
// Setup feathers-vuex
const {
makeServicePlugin,
makeAuthPlugin,
BaseModel,
models,
clients,
FeathersVuex
} = feathersVuex(feathersClient, {
serverAlias: 'api', // or whatever that makes sense for your project
idField: '_id' // `id` and `_id` are both supported, so this is only necessary if you're using something else.
})
// Note that if you want to
// extend the BaseClass for the rest of the app, this is a great place to do it.
// After you've extended the BaseClass with your CustomClass, export it, here.
class CustomBaseModel extends BaseModel {
// Optionally add custom functionality for all services, here.
}
// Export all of the utilities for the rest of the app.
export {
makeAuthPlugin,
makeServicePlugin,
BaseModel,
models,
clients,
FeathersVuex,
CustomBaseModel // Don't forget to export it for use in all other services.
}
# Auth plugin changes
With FeathersJS version 4, the user is returned in the authentication response, by default. This means that it's no longer required to provide a userService
option to populate the user. 👍
If you would like to enable backwards compatibility with the previous version of Feathers, pass the below code in the makeAuthPlugin.
makeAuthPlugin({
userService: 'users',
actions: {
responseHandler({ commit, state, dispatch }, response) {
if (response.accessToken) {
commit('setAccessToken', response.accessToken)
// Decode the token and set the payload, but return the response
return feathersClient.passport
.verifyJWT(response.accessToken)
.then(payload => {
commit('setPayload', payload)
let user = response[state.responseEntityField]
// If a user was returned in the authenticate response, use that user.
if (user) {
if (state.serverAlias && state.userService) {
const Model = Object.keys(models[state.serverAlias])
.map(modelName => models[state.serverAlias][modelName])
.find(model => model.servicePath === state.userService)
if (Model) {
user = new Model(user)
}
}
commit('setUser', user)
// Populate the user if the userService was provided
} else if (
state.userService &&
payload.hasOwnProperty(state.entityIdField)
) {
return dispatch(
'populateUser',
payload[state.entityIdField]
).then(() => {
commit('unsetAuthenticatePending')
return response
})
} else {
commit('unsetAuthenticatePending')
}
return response
})
// If there was not an accessToken in the response, allow the response to pass through to handle two-factor-auth
} else {
return response
}
}
}
The above code will override the responseHandler
auth action to work with the Passport-based version of Feathers Authentication.
# Gotchas
# Don't Perform Queries (Side Effects) in Getters
Don't try to perform a query from within a getter like the example, below. It will result in an infinite loop:
get user () {
if (this.userId) {
const user = Models.User.getFromStore(this.userId)
// Fetch the User record if we don't already have it
if (!user) {
Models.User.get(this.userId)
}
return user
} else {
return null
}
}
# Using custom query parameters
There are two places where the query operators have to be allowed.
- In the Feathers Client (for the actions): refer to the FeathersJS docs for
whitelist
ing operators. - Inside feathers-vuex (for the getters): Check out the
paramsForServer
andwhitelist
options forfeathers-vuex
. Both accept an array of strings representing prop names, but now I can't remember why I determined that I needed both. 😃
For the Feathers Client, follow the FeathersJS docs for your database adapter.
← Nuxt