Loading...
Loading...
Create Convex queries, mutations, and actions with proper validation, authentication, and error handling. Use when implementing new API endpoints.
npx skill4agent add get-convex/agent-skills function-creatorimport { 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);
},
});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(),
});
},
});ctx.runMutation"use node""use node""use node""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;
},
});"use node"argsargs: {
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(),
})),
}returnsreturns: 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())const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}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");
}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;
},
});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<any> = {};
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;
},
});// 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;
},
});convex/tasks.tsconvex/taskActions.tsimport { 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" });
}
},
});argsreturnsctx.auth.getUserIdentity().filter()internal.*api.*"use node""use node"