# 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

  1. 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.
  2. The exports have changed.
  • (a) A new BaseModel is available. This is the base FeathersVuexModel which contains the model methods. Feel free to extend it and make it fit your awesome services!
  • (b) The service method has been renamed to makeServicePlugin.
  • (c) The auth method is now called makeAuthPlugin
  • (d) The models object is now exported, so you can access them from anywhere. They are keyed by serverAlias.
  • (e) A new clients object is available. The intention is to allow working with multiple FeathersJS API servers.
  1. You no longer pass a servicePath to create a service-plugin. Instead, pass the actual Feathers service.
  2. 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 pass params.data to the patch object to implement partial patching on objects. You might choose to use params.data instead of diffOnPatch.

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 whitelisting operators.
  • Inside feathers-vuex (for the getters): Check out the paramsForServer and whitelist options for feathers-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.