convex-helpers-guide

安装量: 39
排名: #18411

安装

npx skills add https://github.com/get-convex/agent-skills --skill convex-helpers-guide

Convex Helpers Guide Use convex-helpers to add common patterns and utilities to your Convex backend without reinventing the wheel. What is convex-helpers? convex-helpers is the official collection of utilities that complement Convex. It provides battle-tested patterns for common backend needs. Installation: npm install convex-helpers Available Helpers 1. Relationship Helpers Traverse relationships between tables in a readable, type-safe way. Use when: Loading related data across tables Following foreign key relationships Building nested data structures Example: import { getOneFrom , getManyFrom } from "convex-helpers/server/relationships" ; export const getTaskWithUser = query ( { args : { taskId : v . id ( "tasks" ) } , handler : async ( ctx , args ) => { const task = await ctx . db . get ( args . taskId ) ; if ( ! task ) return null ; // Get related user const user = await getOneFrom ( ctx . db , "users" , "by_id" , task . userId , "_id" ) ; // Get related comments const comments = await getManyFrom ( ctx . db , "comments" , "by_task" , task . _id , "taskId" ) ; return { ... task , user , comments } ; } , } ) ; Key Functions: getOneFrom - Get single related document getManyFrom - Get multiple related documents getManyVia - Get many-to-many relationships through junction table 2. Custom Functions (Data Protection) - MOST IMPORTANT This is Convex's alternative to Row Level Security (RLS). Instead of database-level policies, use custom function wrappers to automatically add auth and access control to all queries and mutations. Create wrapped versions of query/mutation/action with custom behavior. Use when: Data protection and access control (PRIMARY USE CASE) Want to add auth logic to all functions Multi-tenant applications Role-based access control (RBAC) Need to inject common data into ctx Building internal-only functions Adding logging/monitoring to all functions Why this instead of RLS: TypeScript, not SQL policies Full type safety Easy to test and debug More flexible than database policies Works across your entire backend Example: Custom Query with Auto-Auth // convex/lib/customFunctions.ts import { customQuery } from "convex-helpers/server/customFunctions" ; import { query } from "../_generated/server" ; export const authenticatedQuery = customQuery ( query , { args : { } , // No additional args required input : 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" ) ; // Add user to context return { ctx : { ... ctx , user } , args } ; } , } ) ; // Usage in your functions export const getMyTasks = authenticatedQuery ( { handler : async ( ctx ) => { // ctx.user is automatically available! return await ctx . db . query ( "tasks" ) . withIndex ( "by_user" , q => q . eq ( "userId" , ctx . user . _id ) ) . collect ( ) ; } , } ) ; Example: Multi-Tenant Data Protection import { customQuery } from "convex-helpers/server/customFunctions" ; import { query } from "../_generated/server" ; // Organization-scoped query - automatic access control export const orgQuery = customQuery ( query , { args : { orgId : v . id ( "organizations" ) } , input : async ( ctx , args ) => { const user = await getCurrentUser ( ctx ) ; // Verify user is a member of this organization const member = await ctx . db . query ( "organizationMembers" ) . withIndex ( "by_org_and_user" , q => q . eq ( "orgId" , args . orgId ) . eq ( "userId" , user . _id ) ) . unique ( ) ; if ( ! member ) { throw new Error ( "Not authorized for this organization" ) ; } // Inject org context return { ctx : { ... ctx , user , orgId : args . orgId , role : member . role } , args } ; } , } ) ; // Usage - data automatically scoped to organization export const getOrgProjects = orgQuery ( { args : { orgId : v . id ( "organizations" ) } , handler : async ( ctx ) => { // ctx.user and ctx.orgId automatically available and verified! return await ctx . db . query ( "projects" ) . withIndex ( "by_org" , q => q . eq ( "orgId" , ctx . orgId ) ) . collect ( ) ; } , } ) ; Example: Role-Based Access Control import { customMutation } from "convex-helpers/server/customFunctions" ; import { mutation } from "../_generated/server" ; export const adminMutation = customMutation ( mutation , { args : { } , input : async ( ctx , args ) => { const user = await getCurrentUser ( ctx ) ; if ( user . role !== "admin" ) { throw new Error ( "Admin access required" ) ; } return { ctx : { ... ctx , user } , args } ; } , } ) ; // Usage - only admins can call this export const deleteUser = adminMutation ( { args : { userId : v . id ( "users" ) } , handler : async ( ctx , args ) => { // Only admins reach this code await ctx . db . delete ( args . userId ) ; } , } ) ; 3. Filter Helper Apply complex TypeScript filters to database queries. Use when: Need to filter by computed values Filtering logic is too complex for indexes Working with small result sets Example: import { filter } from "convex-helpers/server/filter" ; export const getActiveTasks = query ( { handler : async ( ctx ) => { const now = Date . now ( ) ; const threeDaysAgo = now - 3 * 24 * 60 * 60 * 1000 ; return await filter ( ctx . db . query ( "tasks" ) , ( task ) => ! task . completed && task . createdAt

threeDaysAgo && task . priority === "high" ) . collect ( ) ; } , } ) ; Note: Still prefer indexes when possible! Use filter for complex logic that can't be indexed. 4. Sessions Track users across requests even when not logged in. Use when: Need to track anonymous users Building shopping cart for guests Tracking user behavior before signup A/B testing without auth Setup: // convex/sessions.ts import { SessionIdArg } from "convex-helpers/server/sessions" ; import { query } from "./_generated/server" ; export const trackView = query ( { args : { ... SessionIdArg , // Adds sessionId: v.string() pageUrl : v . string ( ) , } , handler : async ( ctx , args ) => { await ctx . db . insert ( "pageViews" , { sessionId : args . sessionId , pageUrl : args . pageUrl , timestamp : Date . now ( ) , } ) ; } , } ) ; Client (React): import { useSessionId } from "convex-helpers/react/sessions" ; import { useQuery } from "convex/react" ; import { api } from "../convex/_generated/api" ; function MyComponent ( ) { const sessionId = useSessionId ( ) ; // Automatically includes sessionId in all requests useQuery ( api . sessions . trackView , { sessionId , pageUrl : window . location . href , } ) ; } 5. Zod Validation Use Zod schemas instead of Convex validators. Use when: Already using Zod in your project Want more complex validation logic Need custom error messages Example: import { zCustomQuery } from "convex-helpers/server/zod" ; import { z } from "zod" ; import { query } from "./_generated/server" ; const argsSchema = z . object ( { email : z . string ( ) . email ( ) , age : z . number ( ) . min ( 18 ) . max ( 120 ) , } ) ; export const createUser = zCustomQuery ( query , { args : argsSchema , handler : async ( ctx , args ) => { // args is typed from Zod schema return await ctx . db . insert ( "users" , args ) ; } , } ) ; 6. Alternative: Row-Level Security Helper Note: Convex recommends using custom functions (see #2 above) as the primary data protection pattern. This RLS helper is an alternative approach that mimics traditional RLS. However, custom functions are usually better because: Type-safe at compile time (RLS is runtime) More explicit (easy to see what auth is applied) Better error messages Easier to test 7. Migrations Run data migrations safely. Use when: Backfilling new fields Transforming existing data Moving between schema versions Example: import { makeMigration } from "convex-helpers/server/migrations" ; export const addDefaultPriority = makeMigration ( { table : "tasks" , migrateOne : async ( ctx , doc ) => { if ( doc . priority === undefined ) { await ctx . db . patch ( doc . _id , { priority : "medium" } ) ; } } , } ) ; // Run: npx convex run migrations:addDefaultPriority 8. Triggers Execute code automatically when data changes. Use when: Sending notifications on data changes Updating related records Logging changes Maintaining computed fields Example: import { Triggers } from "convex-helpers/server/triggers" ; const triggers = new Triggers ( ) ; triggers . register ( "tasks" , "insert" , async ( ctx , task ) => { // Send notification when task is created await ctx . db . insert ( "notifications" , { userId : task . userId , type : "task_created" , taskId : task . _id , } ) ; } ) ; Common Patterns Pattern 1: Authenticated Queries with User Context import { customQuery } from "convex-helpers/server/customFunctions" ; export const authedQuery = customQuery ( query , { args : { } , input : async ( ctx , args ) => { const user = await getCurrentUser ( ctx ) ; return { ctx : { ... ctx , user } , args } ; } , } ) ; // Now all queries automatically have user in context export const getMyData = authedQuery ( { handler : async ( ctx ) => { // ctx.user is typed and available! return await ctx . db . query ( "data" ) . withIndex ( "by_user" , q => q . eq ( "userId" , ctx . user . _id ) ) . collect ( ) ; } , } ) ; Pattern 2: Loading Related Data import { getOneFrom , getManyFrom } from "convex-helpers/server/relationships" ; export const getPostWithDetails = query ( { args : { postId : v . id ( "posts" ) } , handler : async ( ctx , args ) => { const post = await ctx . db . get ( args . postId ) ; if ( ! post ) return null ; const author = await getOneFrom ( ctx . db , "users" , "by_id" , post . authorId , "_id" ) ; const comments = await getManyFrom ( ctx . db , "comments" , "by_post" , post . _id , "postId" ) ; const tagLinks = await getManyFrom ( ctx . db , "postTags" , "by_post" , post . _id , "postId" ) ; const tags = await Promise . all ( tagLinks . map ( link => getOneFrom ( ctx . db , "tags" , "by_id" , link . tagId , "_id" ) ) ) ; return { ... post , author , comments , tags } ; } , } ) ; Pattern 3: Batch Operations with Error Handling import { asyncMap } from "convex-helpers" ; export const batchUpdateTasks = mutation ( { args : { taskIds : v . array ( v . id ( "tasks" ) ) , status : v . string ( ) , } , handler : async ( ctx , args ) => { const results = await asyncMap ( args . taskIds , async ( taskId ) => { try { const task = await ctx . db . get ( taskId ) ; if ( task ) { await ctx . db . patch ( taskId , { status : args . status } ) ; return { success : true , taskId } ; } return { success : false , taskId , error : "Not found" } ; } catch ( error ) { return { success : false , taskId , error : error . message } ; } } ) ; return results ; } , } ) ; When to Use What Need Use Import From Load related data getOneFrom , getManyFrom convex-helpers/server/relationships Auth in all functions customQuery convex-helpers/server/customFunctions Complex filters filter convex-helpers/server/filter Anonymous users useSessionId convex-helpers/react/sessions Zod validation zCustomQuery convex-helpers/server/zod Data migrations makeMigration convex-helpers/server/migrations Triggers Triggers convex-helpers/server/triggers Checklist Installed convex-helpers: npm install convex-helpers Using relationship helpers for related data Created custom functions for common auth patterns Using sessions for anonymous tracking (if needed) Prefer indexes over filter when possible Check convex-helpers docs for new utilities

返回排行榜