🎁 Upgrade to Feathers-Pinia 2.0 🎁
Feathers-Pinia 2.0 is almost ready for final release. Read the new documentation.Model Classes
Each Service Store gets its own Model Class. If you don't explicitly create it, one gets created under the hood. There are some benefits to using them:
- Convenient access to Feathers Service methods. Methods directly on the Model will effect the store. You can also directly access the Feathers service at
Model.service
. Using the Feathers Service directly allows you to bypass the store when needed. - The Model Class API provides a common interface that abstracts away the underlying implementation. This is similar to how FeathersJS database adapters work. FeathersJS supports many database adapters. By swapping out an adapter, the same code that was previously running on one database now runs on some other database.
- You can extend the common interface with custom methods, getters, and setters.
Model Class Optional
On the section on Setup: Service Stores, we learned how to setup a basic service store with a Model class.
In Feathers-Pinia, model classes are not required in all cases. Model classes are especially beneficial to
- Define Relationships with other stores 🥰
- Define default data. This is especially important if we add support for Vue 2.
- Be able to use the
new
operator. No Model class means tonew
.
If neither of the above scenarios applies to your situation, you can shorten the service setup and remove the Model
.
import { defineStore, BaseModel } from '../pinia'
import { api } from '../feathers'
const servicePath = 'users'
export const useUsers = defineStore({ servicePath })
api.service(servicePath).hooks({})
If you don't provide a Model class, one will be created dynamically using the servicePath as the class name. This means that you can still take advantage of instance methods! It's pretty convenient!
Working without a model class
One caveat about working WITHOUT a Model class is that you can't use the new
operator (you know, since that requires a class;). To add an item to the store, pass an object to the addToStore
action.
<script setup lang="ts">
import { useUsers } from '~/store/users.pinia.ts'
const usersService = useUsers()
usersService.addToStore({ id: 0, name: 'Marshall' })
</script>
Default Class Properties
Another potential caveat with using Model classes in Feathers-Pinia is that any default values defined on a class will override and overwrite the values provided in instanceDefaults
UNLESS you assign them again in the extending class's constructor. Read the comment and string values in the next example for more information.
import { defineStore, BaseModel } from '../pinia'
class Message extends BaseModel {
// This doesn't work as a default value. It will overwrite all passed-in values and always be this value.
text = 'The text in the model always wins. You can only overwrite it after instantiation'
static instanceDefaults(data: Message) {
return {
text: 'this gets overwritten by the class-level `text`',
otherText: `this won't get overwritten and works great for a default value`,
}
}
}
const message = new Message({ text: 'hello there!' })
console.log(message.text) // --> 'The text in the model always wins. You can only overwrite it after instantiation'
Notice in the above example how even though we've provided text: 'hello there!'
to the new message, the value ends up being the default value defined in the class definition. This is an important part of how extending classes works in JavaScript. If you definitely require to define instance properties inside the class definition, the workaround is to add a constructor
to the class and re-assign the properties in the same way that the BaseModel
constructor does it. Here's what it looks like:
import { defineStore, BaseModel } from '../pinia'
import { models } from 'feathers-pinia'
import type { ModelStatic } from 'feathers-pinia'
class Message extends BaseModel {
// This doesn't work as a default value. It will overwrite all passed-in values and always be this value.
text = 'The text in the model always wins. You can only overwrite it after instantiation'
constructor(data: any, options: any = {}) {
// You must call `super` very first to instantiate the BaseModel
super(data, options)
const constructor = this.constructor as ModelStatic<BaseModel>
const { store, instanceDefaults, setupInstance } = constructor
// Assign the default values again, because you can override this class's defaults inside this class's `constructor`.
Object.assign(this, instanceDefaults.call(constructor, data, { models, store })) // only needed when this class implements `instanceDefaults`
setupInstance.call(constructor, this, { models, store }) // only needed when this class implements `setupInstance`
return this
}
static instanceDefaults(data: Message, store: any) {
return {
text: 'gets overwritten by the class-level `text`',
otherText: `this works great for a default value because there's not a default initialized at the class level. But this could also be moved into the class definition`,
}
}
}
const message = new Message({ text: 'hello there!' })
console.log(message.text) // --> 'hello there!'
But note that in the example, above, you probably don't need to use instanceDefaults, anymore. If you define a default value for the otherText
property inside of the Model class, you can remove the static instanceDefaults
function, completely. You may or may not want to still use the setupInstance
method for setting up related data in other stores.
Recipes
Compound Keys
I've not tried this, but it might be possible to support compound keys in the idField
of a service by following these steps:
- Create a custom class (
extends BaseModel
) - Add a virtual property to the class using ES5 accessors
get
andset
. - Use the name of the virtual property as the
idField
in thedefineStore
options.
It would look something like the example, below.
import { defineStore, BaseModel } from 'feathers-pinia'
import { Id } from '@feathersjs/feathers'
import { api } from '../feathers'
// (1)^
export class User extends BaseModel {
static instanceDefaults() {
return {
name: '',
timezone: '',
}
}
get myCompoundKey() {
return `${this.name}:${this.timezone}`
}
// This would be necessary if you were going to manually set the key on the frontend.
set myCompoundKey(val: string) {
const [name, timezone] = val.split(':')
this.name = name
this.timezone = timezone
}
}
const servicePath = 'users'
export const useUsers = defineStore({
idField: 'id', // (2)
clients: { api }, // (2)
servicePath,
Model: User,
})
api.service(servicePath).hooks({})
If you do try it out, please let me know in an issue or make a PR to the docs. If it works, the wording of this section should be updated with more certainty. 🤓