Data Modeling
Feathers-Pinia v4.2 introduced some FeathersPinia methods to help form relationships between services inside your setupInstance functions. Let's review what they do before seeing a full example.
Note
In all of the below examples, the exported books object can be passed to the services config when calling createPiniaClient.
pushToStore
The FeathersPinia client's pushToStore method pushes data into a related store.
import { PiniaServiceConfig } from 'feathers-pinia'
export const books: PiniaServiceConfig = {
setupInstance(data: any, { app }: any) {
// replace data.pages with stored pages
data.pages = app.pushToStore(data.pages, 'pages')
return data
},
}The above example uses app.pushToStore to replace data.pages with the pages in the pages service store. If the data didn't exist already, it will be added. If it did exist, the store record will be patched with any new data. The data.pages array will be a static array, not adjusting its length when new data arrives. So we need another utility to make it reactive: defineVirtualProperty.
defineVirtualProperty
The FeathersPinia client's defineVirtualProperty method sets up a virtual property on an object. We can use it to define reactive properties on instances. In the following example, we no longer replace data.pages with stored pages. Instead, we overwrite the pages property with a virtual getter that returns the stored pages.
Note
if you're defining more than one virtual property, use defineVirtualProperties instead.
import { PiniaServiceConfig } from 'feathers-pinia'
export const books: PiniaServiceConfig = {
setupInstance(data: any, { app }: any) {
// store the page records
app.pushToStore(data.pages, 'pages')
// define a findInStore virtual property
app.defineVirtualProperty(data, 'pages', (item: any) => {
return app.service('pages').findInStore({ query: { book_id: item.id } }).data
})
// store the creator record
app.pushToStore(data.creator, 'users')
// define a getFromStore virtual property
app.defineVirtualProperty(data, 'creator', (item: any) => {
return app.service('users').getFromStore(item.created_by)
})
return data
},
}All virtual properties are lazily evaluated (they only run when you reference them), making them very lightweight. Virtual properties are non-enumerable, so they'll never be accidentally sent to the API server. But you could create a client-side Feathers hook to send them if you wanted to.
It's a bit verbose when you need to define more than one virtual property, so let's instead define many at once with defineVirtualProperties
defineVirtualProperties
The FeathersPinia client's defineVirtualProperties method sets up multiple virtual properties on an object. We can use it to define lots of reactive properties on our instances. In the following example, we define two virtual properties: pages and creator.
import { PiniaServiceConfig } from 'feathers-pinia'
export const books: PiniaServiceConfig = {
setupInstance(data: any, { app }: any) {
// store related data
app.pushToStore(data.pages, 'pages')
app.pushToStore(data.creator, 'users')
// overwrite the original properties with virtual properties
app.defineVirtualProperties(data, {
// findInStore example
pages: (item: any) => {
return app.service('pages').findInStore({ query: { book_id: item.id } }).data
},
// getFromStore example
creator: (item: any) => {
return app.service('users').getFromStore(item.created_by)
},
})
return data
},
}Since we used store methods, the pages and creator properties are computed properties. Remember, the data property returned from findInStore is a computed property, and so is the value returned by getFromStore. As mentioned earlier, Virtual properties are non-enumerable, so they'll never be accidentally sent to the API server. But you could create a client-side Feathers hook to send them if you wanted to.
Complete Example with Types
This example begins by showing how to use the ServiceInstance type to define a Book type.
import { type ServiceInstance, type PiniaServiceConfig, defineVirtualProperties, pushToStore, useInstanceDefaults } from 'feathers-pinia'
import type { UserWithIncludes } from './users'
import type { PageWithIncludes } from './pages'
// Define the `Book` type
export interface Book {
id?: string
title: string
description?: string
created_by?: string
created_at?: number
updated_at?: number
}
// Define types for related data
export interface BookIncludes {
pages: PageWithIncludes[]
creator: UserWithIncludes
}
export type BookWithIncludes = ServiceInstance<Book & BookIncludes>
// Create the books service configuration
export const books: PiniaServiceConfig = {
setupInstance(data: BookWithIncludes, { app }: any) {
data = useInstanceDefaults({
title: '🐸 Book',
}, data)
// move related data to the correct stores
app.pushToStore(data.pages, 'pages')
app.pushToStore(data.creator, 'users')
// virtual properties
app.defineVirtualProperties(data, {
// findInStore example
pages: (item: Book) => {
return app.service('pages').findInStore({ query: { book_id: item.id } }).data
},
// getFromStore example
creator: (item: Book) => {
return app.service('users').getFromStore(item.created_by)
},
})
return data
},
}Review the Benefits
Let's review the benefits:
- Segregating our data into service stores organizes it to be flexibly reused throughout our applications. The
pushToStoreutility does it elegantly. - Defining reactive virtual properties on our instances puts related data directly on each instance. Having ready access to related data is a huge timesaver and prevents writing a lot of boilerplate code in our components.
- This sort of functional declarative code is concise, clear, and testable. It's a joy to work with.