feat: wire convex data foundations
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# 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 });
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
# Hybrid Convex Components
|
||||
|
||||
Read this file only when the user explicitly wants a hybrid setup.
|
||||
|
||||
## What This Means
|
||||
|
||||
A hybrid component combines a local Convex component with shared library code.
|
||||
|
||||
This can help when:
|
||||
|
||||
- the user wants a local install but also shared package logic
|
||||
- the component needs extension points or override hooks
|
||||
- some logic should live in normal TypeScript code outside the component
|
||||
boundary
|
||||
|
||||
## Default Advice
|
||||
|
||||
Treat hybrid as an advanced option, not the default.
|
||||
|
||||
Before choosing it, ask:
|
||||
|
||||
- Why is a plain local component not enough?
|
||||
- Why is a packaged component not enough?
|
||||
- What exactly needs to stay overridable or shared?
|
||||
|
||||
If the answer is vague, fall back to local or packaged.
|
||||
|
||||
## Risks
|
||||
|
||||
- More moving parts
|
||||
- Harder upgrades and backwards compatibility
|
||||
- Easier to blur the component boundary
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] User explicitly needs hybrid behavior
|
||||
- [ ] Local-only and packaged-only options were considered first
|
||||
- [ ] The extension points are clearly defined before coding
|
||||
@@ -0,0 +1,39 @@
|
||||
# Local Convex Components
|
||||
|
||||
Read this file when the component should live inside the current app and does
|
||||
not need to be published as an npm package.
|
||||
|
||||
## When to Choose This
|
||||
|
||||
- The user wants the simplest path
|
||||
- The component only needs to work in this repo
|
||||
- The goal is extracting app logic into a cleaner boundary
|
||||
|
||||
## Default Layout
|
||||
|
||||
Use this structure unless the repo already has a clear alternative pattern:
|
||||
|
||||
```text
|
||||
convex/
|
||||
convex.config.ts
|
||||
components/
|
||||
<name>/
|
||||
convex.config.ts
|
||||
schema.ts
|
||||
<feature>.ts
|
||||
```
|
||||
|
||||
## Workflow Notes
|
||||
|
||||
- Define the component with `defineComponent("<name>")`
|
||||
- Install it from the app with `defineApp()` and `app.use(...)`
|
||||
- Keep auth, env access, public API wrappers, and HTTP route mounting in the app
|
||||
- Let the component own isolated tables and reusable backend workflows
|
||||
- Add app wrappers if clients need to call into the component
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Component is inside `convex/components/<name>/`
|
||||
- [ ] App installs it with `app.use(...)`
|
||||
- [ ] Component owns only its own tables
|
||||
- [ ] App wrappers handle client-facing calls when needed
|
||||
@@ -0,0 +1,54 @@
|
||||
# Packaged Convex Components
|
||||
|
||||
Read this file when the user wants a reusable npm package or a component shared
|
||||
across multiple apps.
|
||||
|
||||
## When to Choose This
|
||||
|
||||
- The user wants to publish the component
|
||||
- The user wants a stable reusable package boundary
|
||||
- The component will be shared across multiple apps or teams
|
||||
|
||||
## Default Approach
|
||||
|
||||
- Prefer starting from `npx create-convex@latest --component` when possible
|
||||
- Keep the official authoring docs as the source of truth for package layout and
|
||||
exports
|
||||
- Validate the bundled package through an example app, not just the source files
|
||||
|
||||
## Build Flow
|
||||
|
||||
When building a packaged component, make sure the bundled output exists before
|
||||
the example app tries to consume it.
|
||||
|
||||
Recommended order:
|
||||
|
||||
1. `npx convex codegen --component-dir ./path/to/component`
|
||||
2. Run the package build command
|
||||
3. Run `npx convex dev --typecheck-components` in the example app
|
||||
|
||||
Do not assume normal app codegen is enough for packaged component workflows.
|
||||
|
||||
## Package Exports
|
||||
|
||||
If publishing to npm, make sure the package exposes the entry points apps need:
|
||||
|
||||
- package root for client helpers, types, or classes
|
||||
- `./convex.config.js` for installing the component
|
||||
- `./_generated/component.js` for the app-facing `ComponentApi` type
|
||||
- `./test` for testing helpers when applicable
|
||||
|
||||
## Testing
|
||||
|
||||
- Use `convex-test` for component logic
|
||||
- Register the component schema and modules with the test instance
|
||||
- Test app-side wrapper code from an example app that installs the package
|
||||
- Export a small helper from `./test` if consumers need easy test registration
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Packaging is actually required
|
||||
- [ ] Build order avoids bundle and codegen races
|
||||
- [ ] Package exports include install and typing entry points
|
||||
- [ ] Example app exercises the packaged component
|
||||
- [ ] Core behavior is covered by tests
|
||||
Reference in New Issue
Block a user