0.6 to 0.7 Migration Guide
This guide will help you migrate your code from rstore v0.6 to v0.7. There are a lot of breaking changes, so please read the entire guide carefully and test your code thoroughly after the migration.
Those changes are necessary to prepare the library for the 1.0 release. 🙇♂️
Model renamed to Collection
The term model
has been replaced with collection
throughout the rstore codebase and API to better reflect its purpose and avoid confusion with Vue's own defineModel
function and v-model
API.
// Schema
defineDataModel({ name: 'todos' })
defineCollection({ name: 'todos' })
defineItemType<Todo>().model({ name: 'todos' })
withItemType<Todo>().defineCollection({ name: 'todos' })
// Advanced APIs
store.$models.find(m => m.name === 'Users')
store.$collections.find(c => c.name === 'Users')
store.$model('Users').peekFirst('some-id')
store.$collection('Users').peekFirst('some-id')
store.$getModel({ __typename: 'Users ', id: 'some-id'})
store.$getCollection({ __typename: 'Users ', id: 'some-id'})
// Plugins
hook('fetchFirst', async ({ model, key, params }) => {
hook('fetchFirst', async ({ collection, key, params }) => {
// ...
})
TIP
The main APIs such as store.Users.peekFirst()
are not impacted because the collection name is used directly.
Store setup
defineDataModel
renamed to defineCollection
Why this change?
The term model
has been replaced with collection
throughout the rstore codebase and API to better reflect its purpose and avoid confusion with Vue's own defineModel
function and v-model
API.
import { defineDataModel } from '@rstore/vue'
import { defineCollection } from '@rstore/vue'
const todoCollection = defineDataModel({
const todoCollection = defineCollection({
name: 'todos',
})
defineItemType
renamed to withItemType
Why this change?
The defineItemType
function has been renamed to withItemType
(and its method from model
to defineCollection
) to better reflect its purpose of defining the collection with the defineCollection
method that is chained to it.
import { defineItemType } from '@rstore/vue'
import { withItemType } from '@rstore/vue'
interface Todo {
id: string
title: string
completed: boolean
}
const todoCollection = defineItemType<Todo>().model({
const todoCollection = withItemType<Todo>().defineCollection({
name: 'todos',
})
Use RStoreSchema
in Nuxt
You need to use the RStoreSchema
auto-imported namespace to define collections and relations.
Why this change?
This change was made to avoid potential naming conflicts with other libraries or your own code. It also makes it clearer that those functions are part of the rstore library to define your store schema.
const Users = defineItemType<User>().model({
const Users = RStoreSchema.withItemType<User>().defineCollection({
name: 'users',
})
models
renamed to schema
Why this change?
Since it now contains both collections (previously models) and relations (see below) - and maybe other objects in the future, the models
option has been renamed to schema
.
const store = await createStore({
models: [
schema: [
{ name: 'todos' },
{ name: 'users' },
],
plugins: [],
})
Use the RstorePlugin
You should now use the RstorePlugin
from @rstore/vue
to install the store in your Vue app. This will automatically provide the store to all components and make it available via the new useStore
function from @rstore/vue
.
Why this change?
This change simplifies the store setup and makes it easier to use the store in your components. It also makes it possible to use the new useStore
function from @rstore/vue
directly without having to create your own.
import type { VueStore } from '@rstore/vue'
import type { InjectionKey } from 'vue'
import { RstorePlugin } from '@rstore/vue'
const injectStoreKey = Symbol('rstore') as InjectionKey<VueStore<typeof schema>>
export async function rstore(app: App) {
const store = await createStore({
// ...
})
app.provide(injectStoreKey, store)
app.use(RstorePlugin, { store })
}
export function useStore () {
const store = inject(injectStoreKey, null)
if (store == null) {
throw new Error('No rstore provided.')
}
return store
}
// Augment the `useStore` type
declare module '@rstore/vue' {
export function useStore(): VueStore<typeof schema>
}
useStore()
You can now use the new useStore
function from @rstore/vue
directly instead of creating your own.
Why this change?
This change simplifies the store usage in your components and makes it easier to get the store instance without having to create your own useStore
function.
import { useStore } from 'your-own-path/rstore'
import { useStore } from '@rstore/vue'
Relation definitions shape changed
Instead of defining the related fields using on
and eq
that were confusing, you now define the related fields using an on
object that maps the foreign fields to the local fields with the name of the collections to make it clearer.
Why this change?
This new syntax is more explicit and easier to understand. It also allows for more complex relations such as mapping multiple fields or using a custom filter function.
import { defineItemType } from '@rstore/vue'
interface User {
id: string
name: string
}
const userCollection = defineItemType<User>().defineCollection({
name: 'users',
relations: {
receivedMessages: {
to: {
messages: {
on: 'recipientId', // Message.recipientId
eq: 'id', // User.id
on: {
'messages.recipientId': 'users.id',
},
},
},
many: true,
},
sentMessages: {
to: {
messages: {
on: 'senderId', // Message.senderId
eq: 'id', // User.id
on: {
'messages.senderId': 'users.id',
},
},
},
many: true,
},
},
})
interface Message {
id: string
content: string
senderId: string
recipientId: string
}
const messageCollection = defineItemType<Message>().defineCollection({
name: 'messages',
relations: {
sender: {
to: {
users: {
on: 'senderId', // Message.senderId
eq: 'id', // User.id
on: {
'users.id': 'messages.senderId',
},
},
},
},
recipient: {
to: {
users: {
on: 'recipientId', // Message.recipientId
eq: 'id', // User.id
on: {
'users.id': 'messages.recipientId',
},
},
},
},
},
})
This new syntax also allows you to map multiple fields on a single relation:
const myCollection = {
name: 'things',
relations: {
relatedItems: {
to: {
otherCollection: {
on: {
'otherCollection.type': 'things.relatedItemType',
'otherCollection.subType': 'things.relatedItemSubType',
},
},
},
},
},
}
Or specify a custom filter function:
const myCollection = {
name: 'things',
relations: {
relatedItems: {
to: {
otherCollection: {
on: {
// 'otherCollection.type': 'things.relatedItemType',
},
filter: (item, relationItem) =>
item.relatedStatusList.includes(relationItem.status),
},
},
},
},
}
If you use TypeScript, it is now recommended to use the new defineRelations
helper to define your relations with proper typing:
import type { StoreSchema } from '@rstore/vue'
import { defineItemType, defineRelations } from '@rstore/vue'
interface User {
id: string
name: string
}
const userCollection = defineItemType<User>().defineCollection({
name: 'users',
})
interface Message {
id: string
content: string
senderId: string
recipientId: string
}
const messageCollection = defineItemType<Message>().defineCollection({
name: 'messages',
})
const userRelations = defineRelations(userCollection, ({ collection }) => ({
receivedMessages: {
to: collection(messageCollection, {
on: {
'messages.recipientId': 'users.id', // Type checked!
},
}),
many: true,
},
sentMessages: {
to: collection(messageCollection, {
on: {
'messages.senderId': 'users.id', // Type checked!
},
}),
many: true,
},
}))
const messageRelations = defineRelations(messageCollection, ({ collection }) => ({
sender: {
to: collection(userCollection, {
on: {
'users.id': 'messages.senderId', // Type checked!
},
}),
},
recipient: {
to: collection(userCollection, {
on: {
'users.id': 'messages.recipientId', // Type checked!
},
}),
},
}))
export const schema = [
userCollection,
messageCollection,
userRelations,
messageRelations,
] as const satisfies StoreSchema
Example of the typechecking in action:
Nuxt module now looks for exported consts
Instead of using the default export as an array of collections, the Nuxt module now looks for all exported consts in the files and merges them into a single array.
Why this change?
The export default [...]
syntax at the end of the files was not very ergonomic and could lead to mistakes (for example forgetting to add the new collection to the array).
const Users = defineItemType<User>().model({
export const Users = defineItemType<User>().defineCollection({
name: 'users',
})
const Messages = defineItemType<Message>().model({
export const Messages = defineItemType<Message>().defineCollection({
name: 'messages',
})
export default [
Users,
Messages,
]
Nuxt StoreResolvedModdelItem
renamed to StoreResolvedCollectionItem
import type { StoreResolvedModelItem } from '#imports';
import type { StoreResolvedCollectionItem } from '#imports';
export type UserResolvedItem = StoreResolvedModelItem<'users'>;
export type UserResolvedItem = StoreResolvedCollectionItem<'users'>;
Query
Methods don't accept Ref or getter anymore
The following methods no longer accept a Ref or a getter function as argument. You must now always pass a plain object (or string/number for single key):
peekFirst
peekMany
findFirst
findMany
store.User.peekFirst(() => 'toto')
store.User.peekFirst('toto')
store.User.peekFirst(userIdRef)
store.User.peekFirst(userIdRef.value)
Why this change?
This change was made to simplify the API and avoid confusion. Those methods are not reactive and will not update automatically when the Ref or the result of the getter function changes. If you want to use a Ref or a getter function, you can use the query
or liveQuery
methods instead.
New Query API
The following methods have been removed:
queryFirst
queryMany
liveQueryFirst
liveQueryMany
They have been replaced by the following methods:
query
liveQuery
Those two new methods accept a function that receives a query builder as argument. You can then use the first
or many
methods of the query builder to build your query.
Why this change?
This new syntax allows TypeScript to fully type check the entire options object, including verifying for excess properties in deeply nested objects. The old syntax was not type checked properly and your code could easily contain typos or mistakes that would not be caught by the compiler. This is due to a limitation in TypeScript (see microsoft/TypeScript#241).
const { data: user } = await store.User.queryFirst('toto')
const { data: user } = await store.User.query(q => q.first('toto'))
const { data: user } = await store.User.queryFirst(() => ({
const { data: user } = await store.User.query(q => q.first({
key: userId.value,
include: { receivedMessages: true },
}))
const { data: messages } = await store.Message.queryMany(() => ({
const { data: messages } = await store.Message.query(q => q.many({
filter: m => m.recipientId === userId.value,
}))
const { data: messages } = await store.Message.liveQueryMany(() => ({
const { data: messages } = await store.Message.liveQuery(q => q.many({
filter: m => m.recipientId === userId.value,
}))
WARNING
Since the type checking is now stricter, you might encounter new type errors in your code that you didn't have before. This means that your code was not fully correct and you should fix the errors. If somethig looks wrong, please open an issue. 🙏
Subscribe method now accepts query builder
The subscribe
method now only accepts a function that receives a query builder as argument. You then have to call the query builder function with the options object.
const { unsubscribe } = store.Message.subscribe({
const { unsubscribe } = store.Message.subscribe(q => q({
params: {
recipientId: 'user-123',
},
})
}))
const { unsubscribe } = store.Message.subscribe(() => ({
const { unsubscribe } = store.Message.subscribe(q => q({
params: {
recipientId: userId.value,
},
}))
Why this change?
This new syntax allows TypeScript to fully type check the entire options object, including verifying for excess properties in deeply nested objects. The old syntax was not type checked properly and your code could easily contain typos or mistakes that would not be caught by the compiler. This is due to a limitation in TypeScript (see microsoft/TypeScript#241).
WARNING
Since the type checking is now stricter, you might encounter new type errors in your code that you didn't have before. This means that your code was not fully correct and you should fix the errors. If somethig looks wrong, please open an issue. 🙏
Customizing types
The types in rstore changed, so you need to update your type augmentations for:
CustomCacheState
CustomCollectionMeta
(renamed fromCustomModelMeta
)CustomFilterOption
CustomHookMeta
CustomParams
CustomPluginMeta
CustomSortOption
FindOptions
Why this change?
Model
has been renamed to Collection
and Model List
to Schema
throughout the rstore codebase.
import { Model, ModelDefaults, ModelList } from '@rstore/core'
import { Collection, CollectionDefaults, StoreSchema } from '@rstore/vue'
declare module '@rstore/vue' {
export interface FindOptions<
TModel extends Model,
TModelDefaults extends ModelDefaults,
TModelList extends ModelList,
TCollection extends Collection,
TCollectionDefaults extends CollectionDefaults,
TSchema extends StoreSchema,
> extends FindOptionsBase<TModel, TModelDefaults, TModelList> {
> extends FindOptionsBase<TCollection, TCollectionDefaults, TSchema> {
// Your custom properties here
}
}
export {}
Modules
New Module definition syntax
The way modules are defined has changed:
defineModule
now accepts the module name as first argument and a setup function as second argument.- The setup function receives a payload as the first argument that contains:
store
: the store instancedefineState
: function to define reactive state (automatically hydrated after SSR)defineMutation
: function to define mutationsonResolve
: function to register a (optionally async) callback to be called when the module is resolved
resolve
function has been removed. You can now simply return the module's public API from the setup function.createModule
has been removed. You can now use thestore
instance and thedefineState
function directly in your module from the setup function parameter.- You must now setup the store with the RStorePlugin from
@rstore/vue
(see above) so the store is correctly injected.
Why this change?
This new syntax is much simpler and more consistent with other libraries such as pinia
.
import { useStore } from 'path/to/store'
import { defineModule, createModule } from '@rstore/vue'
import { defineModule } from '@rstore/vue'
export const useAuth = defineModule(() => {
export const useAuth = defineModule('auth', ({
store,
defineState,
defineMutation,
onResolve,
}) => {
const store = useStore()
const { state, resolve, onResolve, defineMutation } = createModule(store, {
name: 'auth',
state: {
currentUserKey: null as string | null,
},
})
const state = defineState({
currentUserKey: null as string | null,
})
const currentUser = store.User.query(q => q.first(state.currentUserKey
? {
key: state.currentUserKey,
}
: {
enabled: false,
}))
const requestFetch = useRequestFetch()
async function initCurrentUser() {
try {
const user = await requestFetch('/api/auth/me')
if (user) {
state.currentUserKey = user.id
store.User.writeItem({
...user,
createdAt: new Date(user.createdAt),
})
}
else {
state.currentUserKey = null
}
}
catch (e) {
console.error('Failed to init current user', e)
}
}
const login = defineMutation(async (email: string, password: string) => {
const result = await $fetch('/api/auth/login', {
method: 'POST',
body: {
email,
password,
},
})
state.currentUserKey = result.userId
})
const logout = defineMutation(async () => {
await $fetch('/api/auth/logout', {
method: 'POST',
})
state.currentUserKey = null
})
onResolve(async () => {
await initCurrentUser()
})
return resolve({
return {
currentUser,
login,
logout,
loggedIn: computed(() => !!state.currentUserKey),
})
}
})
Module mutation state not longer Refs
The mutations created with defineMutation
have a few useful properties to track their state such as $loading
and $error
. Previously, those properties were Refs, but they are now plain values (while still reactive).
Why this change?
This change was made to simplify the usage of those properties and avoid the need to access the .value
property.
<script setup lang="ts">
const auth = useAuth() // A module defined with defineModule
</script>
<template>
<UAlert
v-if="auth.login.$error.value"
v-if="auth.login.$error"
:description="auth.login.$error.value.message"
:description="auth.login.$error.message"
color="error"
/>
<UButton
type="submit"
:loading="auth.login.$loading.value"
:loading="auth.login.$loading"
/>
</template>
Plugins
model
renamed to collection
in payload
Why this change?
The term model
has been replaced with collection
throughout the rstore codebase and API to better reflect its purpose and avoid confusion with Vue's own defineModel
function and v-model
API.
hook('fetchFirst', async ({ model, key, params }) => {
hook('fetchFirst', async ({ collection, key, params }) => {
// ...
})
Advanced APIs
$models
renamed to $collections
store.$models.find(m => m.name === 'Users')
store.$collections.find(c => c.name === 'Users')
$model
renamed to $collection
store.$model('Users').peekFirst('some-id')
store.$collection('Users').peekFirst('some-id')
$getModel
renamed to $getCollection
store.$getModel({ __typename: 'Users ', id: 'some-id'})
store.$getCollection({ __typename: 'Users ', id: 'some-id'})
$cache.clearModel
renamed to $cache.clearCollection
store.$cache.clearModel('Users')
store.$cache.clearCollection('Users')
addModel
and removeModel
renamed to addCollection
and removeCollection
import { addModel, removeModel } from '@rstore/vue'
import { addCollection, removeCollection } from '@rstore/vue'
addModel(store, { name: 'newModel' })
addCollection(store, { name: 'newCollection' })
removeModel(store, 'oldModel')
removeCollection(store, 'oldCollection')