Files
DiscordClone/CONVEX_RULES.md

255 lines
7.5 KiB
Markdown

# Convex Framework Guidelines
Generic Convex best practices for building backend applications.
---
## Function Guidelines
### New Function Syntax
ALWAYS use the new function syntax for Convex functions:
```typescript
import { query } from "./_generated/server";
import { v } from "convex/values";
export const f = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
// Function body
},
});
```
### HTTP Endpoint Syntax
HTTP endpoints are defined in `convex/http.ts` and require an `httpAction` decorator:
```typescript
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
const http = httpRouter();
http.route({
path: "/echo",
method: "POST",
handler: httpAction(async (ctx, req) => {
const body = await req.bytes();
return new Response(body, { status: 200 });
}),
});
```
HTTP endpoints are registered at the exact path you specify.
### Validators
Array validator example:
```typescript
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export default mutation({
args: {
simpleArray: v.array(v.union(v.string(), v.number())),
},
handler: async (ctx, args) => {
//...
},
});
```
Discriminated union example:
```typescript
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
results: defineTable(
v.union(
v.object({
kind: v.literal("error"),
errorMessage: v.string(),
}),
v.object({
kind: v.literal("success"),
value: v.number(),
}),
),
)
});
```
Always use `v.null()` when returning null:
```typescript
export const exampleQuery = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
return null;
},
});
```
### Convex Types Reference
| Convex Type | TS/JS type | Example | Validator | Notes |
|-------------|-------------|---------------|----------------------------------|-------|
| Id | string | `doc._id` | `v.id(tableName)` | |
| Null | null | `null` | `v.null()` | Use `null` instead of `undefined` |
| Int64 | bigint | `3n` | `v.int64()` | -2^63 to 2^63-1 |
| Float64 | number | `3.1` | `v.number()` | IEEE-754 double-precision |
| Boolean | boolean | `true` | `v.boolean()` | |
| String | string | `"abc"` | `v.string()` | UTF-8, <1MB |
| Bytes | ArrayBuffer | `new ArrayBuffer(8)` | `v.bytes()` | <1MB |
| Array | Array | `[1, 3.2]` | `v.array(values)` | Max 8192 values |
| Object | Object | `{a: "abc"}` | `v.object({property: value})` | Max 1024 entries |
| Record | Record | `{"a": "1"}` | `v.record(keys, values)` | ASCII keys only |
### Function Registration
- Use `internalQuery`, `internalMutation`, `internalAction` for private functions (only callable by other Convex functions)
- Use `query`, `mutation`, `action` for public API functions
- You CANNOT register functions through the `api` or `internal` objects
- ALWAYS include argument and return validators. Use `returns: v.null()` if no return value
### Function Calling
- `ctx.runQuery` - call a query from query, mutation, or action
- `ctx.runMutation` - call a mutation from mutation or action
- `ctx.runAction` - call an action from action (only for crossing runtimes V8 to Node)
- All calls take a `FunctionReference`, not the function directly
- Minimize calls from actions to queries/mutations (risk of race conditions)
When calling functions in the same file, add type annotation:
```typescript
export const g = query({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });
return null;
},
});
```
### Function References
- Use `api` object for public functions: `api.example.f`
- Use `internal` object for private functions: `internal.example.g`
- File-based routing: `convex/messages/access.ts` -> `api.messages.access.h`
---
## Validator Guidelines
- `v.bigint()` is deprecated - use `v.int64()` instead
- Use `v.record()` for record types. `v.map()` and `v.set()` are not supported
---
## Schema Guidelines
- Define schema in `convex/schema.ts`
- Import schema functions from `convex/server`
- System fields `_creationTime` (v.number()) and `_id` (v.id(tableName)) are automatic
- Name indexes after their fields: `["field1", "field2"]` -> `"by_field1_and_field2"`
- Index fields must be queried in order defined
---
## TypeScript Guidelines
- Use `Id<'tableName'>` from `./_generated/dataModel` for typed IDs
- Record example: `Record<Id<"users">, string>`
- Use `as const` for string literals in discriminated unions
- Always define arrays as `const array: Array<T> = [...]`
- Always define records as `const record: Record<K, V> = {...}`
- Add `@types/node` when using Node.js built-in modules
---
## Query Guidelines
- Do NOT use `filter` - define an index and use `withIndex` instead
- No `.delete()` - use `.collect()` then iterate with `ctx.db.delete(row._id)`
- Use `.unique()` for single document (throws if multiple match)
- For async iteration, use `for await (const row of query)` instead of `.collect()`
### Ordering
- Default: ascending `_creationTime`
- Use `.order('asc')` or `.order('desc')`
- Indexed queries ordered by index columns
### Full Text Search
```typescript
const messages = await ctx.db
.query("messages")
.withSearchIndex("search_body", (q) =>
q.search("body", "hello hi").eq("channel", "#general"),
)
.take(10);
```
---
## Mutation Guidelines
- `ctx.db.replace` - fully replace document (throws if not exists)
- `ctx.db.patch` - shallow merge updates (throws if not exists)
---
## Action Guidelines
- Add `"use node";` at top of files using Node.js modules
- Actions don't have database access (`ctx.db` not available)
```typescript
import { action } from "./_generated/server";
export const exampleAction = action({
args: {},
returns: v.null(),
handler: async (ctx, args) => {
return null;
},
});
```
---
## Pagination
```typescript
import { paginationOptsValidator } from "convex/server";
export const listWithExtraArg = query({
args: { paginationOpts: paginationOptsValidator, author: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.withIndex("by_author", (q) => q.eq("author", args.author))
.order("desc")
.paginate(args.paginationOpts);
},
});
```
`paginationOpts`: `{ numItems: number, cursor: string | null }`
Returns: `{ page: Doc[], isDone: boolean, continueCursor: string }`
---
## Cron Jobs
- Use `crons.interval` or `crons.cron` only (not hourly/daily/weekly helpers)
- Pass FunctionReference, not the function directly
```typescript
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
crons.interval("job name", { hours: 2 }, internal.crons.myFunction, {});
export default crons;
```
---
## File Storage
- `ctx.storage.getUrl()` returns signed URL (null if file doesn't exist)
- Query `_storage` system table for metadata (don't use deprecated `ctx.storage.getMetadata`)
```typescript
const metadata = await ctx.db.system.get(args.fileId);
// Returns: { _id, _creationTime, contentType?, sha256, size }
```
- Store items as `Blob` objects