AGENT SKILL

convex-functions

Writing queries, mutations, actions, and HTTP actions with proper argument validation, error handling, internal functions, and runtime considerations

License 许可
AGPL-3.0
所属仓库
baisharg/ASTN
最近更新
2026-11-13
Stars
★ 2
Forks
2

01 前言 Front Matter

name
convex-functions
tags
[ "convex", "functions", "queries", "mutations", "actions", "http" ]
author
Convex
version
1.0.0
description
Writing queries, mutations, actions, and HTTP actions with proper argument validation, error handling, internal functions, and runtime considerations
displayName
Convex Functions

02 资源文件 3

03 SKILL.md 正文

Convex Functions

Master Convex functions including queries, mutations, actions, and HTTP endpoints with proper validation, error handling, and runtime considerations.

Documentation Sources

Before implementing, do not assume; fetch the latest documentation:

Instructions

Function Types Overview

Type Database Access External APIs Caching Use Case
Query Read-only No Yes, reactive Fetching data
Mutation Read/Write No No Modifying data
Action Via runQuery/runMutation Yes No External integrations
HTTP Action Via runQuery/runMutation Yes No Webhooks, APIs

Queries

Queries are reactive, cached, and read-only:

import { query } from './_generated/server'
import { v } from 'convex/values'

export const getUser = query({
  args: { userId: v.id('users') },
  returns: v.union(
    v.object({
      _id: v.id('users'),
      _creationTime: v.number(),
      name: v.string(),
      email: v.string(),
    }),
    v.null(),
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.userId)
  },
})

// Query with index
export const listUserTasks = query({
  args: { userId: v.id('users') },
  returns: v.array(
    v.object({
      _id: v.id('tasks'),
      _creationTime: v.number(),
      title: v.string(),
      completed: v.boolean(),
    }),
  ),
  handler: async (ctx, args) => {
    return await ctx.db
      .query('tasks')
      .withIndex('by_user', (q) => q.eq('userId', args.userId))
      .order('desc')
      .collect()
  },
})

Mutations

Mutations modify the database and are transactional:

import { mutation } from './_generated/server'
import { v } from 'convex/values'
import { ConvexError } from 'convex/values'

export const createTask = mutation({
  args: {
    title: v.string(),
    userId: v.id('users'),
  },
  returns: v.id('tasks'),
  handler: async (ctx, args) => {
    // Validate user exists
    const user = await ctx.db.get(args.userId)
    if (!user) {
      throw new ConvexError('User not found')
    }

    return await ctx.db.insert('tasks', {
      title: args.title,
      userId: args.userId,
      completed: false,
      createdAt: Date.now(),
    })
  },
})

export const deleteTask = mutation({
  args: { taskId: v.id('tasks') },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.delete(args.taskId)
    return null
  },
})

Actions

Actions can call external APIs but have no direct database access:

'use node'

import { action } from './_generated/server'
import { v } from 'convex/values'
import { api, internal } from './_generated/api'

export const sendEmail = action({
  args: {
    to: v.string(),
    subject: v.string(),
    body: v.string(),
  },
  returns: v.object({ success: v.boolean() }),
  handler: async (ctx, args) => {
    // Call external API
    const response = await fetch('https://api.email.com/send', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(args),
    })

    return { success: response.ok }
  },
})

// Action calling queries and mutations
export const processOrder = action({
  args: { orderId: v.id('orders') },
  returns: v.null(),
  handler: async (ctx, args) => {
    // Read data via query
    const order = await ctx.runQuery(api.orders.get, { orderId: args.orderId })

    if (!order) {
      throw new Error('Order not found')
    }

    // Call external payment API
    const paymentResult = await processPayment(order)

    // Update database via mutation
    await ctx.runMutation(internal.orders.updateStatus, {
      orderId: args.orderId,
      status: paymentResult.success ? 'paid' : 'failed',
    })

    return null
  },
})

HTTP Actions

HTTP actions handle webhooks and external requests:

// convex/http.ts
import { httpRouter } from 'convex/server'
import { httpAction } from './_generated/server'
import { api, internal } from './_generated/api'

const http = httpRouter()

// Webhook endpoint
http.route({
  path: '/webhooks/stripe',
  method: 'POST',
  handler: httpAction(async (ctx, request) => {
    const signature = request.headers.get('stripe-signature')
    const body = await request.text()

    // Verify webhook signature
    if (!verifyStripeSignature(body, signature)) {
      return new Response('Invalid signature', { status: 401 })
    }

    const event = JSON.parse(body)

    // Process webhook
    await ctx.runMutation(internal.payments.handleWebhook, {
      eventType: event.type,
      data: event.data,
    })

    return new Response('OK', { status: 200 })
  }),
})

// API endpoint
http.route({
  path: '/api/users/:userId',
  method: 'GET',
  handler: httpAction(async (ctx, request) => {
    const url = new URL(request.url)
    const userId = url.pathname.split('/').pop()

    const user = await ctx.runQuery(api.users.get, {
      userId: userId as Id<'users'>,
    })

    if (!user) {
      return new Response('Not found', { status: 404 })
    }

    return Response.json(user)
  }),
})

export default http

Internal Functions

Use internal functions for sensitive operations:

import {
  internalMutation,
  internalQuery,
  internalAction,
} from './_generated/server'
import { v } from 'convex/values'

// Only callable from other Convex functions
export const _updateUserCredits = internalMutation({
  args: {
    userId: v.id('users'),
    amount: v.number(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await ctx.db.get(args.userId)
    if (!user) return null

    await ctx.db.patch(args.userId, {
      credits: (user.credits || 0) + args.amount,
    })
    return null
  },
})

// Call internal function from action
export const purchaseCredits = action({
  args: { userId: v.id('users'), amount: v.number() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // Process payment externally
    await processPayment(args.amount)

    // Update credits via internal mutation
    await ctx.runMutation(internal.users._updateUserCredits, {
      userId: args.userId,
      amount: args.amount,
    })

    return null
  },
})

Scheduling Functions

Schedule functions to run later:

import { mutation, internalMutation } from './_generated/server'
import { v } from 'convex/values'
import { internal } from './_generated/api'

export const scheduleReminder = mutation({
  args: {
    userId: v.id('users'),
    message: v.string(),
    delayMs: v.number(),
  },
  returns: v.id('_scheduled_functions'),
  handler: async (ctx, args) => {
    return await ctx.scheduler.runAfter(
      args.delayMs,
      internal.notifications.sendReminder,
      { userId: args.userId, message: args.message },
    )
  },
})

export const sendReminder = internalMutation({
  args: {
    userId: v.id('users'),
    message: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.insert('notifications', {
      userId: args.userId,
      message: args.message,
      sentAt: Date.now(),
    })
    return null
  },
})

Examples

Complete Function File

// convex/messages.ts
import { query, mutation, internalMutation } from './_generated/server'
import { v } from 'convex/values'
import { ConvexError } from 'convex/values'
import { internal } from './_generated/api'

const messageValidator = v.object({
  _id: v.id('messages'),
  _creationTime: v.number(),
  channelId: v.id('channels'),
  authorId: v.id('users'),
  content: v.string(),
  editedAt: v.optional(v.number()),
})

// Public query
export const list = query({
  args: {
    channelId: v.id('channels'),
    limit: v.optional(v.number()),
  },
  returns: v.array(messageValidator),
  handler: async (ctx, args) => {
    const limit = args.limit ?? 50
    return await ctx.db
      .query('messages')
      .withIndex('by_channel', (q) => q.eq('channelId', args.channelId))
      .order('desc')
      .take(limit)
  },
})

// Public mutation
export const send = mutation({
  args: {
    channelId: v.id('channels'),
    authorId: v.id('users'),
    content: v.string(),
  },
  returns: v.id('messages'),
  handler: async (ctx, args) => {
    if (args.content.trim().length === 0) {
      throw new ConvexError('Message cannot be empty')
    }

    const messageId = await ctx.db.insert('messages', {
      channelId: args.channelId,
      authorId: args.authorId,
      content: args.content.trim(),
    })

    // Schedule notification
    await ctx.scheduler.runAfter(0, internal.messages.notifySubscribers, {
      channelId: args.channelId,
      messageId,
    })

    return messageId
  },
})

// Internal mutation
export const notifySubscribers = internalMutation({
  args: {
    channelId: v.id('channels'),
    messageId: v.id('messages'),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    // Get channel subscribers and notify them
    const subscribers = await ctx.db
      .query('subscriptions')
      .withIndex('by_channel', (q) => q.eq('channelId', args.channelId))
      .collect()

    for (const sub of subscribers) {
      await ctx.db.insert('notifications', {
        userId: sub.userId,
        messageId: args.messageId,
        read: false,
      })
    }
    return null
  },
})

Best Practices

  • Never run npx convex deploy unless explicitly instructed
  • Never run any git commands unless explicitly instructed
  • Always define args and returns validators
  • Use queries for read operations (they are cached and reactive)
  • Use mutations for write operations (they are transactional)
  • Use actions only when calling external APIs
  • Use internal functions for sensitive operations
  • Add "use node"; at the top of action files using Node.js APIs
  • Handle errors with ConvexError for user-facing messages

Common Pitfalls

  1. Using actions for database operations - Use queries/mutations instead
  2. Calling external APIs from queries/mutations - Use actions
  3. Forgetting to add "use node" - Required for Node.js APIs in actions
  4. Missing return validators - Always specify returns
  5. Not using internal functions for sensitive logic - Protect with internalMutation

References

04 同仓库更多 Skills