This skill provides Drizzle ORM patterns and database conventions for the fitness app. Use when creating queries, implementing RLS policies, or working with database schemas.
/plugin marketplace add JosephAnson/claude-plugin/plugin install josephanson-ja@JosephAnson/claude-pluginThis skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill provides comprehensive patterns for database operations using Drizzle ORM in the fitness application.
Separation of Concerns: All database logic MUST be in /server/database/queries/ files. Never use useDB() directly in API routes.
Type Safety: Let Drizzle infer return types. Do not add explicit Promise<...> return types to query functions.
No Error Handling in Queries: Never use try/catch in database query functions. Let errors bubble up to the API handler.
Row Level Security: Implement and enforce RLS for all user data at the database level.
Use consistent naming to indicate behavior:
| Operation | Function | Throws on Not Found? | Returns |
|---|---|---|---|
| Create | createX | N/A | Created record |
| Read | getX | Yes (404) | Record |
| Read | queryX | No | Record/Array/null |
| Update | updateX | Yes (404) | Updated record |
| Delete | deleteX | Yes (404) | void/deleted record |
Return data or null/[]. Never throw on "not found":
// server/database/queries/workouts.ts
export function queryWorkoutById(id: string, userId: string) {
return useDB().query.workouts.findFirst({
where: and(
eq(workouts.id, id),
eq(workouts.userId, userId)
),
})
// Returns: Workout | undefined
}
// Usage in API route
const workout = await queryWorkoutById(id, user.id)
if (!workout) {
throw createError({
statusCode: 404,
statusMessage: 'Workout not found'
})
}
Return data or throw 404 error if not found:
export async function getWorkoutById(id: string, userId: string) {
const workout = await useDB().query.workouts.findFirst({
where: and(
eq(workouts.id, id),
eq(workouts.userId, userId)
),
})
if (!workout) {
throw createError({
statusCode: 404,
statusMessage: 'Workout not found'
})
}
return workout
}
// Usage in API route
const workout = await getWorkoutById(id, user.id)
// No null check needed - throws 404 if not found
return workout
Prefer useDB().query.<table> for most selections, especially with relations:
// ✅ Correct: Using query API with relations
export function queryWorkoutWithExercises(id: string, userId: string) {
return useDB().query.workouts.findFirst({
where: and(
eq(workouts.id, id),
eq(workouts.userId, userId)
),
with: {
exercises: {
with: {
exercise: true
}
}
}
})
}
Use useDB().select().from() for complex joins or specific columns:
// ✅ Correct: Using select for complex queries
export function queryWorkoutStats(userId: string) {
return useDB()
.select({
workoutId: workouts.id,
workoutName: workouts.name,
exerciseCount: sql<number>`count(${workoutExercises.id})`,
totalVolume: sql<number>`sum(${workoutExercises.sets} * ${workoutExercises.reps} * ${workoutExercises.weight})`
})
.from(workouts)
.leftJoin(workoutExercises, eq(workouts.id, workoutExercises.workoutId))
.where(eq(workouts.userId, userId))
.groupBy(workouts.id)
}
import { type InferSelectModel, type InferInsertModel } from 'drizzle-orm'
import { workouts } from '~~/server/database/schema/workouts'
// ✅ Correct: Infer from schema
type Workout = InferSelectModel<typeof workouts>
type InsertWorkout = InferInsertModel<typeof workouts>
// Query function with inferred return type
export function createWorkout(data: InsertWorkout) {
return useDB().insert(workouts).values(data).returning()
// Return type is automatically inferred as Workout[]
}
// ✅ Correct: No explicit return type
export function queryUserWorkouts(userId: string) {
return useDB().query.workouts.findMany({
where: eq(workouts.userId, userId),
with: {
exercises: true
}
})
// Drizzle infers complex return type automatically
}
// ❌ Wrong: Explicit return type
export function queryUserWorkouts(userId: string): Promise<Workout[]> {
return useDB().query.workouts.findMany({
where: eq(workouts.userId, userId),
with: {
exercises: true // Type error: exercises not in Workout
}
})
}
import { workouts } from '~~/server/database/schema/workouts'
export async function createWorkout(data: InsertWorkout) {
const [workout] = await useDB()
.insert(workouts)
.values(data)
.returning()
return workout
}
// Query version (returns undefined if not found)
export function queryWorkoutById(id: string, userId: string) {
return useDB().query.workouts.findFirst({
where: and(
eq(workouts.id, id),
eq(workouts.userId, userId)
),
})
}
// Get version (throws 404 if not found)
export async function getWorkoutById(id: string, userId: string) {
const workout = await queryWorkoutById(id, userId)
if (!workout) {
throw createError({
statusCode: 404,
statusMessage: 'Workout not found'
})
}
return workout
}
interface QueryOptions {
limit?: number
offset?: number
status?: 'active' | 'completed'
}
export function queryUserWorkouts(
userId: string,
options: QueryOptions = {}
) {
const { limit = 20, offset = 0, status } = options
return useDB().query.workouts.findMany({
where: and(
eq(workouts.userId, userId),
status ? eq(workouts.status, status) : undefined
),
limit,
offset,
orderBy: desc(workouts.createdAt)
})
}
export async function updateWorkout(
id: string,
userId: string,
data: Partial<InsertWorkout>
) {
const [workout] = await useDB()
.update(workouts)
.set({
...data,
updatedAt: new Date()
})
.where(and(
eq(workouts.id, id),
eq(workouts.userId, userId)
))
.returning()
if (!workout) {
throw createError({
statusCode: 404,
statusMessage: 'Workout not found'
})
}
return workout
}
export async function deleteWorkout(id: string, userId: string) {
const [workout] = await useDB()
.delete(workouts)
.where(and(
eq(workouts.id, id),
eq(workouts.userId, userId)
))
.returning()
if (!workout) {
throw createError({
statusCode: 404,
statusMessage: 'Workout not found'
})
}
}
Always enforce RLS for user data by including userId in where clauses:
// ✅ Correct: Enforces RLS
export function queryUserWorkouts(userId: string) {
return useDB().query.workouts.findMany({
where: eq(workouts.userId, userId) // RLS enforcement
})
}
// ❌ Wrong: No RLS
export function queryAllWorkouts() {
return useDB().query.workouts.findMany() // Exposes all users' data!
}
Always verify ownership before modifying:
export async function updateWorkout(
id: string,
userId: string, // RLS parameter
data: Partial<InsertWorkout>
) {
const [workout] = await useDB()
.update(workouts)
.set(data)
.where(and(
eq(workouts.id, id),
eq(workouts.userId, userId) // RLS enforcement
))
.returning()
if (!workout) {
throw createError({
statusCode: 404,
statusMessage: 'Workout not found'
})
}
return workout
}
Group related queries by domain:
server/database/queries/
├── workouts.ts # Workout-related queries
├── exercises.ts # Exercise-related queries
├── users.ts # User-related queries
├── goals.ts # Goal-related queries
└── notifications.ts # Notification-related queries
Use specific function names to avoid conflicts:
// ✅ Correct: Specific names
export function deleteWorkoutNotification(id: string) { }
export function deleteProgressNotification(id: string) { }
// ❌ Wrong: Generic name causes conflicts
export function deleteNotification(id: string) { } // Which table?
Define relations in schema for easy querying:
// server/database/schema/workouts.ts
export const workouts = pgTable('workouts', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id),
name: text('name').notNull(),
// ...
})
export const workoutsRelations = relations(workouts, ({ one, many }) => ({
user: one(users, {
fields: [workouts.userId],
references: [users.id],
}),
exercises: many(workoutExercises),
}))
Query with relations:
export function queryWorkoutWithUser(id: string) {
return useDB().query.workouts.findFirst({
where: eq(workouts.id, id),
with: {
user: true,
exercises: {
with: {
exercise: true
}
}
}
})
}
Use transactions for atomic operations:
export async function createWorkoutWithExercises(
userId: string,
workoutData: InsertWorkout,
exercisesData: InsertWorkoutExercise[]
) {
return useDB().transaction(async (tx) => {
// Create workout
const [workout] = await tx
.insert(workouts)
.values({ ...workoutData, userId })
.returning()
// Create exercises
const exercises = await tx
.insert(workoutExercises)
.values(
exercisesData.map(ex => ({
...ex,
workoutId: workout.id
}))
)
.returning()
return { workout, exercises }
})
}
Use SQL functions for aggregations:
import { sql } from 'drizzle-orm'
export async function queryWorkoutStats(userId: string) {
const result = await useDB()
.select({
totalWorkouts: sql<number>`count(*)`,
avgDuration: sql<number>`avg(${workouts.duration})`,
maxWeight: sql<number>`max(${workouts.totalWeight})`,
})
.from(workouts)
.where(eq(workouts.userId, userId))
return result[0]
}
export async function countUserWorkouts(
userId: string,
status?: 'active' | 'completed'
) {
const result = await useDB()
.select({ count: sql<number>`count(*)` })
.from(workouts)
.where(and(
eq(workouts.userId, userId),
status ? eq(workouts.status, status) : undefined
))
return result[0].count
}
interface PaginationOptions {
page: number
limit: number
}
export async function queryPaginatedWorkouts(
userId: string,
options: PaginationOptions
) {
const { page, limit } = options
const offset = (page - 1) * limit
const [workouts, [{ total }]] = await Promise.all([
useDB().query.workouts.findMany({
where: eq(workouts.userId, userId),
limit,
offset,
orderBy: desc(workouts.createdAt)
}),
useDB()
.select({ total: sql<number>`count(*)` })
.from(workouts)
.where(eq(workouts.userId, userId))
])
return {
data: workouts,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
}
}
/server/database/queries/queryX() vs getX()Promise<...>)userId in where clauses)useDB().query for relationsuseDB().select() for complex queriesuseDB() directly in API routesPromise<...> return types to query functionsFor advanced patterns:
references/schema-patterns.md - Schema design patternsreferences/relations.md - Working with relationsreferences/transactions.md - Transaction patternsreferences/migrations.md - Migration best practicesActivates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Activates when the user asks about Agent Skills, wants to find reusable AI capabilities, needs to install skills, or mentions skills for Claude. Use for discovering, retrieving, and installing skills.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.