convex-helpers

repository·main·Indexed 19 days ago

https://github.com/get-convex/convex-helpers

A collection of utility packages designed to complement official Convex packages. It provides advanced patterns and helpers for session management, relationship traversal, Row-Level Security (RLS), Zod validation, and custom query/mutation/action builders. Additional features include Hono integration for HTTP endpoints, CRUD API generation, manual pagination via getPage, and a non-reactive paginator.

Tokens
32.6K
Snippets
95
Records
119
Agent score
66%

What's inside convex-helpers

  1. Use Composable QueryStreams for complex queries

    main

    A QueryStream is an async iterable of documents ordered by indexed fields. They allow you to perform operations equivalent to SQL's UNION ALL, WHERE, JOIN, and ORDER BY directly on Convex data streams before returning the final result to the client. This is useful for merging multiple queries, filtering results based on complex predicates, or joining data from different tables.

    Core Stream Operations:

    • stream(ctx.db, schema): Constructs a new stream using DatabaseReader syntax.
    • mergedStream(streams, fields): Combines multiple streams into one, maintaining order based on the provided fields.
    • .flatMap(async (doc) => ...): Expands each document into its own stream and chains them together (useful for joins).
    • .map(async (doc) => ...): Modifies each item in the stream while preserving order.
    • .filterWith(async (doc) => ...): Filters documents based on a TypeScript predicate.

    Finalizing a Stream: Once configured, you can treat a stream like a standard Convex query by calling methods like .first(), .collect(), .take(n), or .paginate(paginationOpts).

    import { stream, mergedStream } from "convex-helpers/server/stream";
    import schema from "./schema";
    
    export const listForAuthors = query({
      args: {
        authors: v.array(v.id("users")),
        paginationOpts: paginationOptsValidator,
      },
      handler: async (ctx, { authors, paginationOpts }) => {
        const authorStreams = authors.map((author) =>
          stream(ctx.db, schema)
            .query("messages")
            .withIndex("by_author", (q) => q.eq("author", author)),
        );
        
        const allAuthorsStream = mergedStream(authorStreams, [
          "author",
          "_creationTime",
        ]);
    
        return await allAuthorsStream.paginate(paginationOpts);
      },
    });
  2. How Triggers work in Convex Helpers

    main

    Triggers allow you to register functions that run automatically whenever data in a specific table changes via ctx.db.insert, ctx.db.patch, ctx.db.replace, or ctx.db.delete.

    Key Characteristics:

    • Atomicity: Triggers run in the same transaction as the mutation that caused them. If a trigger throws an error, the database write is aborted.
    • Integration: Triggers must be paired with custom functions (like customMutation) to be executed. They will not run if you use raw Convex mutation wrappers, if you edit data in the Convex dashboard, or if you use npx convex import.
    • Recursive Triggers: Triggers can trigger other triggers. These are executed in a breadth-first-search (BFS) queue. To perform writes without triggering subsequent triggers, use ctx.innerDb.
    • Capabilities:
      • Denormalize computed fields.
      • Perform cascading deletes.
      • Schedule side effects (e.g., via ctx.scheduler).
      • Validate constraints or row-level security by throwing errors.
    import { mutation as rawMutation } from "./_generated/server";
    import { DataModel } from "./_generated/dataModel";
    import { Triggers } from "convex-helpers/server/triggers";
    import {
      customCtx,
      customMutation,
    } from "convex-helpers/server/customFunctions";
    
    const triggers = new Triggers<DataModel>();
    
    triggers.register("users", async (ctx, change) => {
      if (change.operation === "insert") {
        // Logic here
      }
    });
    
    // You MUST wrap your mutation to enable triggers
    export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
  3. Reference functions using the api and internal objects

    main

    Convex uses file-based routing to generate function references in convex/_generated/api.ts:

    • Public Functions: Accessed via the api object.
      • convex/example.ts -> api.example.f
      • convex/messages/access.ts -> api.messages.access.h
    • Internal Functions: Accessed via the internal object.
      • convex/example.ts (internal) -> internal.example.g
  4. Track sessions via client-side sessionID storage

    main

    To track users even when they are not logged in, you can store a sessionId on the client and pass it to your Convex functions.

    1. Client Setup: Wrap your application in a SessionProvider from convex-helpers/react/sessions inside your ConvexProvider.
    2. Server Setup: Use queryWithSession (or a custom builder using SessionIdArg) to extract the sessionId and look up an anonymous user.
    3. Usage: Use useSessionQuery from convex-helpers/react/sessions to automatically include the session ID in requests.
    // Client: Add SessionProvider to your root
    import { SessionProvider } from "convex-helpers/react/sessions";
    
    <ConvexProvider client={convex}>
      <SessionProvider>
        <App />
      </SessionProvider>
    </ConvexProvider>
    
    // Client: Use useSessionQuery to fetch data
    import { useSessionQuery } from "convex-helpers/react/sessions";
    const results = useSessionQuery(api.myModule.mySessionQuery, { arg1: 1 });
    
    // Server: Define query with session
    import { customQuery } from "convex-helpers/server/customFunctions";
    import { SessionIdArg } from "convex-helpers/server/sessions";
    
    export const queryWithSession = customQuery(query, {
      args: SessionIdArg,
      input: async (ctx, { sessionId }) => {
        const anonymousUser = await getAnonUser(ctx, sessionId);
        return { ctx: { ...ctx, anonymousUser }, args: {} };
      },
    });
  5. Implement Presence in your application

    main

    To implement real-time presence (e.g., showing who is online or typing indicators), you can adapt the following files from the examples:

    • Server-side: convex/presence.ts (contains presence functions).
    • Client-side: src/hooks/usePresence.ts (React hooks).
    • Typing Indicators: src/hooks/useTypingIndicator.ts (optional).
    • UI Component: src/components/Facepile.tsx (an example component for displaying presence data).
  6. Retrieve File Metadata from Storage

    main

    To get metadata for a file, do not use the deprecated ctx.storage.getMetadata. Instead, query the _storage system table using ctx.db.system.get with an Id<"_storage">.

    Note: Convex storage items are stored as Blob objects. You must convert items to/from Blob when interacting with storage.

    import { query } from "./_generated/server";
    import { Id } from "./_generated/dataModel";
    
    type FileMetadata = {
        _id: Id<"_storage">;
        _creationTime: number;
        contentType?: string;
        sha256: string;
        size: number;
    }
    
    export const exampleQuery = query({
        args: { fileId: v.id("_storage") },
        handler: async (ctx, args) => {
            const metadata: FileMetadata | null = await ctx.db.system.get("_storage", args.fileId);
            console.log(metadata);
            return null;
        },
    });
  7. Implement server-persisted session data

    main

    There are two primary ways to handle sessions using convex-helpers:

    1. Client-generated sessions (Recommended): Create a session ID on the client and pass it to the server with every request. This is available by importing from "convex-helpers/server/sessions".

    2. Server-side session documents: Create a sessions table in your convex/schema.ts to store associated data for every new client. To implement this, copy the following files into your project:

      • packages/convex-helpers/server/sessions.ts for server-side action utilities like ctx.runSessionQuery(...).
      • packages/convex-helpers/react/sessions.ts for client-side React hooks like useSessionMutation(...).
  8. Register and call Convex functions

    main

    Function Registration

    • Public Functions: Use query, mutation, or action from ./_generated/server. These are exposed to the internet.
    • Internal Functions: Use internalQuery, internalMutation, or internalAction from ./_generated/server. These are private and can only be called by other Convex functions.
    • Requirement: ALWAYS include argument validators for all functions.

    Function Calling

    Use the ctx object to call functions from within other functions:

    • ctx.runQuery(functionReference, args)
    • ctx.runMutation(functionReference, args)
    • ctx.runAction(functionReference, args)

    Important Rules:

    • All calls require a FunctionReference (from the api or internal objects), not the function itself.
    • To avoid TypeScript circularity when calling a function in the same file, specify a type annotation on the return value.
    • Avoid calling actions from actions unless crossing runtimes (e.g., V8 to Node); use shared helper functions instead.
    export const f = query({
      args: { name: v.string() },
      handler: async (ctx, args) => {
        return "Hello " + args.name;
      },
    });
    
    export const g = query({
      args: {},
      handler: async (ctx, args) => {
        // Specify return type to avoid circularity
        const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });
        return null;
      },
    });
  9. Paginate QueryStreams in reactive queries

    main

    When using .paginate() with streams inside reactive queries, you must ensure pages remain contiguous to prevent holes or overlaps.

    • If using standard convex-helpers cached query helpers, pass customPagination: true in your pagination options.
    • On the client side, use the usePaginatedQuery hook from "convex-helpers/react".
    • Always pass the endCursor to the pagination method to maintain continuity.
  10. Schedule Cron jobs in Convex

    main

    To schedule recurring tasks, use the cronJobs utility.

    • Methods: Use crons.interval or crons.cron. Do not use the crons.hourly, crons.daily, or crons.weekly helpers.
    • Arguments: These methods require a FunctionReference. Do not pass the function directly.
    • Implementation: Declare a top-level crons object, call the scheduling methods, and export it as the default export of your crons.ts file.
    • Internal Functions: If a cron calls an internal function, import the internal object from _generated/api.
    import { cronJobs } from "convex/server";
    import { internal } from "./_generated/api";
    import { internalAction } from "./_generated/server";
    
    const empty = internalAction({
      args: {},
      handler: async (ctx, args) => {
        console.log("empty");
      },
    });
    
    const crons = cronJobs();
    
    // Run `internal.crons.empty` every two hours.
    crons.interval("delete inactive users", { hours: 2 }, internal.crons.empty, {});
    
    export default crons;