AGENT SKILL

function-creator

Create Convex queries, mutations, and actions with proper validation, authentication, and error handling. Use when implementing new API endpoints.

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

01 前言 Front Matter

name
function-creator
description
Create Convex queries, mutations, and actions with proper validation, authentication, and error handling. Use when implementing new API endpoints.

03 SKILL.md 正文

Convex Function Creator

Generate secure, type-safe Convex functions following all best practices.

When to Use

  • Creating new query functions (read data)
  • Creating new mutation functions (write data)
  • Creating new action functions (external APIs, long-running)
  • Adding API endpoints to your Convex backend

Function Types

Queries (Read-Only)

  • Can only read from database
  • Cannot modify data or call external APIs
  • Cached and reactive
  • Run in transactions
import { query } from './_generated/server'
import { v } from 'convex/values'

export const getTask = query({
  args: { taskId: v.id('tasks') },
  returns: v.union(
    v.object({
      _id: v.id('tasks'),
      text: v.string(),
      completed: v.boolean(),
    }),
    v.null(),
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.taskId)
  },
})

Mutations (Transactional Writes)

  • Can read and write to database
  • Cannot call external APIs
  • Run in ACID transactions
  • Automatic retries on conflicts
import { mutation } from './_generated/server'
import { v } from 'convex/values'

export const createTask = mutation({
  args: {
    text: v.string(),
    priority: v.optional(
      v.union(v.literal('low'), v.literal('medium'), v.literal('high')),
    ),
  },
  returns: v.id('tasks'),
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity()
    if (!identity) throw new Error('Not authenticated')

    return await ctx.db.insert('tasks', {
      text: args.text,
      priority: args.priority ?? 'medium',
      completed: false,
      createdAt: Date.now(),
    })
  },
})

Actions (External + Non-Transactional)

  • Can call external APIs (fetch, AI, etc.)
  • Can call mutations via ctx.runMutation
  • Cannot directly access database
  • No automatic retries
  • Use "use node" directive when needing Node.js APIs

Important: If your action needs Node.js-specific APIs (crypto, third-party SDKs, etc.), add "use node" at the top of the file. Files with "use node" can ONLY contain actions, not queries or mutations.

'use node' // Required for Node.js APIs like OpenAI SDK

import { action } from './_generated/server'
import { api } from './_generated/api'
import { v } from 'convex/values'
import OpenAI from 'openai'

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

export const generateTaskSuggestion = action({
  args: { prompt: v.string() },
  returns: v.string(),
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity()
    if (!identity) throw new Error('Not authenticated')

    // Call OpenAI (requires "use node")
    const completion = await openai.chat.completions.create({
      model: 'gpt-4',
      messages: [{ role: 'user', content: args.prompt }],
    })

    const suggestion = completion.choices[0].message.content

    // Write to database via mutation
    await ctx.runMutation(api.tasks.createTask, {
      text: suggestion,
    })

    return suggestion
  },
})

Note: If you only need basic fetch (no Node.js APIs), you can omit "use node". But for third-party SDKs, crypto, or other Node.js features, you must use it.

Required Components

1. Argument Validation

Always define args with validators:

args: {
  id: v.id("tasks"),
  text: v.string(),
  count: v.number(),
  enabled: v.boolean(),
  tags: v.array(v.string()),
  metadata: v.optional(v.object({
    key: v.string(),
  })),
}

2. Return Type Validation

Always define returns:

returns: v.object({
  _id: v.id('tasks'),
  text: v.string(),
})

// Or for arrays
returns: v.array(
  v.object({
    /* ... */
  }),
)

// Or for nullable
returns: v.union(
  v.object({
    /* ... */
  }),
  v.null(),
)

3. Authentication Check

Always verify auth in public functions:

const identity = await ctx.auth.getUserIdentity()
if (!identity) {
  throw new Error('Not authenticated')
}

4. Authorization Check

Always verify ownership/permissions:

const task = await ctx.db.get(args.taskId)
if (!task) {
  throw new Error('Task not found')
}

if (task.userId !== user._id) {
  throw new Error('Unauthorized')
}

Complete Examples

Secure Query with Auth

export const getMyTasks = query({
  args: {
    status: v.optional(v.union(v.literal('active'), v.literal('completed'))),
  },
  returns: v.array(
    v.object({
      _id: v.id('tasks'),
      text: v.string(),
      completed: v.boolean(),
    }),
  ),
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity()
    if (!identity) throw new Error('Not authenticated')

    const user = await ctx.db
      .query('users')
      .withIndex('by_token', (q) =>
        q.eq('tokenIdentifier', identity.tokenIdentifier),
      )
      .unique()

    if (!user) throw new Error('User not found')

    let query = ctx.db
      .query('tasks')
      .withIndex('by_user', (q) => q.eq('userId', user._id))

    const tasks = await query.collect()

    if (args.status) {
      return tasks.filter((t) =>
        args.status === 'completed' ? t.completed : !t.completed,
      )
    }

    return tasks
  },
})

Secure Mutation with Validation

export const updateTask = mutation({
  args: {
    taskId: v.id('tasks'),
    text: v.optional(v.string()),
    completed: v.optional(v.boolean()),
  },
  returns: v.id('tasks'),
  handler: async (ctx, args) => {
    // 1. Authentication
    const identity = await ctx.auth.getUserIdentity()
    if (!identity) throw new Error('Not authenticated')

    // 2. Get user
    const user = await ctx.db
      .query('users')
      .withIndex('by_token', (q) =>
        q.eq('tokenIdentifier', identity.tokenIdentifier),
      )
      .unique()

    if (!user) throw new Error('User not found')

    // 3. Get resource
    const task = await ctx.db.get(args.taskId)
    if (!task) throw new Error('Task not found')

    // 4. Authorization
    if (task.userId !== user._id) {
      throw new Error('Unauthorized')
    }

    // 5. Update
    const updates: Partial<typeof task> = {}
    if (args.text !== undefined) updates.text = args.text
    if (args.completed !== undefined) updates.completed = args.completed

    await ctx.db.patch(args.taskId, updates)
    return args.taskId
  },
})

Action Calling External API

Create separate file for actions that need Node.js:

// convex/taskActions.ts
'use node' // Required for SendGrid SDK

import { action } from './_generated/server'
import { api } from './_generated/api'
import { v } from 'convex/values'
import sendgrid from '@sendgrid/mail'

sendgrid.setApiKey(process.env.SENDGRID_API_KEY)

export const sendTaskReminder = action({
  args: { taskId: v.id('tasks') },
  returns: v.boolean(),
  handler: async (ctx, args) => {
    // 1. Auth
    const identity = await ctx.auth.getUserIdentity()
    if (!identity) throw new Error('Not authenticated')

    // 2. Get data via query
    const task = await ctx.runQuery(api.tasks.getTask, {
      taskId: args.taskId,
    })

    if (!task) throw new Error('Task not found')

    // 3. Call external service (using Node.js SDK)
    await sendgrid.send({
      to: identity.email,
      from: 'noreply@example.com',
      subject: 'Task Reminder',
      text: `Don't forget: ${task.text}`,
    })

    // 4. Update via mutation
    await ctx.runMutation(api.tasks.markReminderSent, {
      taskId: args.taskId,
    })

    return true
  },
})

Note: Keep queries and mutations in convex/tasks.ts (without "use node"), and actions that need Node.js in convex/taskActions.ts (with "use node").

Internal Functions

For backend-only functions (called by scheduler, other functions):

import { internalMutation } from './_generated/server'

export const processExpiredTasks = internalMutation({
  args: {},
  handler: async (ctx) => {
    // No auth needed - only callable from backend
    const now = Date.now()
    const expired = await ctx.db
      .query('tasks')
      .withIndex('by_due_date', (q) => q.lt('dueDate', now))
      .collect()

    for (const task of expired) {
      await ctx.db.patch(task._id, { status: 'expired' })
    }
  },
})

Checklist

  • [ ] args defined with validators
  • [ ] returns defined with validator
  • [ ] Authentication check (ctx.auth.getUserIdentity())
  • [ ] Authorization check (ownership/permissions)
  • [ ] All promises awaited
  • [ ] Indexed queries (no .filter() on queries)
  • [ ] Error handling with descriptive messages
  • [ ] Scheduled functions use internal.* not api.*
  • [ ] If using Node.js APIs: "use node" at top of file
  • [ ] If file has "use node": Only actions (no queries/mutations)
  • [ ] Actions in separate file from queries/mutations when using "use node"

04 同仓库更多 Skills