Auto-generated Collections
Collections are automatically generated from your Drizzle schema (including relations), alongside the necessary API and plugin.
Collections are automatically generated from your Drizzle schema (including relations), alongside the necessary API and plugin.
All collections are fully typed using your Drizzle schema.
Easily restrict the tables exposed through the API with the `allowTables` function.
Run code server-side before or after certain actions on the tables using hooks.
In case you are using Drizzle, you can install the @rstore/nuxt-drizzle
module instead of @rstore/nuxt
to automatically generate the collections and plugins from your drizzle schema.
npm i @rstore/nuxt-drizzle
export default defineNuxtConfig({
modules: [
'@rstore/nuxt-drizzle',
],
})
Important Notice
By default the module will attempt to import useDrizzle
from ~~/server/utils/drizzle
that should return a drizzle instance.
Example:
// server/utils/drizzle.ts
import { drizzle } from 'drizzle-orm/libsql'
let drizzleInstance: ReturnType<typeof drizzle> | null = null
export function useDrizzle() {
drizzleInstance ??= drizzle({
connection: { url: useRuntimeConfig().dbUrl },
casing: 'snake_case',
})
return drizzleInstance
}
You can customize this in the rstoreDrizzle.drizzleImport
option in the Nuxt config.
export default defineNuxtConfig({
modules: [
'@rstore/nuxt-drizzle',
],
rstoreDrizzle: {
drizzleImport: {
name: 'useDb',
from: '~~/server/useDb',
},
},
})
The module will automatically:
drizzle.config.ts
file (configurable with the rstoreDrizzle.drizzleConfigPath
option in the Nuxt config),/api/rstore
path to handle the CRUD operations,Example drizzle schema:
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export const todos = sqliteTable('todos', {
id: integer().primaryKey({ autoIncrement: true }),
title: text().notNull(),
completed: integer().notNull().$defaultFn(() => 0),
createdAt: integer({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
updatedAt: integer({ mode: 'timestamp' }),
})
Example drizzle config:
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
dialect: 'sqlite',
schema: './server/database/schema.ts',
out: './server/database/migrations',
casing: 'snake_case',
})
You can already use the store in your components without any additional configuration:
<script setup>
const store = useStore()
const { data: todos } = await store.todos.query(q => q.many())
</script>
<template>
<pre>{{ todos }}</pre>
</template>
Collection Names
The collection names are infered from the exported variable names in the drizzle schema, not the table names.
TIP
You can use nitro middlewares to add authentication to the API, for example in a server/middleware/auth.ts
file.
You can use the where
option of any query to filter the results using drizzle's operators such as eq
, gt
, lt
, etc. (which are auto-imported). Since rstore is local-first, it will also compute the where clause on the client side.
The supported operators are defined here (drizzle docs).
<script lang="ts" setup>
const store = useStore()
const email = ref('')
const { data: users } = await store.users.query(q => q.many({
where: email.value ? eq('email', email.value) : undefined,
}))
</script>
INFO
Please note that only simple filters are supported - you can't do joins or subqueries inside where
.
You can use the include
option to include related collections in the query. Learn more here.
<script lang="ts" setup>
const store = useStore()
const { data: users } = await store.users.query(q => q.many({
include: {
posts: true,
},
}))
</script>
By default, all tables in your Drizzle schema are exposed through the API. You can restrict the tables that are exposed by using the allowTables
function.
TIP
Put this code in a Nitro plugin in server/plugins
so it's executed once when the server starts.
import * as tables from 'path-to-your-drizzle-schema'
export default defineNitroPlugin(() => {
allowTables([
tables.todos,
])
})
Any table that is not explicitly listed will throw on all API endpoints. allowTables
can be called multiple times, and the allowed tables will be merged.
You can use hooks to run code before or after certain actions on the collections. You can register global hooks for all collections using the rstoreDrizzleHooks
import, or specific hooks for a given table using the hooksForTable
function (recommended).
TIP
Put this code in a Nitro plugin in server/plugins
so it's executed once when the server starts.
You can use the following hooks:
index.get.before
- before fetching a list of itemsindex.get.after
- after fetching a list of itemsindex.post.before
- before creating a new itemindex.post.after
- after creating a new itemitem.get.before
- before fetching a single itemitem.get.after
- after fetching a single itemitem.patch.before
- before updating a single itemitem.patch.after
- after updating a single itemitem.delete.before
- before deleting a single itemitem.delete.after
- after deleting a single itemIf you throw an error in a before
hook, the action will be aborted and the error will be returned to the client.
import * as tables from 'path-to-your-drizzle-schema'
export default defineNitroPlugin(() => {
hooksForTable(tables.todos, {
'index.get.before': async (payload) => {
console.log('Specific hook for todos - index.get.before', payload.collection, payload.query, payload.params)
},
'index.get.after': async (payload) => {
console.log('Specific hook for todos - index.get.after', payload.collection, payload.result.map(r => r.id))
},
'item.patch.after': async (payload) => {
console.log('Specific hook for todos - item.patch.after', payload.collection, payload.result.id)
},
})
})
export default defineNitroPlugin(() => {
rstoreDrizzleHooks.hook('index.get.before', async (payload) => {
console.log('index.get.before', payload.collection, payload.query, payload.params)
})
rstoreDrizzleHooks.hook('index.get.after', async (payload) => {
console.log('index.get.after', payload.collection)
})
rstoreDrizzleHooks.hook('index.post.before', async (payload) => {
console.log('index.post.before', payload.collection, payload.body)
})
rstoreDrizzleHooks.hook('index.post.after', async (payload) => {
console.log('index.post.after', payload.collection)
})
rstoreDrizzleHooks.hook('item.get.before', async (payload) => {
console.log('item.get.before', payload.collection, payload.params)
})
rstoreDrizzleHooks.hook('item.get.after', async (payload) => {
console.log('item.get.after', payload.collection)
})
rstoreDrizzleHooks.hook('item.patch.before', async (payload) => {
console.log('item.patch.before', payload.collection, payload.params, payload.body)
})
rstoreDrizzleHooks.hook('item.patch.after', async (payload) => {
console.log('item.patch.after', payload.collection)
})
rstoreDrizzleHooks.hook('item.delete.before', async (payload) => {
console.log('item.delete.before', payload.collection, payload.params)
})
rstoreDrizzleHooks.hook('item.delete.after', async (payload) => {
console.log('item.delete.after', payload.collection)
})
})
Throwing an error in a before
hook to prevent the action:
export default defineNitroPlugin(() => {
hooksForTable(tables.projects, {
'index.post.before': async ({ body }) => {
const session = await requireUserSession(event)
// Check that the user is a member of the team
// they are trying to create the project for
const teamId = body.teamId
// Check that the user is a member of the team
const membership = await useDrizzle()
.select()
.from(tables.teamsToUsers)
.where(and(
eq(tables.teamsToUsers.teamId, teamId),
eq(tables.teamsToUsers.userId, session.user.id),
))
.limit(1)
.get()
if (!membership) {
throw createError({
statusCode: 403,
statusMessage: 'You are not a member of this team'
})
}
},
})
})
By adding a SQL condition to the where
clause of a query:
export default defineNitroPlugin(() => {
hooksForTable(tables.projects, {
'index.get.before': async ({ event, transformQuery }) => {
const session = await requireUserSession(event)
// Create a subquery to restrict the projects
// to those that belong to teams the user is a member of
const sq = useDrizzle()
.select({ id: tables.projects.id })
.from(tables.teamsToUsers)
.innerJoin(tables.projects, eq(tables.projects.teamId, tables.teamsToUsers.teamId))
.where(eq(tables.teamsToUsers.userId, session.user.id))
// Use the subquery in the main query
transformQuery(q => q.where(inArray(tables.projects.id, sq)))
},
})
})
Automatically execute a query after a specific action:
export default defineNitroPlugin(() => {
hooksForTable(tables.teams, {
'index.post.after': async ({ event, result }) => {
const session = await requireUserSession(event)
// Add the user as a member of the newly created team
await useDrizzle().insert(tables.teamsToUsers).values({
teamId: result.id,
userId: session.user.id,
})
},
})
})
Customize the path to the API. By default it will be /api/rstore
.
export default defineNuxtConfig({
modules: [
'@rstore/nuxt-drizzle',
],
rstoreDrizzle: {
apiPath: '/api/my-api',
},
})
Customize the path to the drizzle config file. By default it will look for a drizzle.config.ts
file in the root of your project.
export default defineNuxtConfig({
modules: [
'@rstore/nuxt-drizzle',
],
rstoreDrizzle: {
drizzleConfigPath: 'drizzle.config.ts',
},
})
Customize the import path for the drizzle instance. By default it will look for a useDrizzle
function in the ~~/server/utils/drizzle.ts
file.
export default defineNuxtConfig({
modules: [
'@rstore/nuxt-drizzle',
],
rstoreDrizzle: {
drizzleImport: {
default: { name: 'useDb', from: '~~/server/useDb' },
},
},
})