141 lines
3.6 KiB
Markdown
141 lines
3.6 KiB
Markdown
# Advanced Component Patterns
|
|
|
|
Additional patterns for Convex components that go beyond the basics covered in
|
|
the main skill file.
|
|
|
|
## Function Handles for callbacks
|
|
|
|
When the app needs to pass a callback function to the component, use function
|
|
handles. This is common for components that run app-defined logic on a schedule
|
|
or in a workflow.
|
|
|
|
```ts
|
|
// App side: create a handle and pass it to the component
|
|
import { createFunctionHandle } from "convex/server";
|
|
|
|
export const startJob = mutation({
|
|
handler: async (ctx) => {
|
|
const handle = await createFunctionHandle(internal.myModule.processItem);
|
|
await ctx.runMutation(components.workpool.enqueue, {
|
|
callback: handle,
|
|
});
|
|
},
|
|
});
|
|
```
|
|
|
|
```ts
|
|
// Component side: accept and invoke the handle
|
|
import { v } from "convex/values";
|
|
import type { FunctionHandle } from "convex/server";
|
|
import { mutation } from "./_generated/server.js";
|
|
|
|
export const enqueue = mutation({
|
|
args: { callback: v.string() },
|
|
handler: async (ctx, args) => {
|
|
const handle = args.callback as FunctionHandle<"mutation">;
|
|
await ctx.scheduler.runAfter(0, handle, {});
|
|
},
|
|
});
|
|
```
|
|
|
|
## Deriving validators from schema
|
|
|
|
Instead of manually repeating field types in return validators, extend the
|
|
schema validator:
|
|
|
|
```ts
|
|
import { v } from "convex/values";
|
|
import schema from "./schema.js";
|
|
|
|
const notificationDoc = schema.tables.notifications.validator.extend({
|
|
_id: v.id("notifications"),
|
|
_creationTime: v.number(),
|
|
});
|
|
|
|
export const getLatest = query({
|
|
args: {},
|
|
returns: v.nullable(notificationDoc),
|
|
handler: async (ctx) => {
|
|
return await ctx.db.query("notifications").order("desc").first();
|
|
},
|
|
});
|
|
```
|
|
|
|
## Static configuration with a globals table
|
|
|
|
A common pattern for component configuration is a single-document "globals"
|
|
table:
|
|
|
|
```ts
|
|
// schema.ts
|
|
export default defineSchema({
|
|
globals: defineTable({
|
|
maxRetries: v.number(),
|
|
webhookUrl: v.optional(v.string()),
|
|
}),
|
|
// ... other tables
|
|
});
|
|
```
|
|
|
|
```ts
|
|
// lib.ts
|
|
export const configure = mutation({
|
|
args: { maxRetries: v.number(), webhookUrl: v.optional(v.string()) },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const existing = await ctx.db.query("globals").first();
|
|
if (existing) {
|
|
await ctx.db.patch(existing._id, args);
|
|
} else {
|
|
await ctx.db.insert("globals", args);
|
|
}
|
|
return null;
|
|
},
|
|
});
|
|
```
|
|
|
|
## Class-based client wrappers
|
|
|
|
For components with many functions or configuration options, a class-based
|
|
client provides a cleaner API. This pattern is common in published components.
|
|
|
|
```ts
|
|
// src/client/index.ts
|
|
import type { GenericMutationCtx, GenericDataModel } from "convex/server";
|
|
import type { ComponentApi } from "../component/_generated/component.js";
|
|
|
|
type MutationCtx = Pick<GenericMutationCtx<GenericDataModel>, "runMutation">;
|
|
|
|
export class Notifications {
|
|
constructor(
|
|
private component: ComponentApi,
|
|
private options?: { defaultChannel?: string },
|
|
) {}
|
|
|
|
async send(ctx: MutationCtx, args: { userId: string; message: string }) {
|
|
return await ctx.runMutation(this.component.lib.send, {
|
|
...args,
|
|
channel: this.options?.defaultChannel ?? "default",
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
```ts
|
|
// App usage
|
|
import { Notifications } from "@convex-dev/notifications";
|
|
import { components } from "./_generated/api";
|
|
|
|
const notifications = new Notifications(components.notifications, {
|
|
defaultChannel: "alerts",
|
|
});
|
|
|
|
export const send = mutation({
|
|
args: { message: v.string() },
|
|
handler: async (ctx, args) => {
|
|
const userId = await getAuthUserId(ctx);
|
|
await notifications.send(ctx, { userId, message: args.message });
|
|
},
|
|
});
|
|
```
|