feat(convex): add implementation rules and configuration for backend functions
- Introduced a comprehensive markdown document outlining implementation rules for Convex functions, including syntax, registration, HTTP endpoints, and TypeScript usage. - Created a new configuration file for the Convex app, integrating the Resend service. - Added a new HTTP route for handling Shippo webhooks to ensure proper response handling. - Implemented integration tests for order timeline events, covering various scenarios including order fulfillment and status changes. - Enhanced existing functions with type safety improvements and additional validation logic. This commit establishes clear guidelines for backend development and improves the overall structure and reliability of the Convex application.
This commit is contained in:
240
convex/CLAUDE.md
Normal file
240
convex/CLAUDE.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# Convex Backend — Implementation Rules
|
||||
|
||||
Applies to all files inside `convex/`. These rules govern how to write, organize,
|
||||
and call Convex functions in this project.
|
||||
|
||||
---
|
||||
|
||||
## 1. Function Syntax
|
||||
|
||||
Always use the **new object syntax** with explicit `args` and `handler`:
|
||||
|
||||
```typescript
|
||||
import { query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const myFunction = query({
|
||||
args: { id: v.id("products") },
|
||||
handler: async (ctx, args) => {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Function Registration
|
||||
|
||||
| Visibility | Decorators to use |
|
||||
|------------|------------------|
|
||||
| Public API | `query`, `mutation`, `action` |
|
||||
| Private / internal | `internalQuery`, `internalMutation`, `internalAction` |
|
||||
|
||||
- **Always include argument validators** on every function — public and internal alike.
|
||||
- Do NOT register functions through the `api` or `internal` objects — those are for calling, not registering.
|
||||
- If a function returns nothing, it implicitly returns `null`. Do not return `undefined`.
|
||||
|
||||
---
|
||||
|
||||
## 3. HTTP Endpoints
|
||||
|
||||
Define all HTTP endpoints in `convex/http.ts` using `httpAction`:
|
||||
|
||||
```typescript
|
||||
import { httpRouter } from "convex/server";
|
||||
import { httpAction } from "./_generated/server";
|
||||
|
||||
const http = httpRouter();
|
||||
|
||||
http.route({
|
||||
path: "/api/some-route",
|
||||
method: "POST",
|
||||
handler: httpAction(async (ctx, req) => {
|
||||
const body = await req.bytes();
|
||||
return new Response(body, { status: 200 });
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
Endpoints are registered at the exact path specified — no automatic prefix is added.
|
||||
|
||||
---
|
||||
|
||||
## 4. Function Calling
|
||||
|
||||
- `ctx.runQuery` — call a query from a query, mutation, or action
|
||||
- `ctx.runMutation` — call a mutation from a mutation or action
|
||||
- `ctx.runAction` — call an action from an action (only cross-runtime: V8 → Node)
|
||||
- All calls take a **FunctionReference** — never pass the function directly
|
||||
- Minimize query/mutation calls from actions; splitting logic creates race conditions
|
||||
- When calling a function **in the same file**, add a type annotation on the return value to avoid TypeScript circularity errors:
|
||||
|
||||
```typescript
|
||||
const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Function References
|
||||
|
||||
- Public functions → `api` object from `convex/_generated/api.ts`
|
||||
- Internal functions → `internal` object from `convex/_generated/api.ts`
|
||||
- File-based routing: `convex/products.ts` → `api.products.myFn`; `convex/model/users.ts` → `api.model.users.myFn`
|
||||
|
||||
---
|
||||
|
||||
## 6. Validators
|
||||
|
||||
Use `v` from `convex/values`. Valid types:
|
||||
|
||||
| Convex Type | Validator | Notes |
|
||||
|-------------|-----------|-------|
|
||||
| Id | `v.id("tableName")` | |
|
||||
| String | `v.string()` | UTF-8, max 1 MB |
|
||||
| Number | `v.number()` | IEEE-754 float64 |
|
||||
| Boolean | `v.boolean()` | |
|
||||
| Null | `v.null()` | Use instead of `undefined` |
|
||||
| Int64 | `v.int64()` | BigInt between -2^63 and 2^63-1 |
|
||||
| Bytes | `v.bytes()` | ArrayBuffer, max 1 MB |
|
||||
| Array | `v.array(values)` | Max 8192 elements |
|
||||
| Object | `v.object({...})` | Max 1024 entries; keys cannot start with `$` or `_` |
|
||||
| Record | `v.record(keys, values)` | Dynamic keys; ASCII only, no `$`/`_` prefix |
|
||||
| Union | `v.union(...)` | Discriminated unions supported |
|
||||
| Optional | `v.optional(...)` | |
|
||||
|
||||
- `v.bigint()` is **deprecated** — use `v.int64()` instead
|
||||
- `v.map()` and `v.set()` are **not supported** — use `v.record()` instead
|
||||
|
||||
---
|
||||
|
||||
## 7. Schema
|
||||
|
||||
- Define all tables in `convex/schema.ts`
|
||||
- Import from `convex/server`: `defineSchema`, `defineTable`
|
||||
- System fields (`_id`, `_creationTime`) are added automatically — do not define them
|
||||
- Index naming: always include all indexed fields — e.g. `["field1", "field2"]` → name `"by_field1_and_field2"`
|
||||
- Index fields must be queried in the same order they are defined; create separate indexes for different orderings
|
||||
|
||||
---
|
||||
|
||||
## 8. TypeScript
|
||||
|
||||
- Use `Id<"tableName">` from `./_generated/dataModel` for document ID types — not plain `string`
|
||||
- Use `Doc<"tableName">` for full document types
|
||||
- Use `as const` for string literals in discriminated unions
|
||||
- Declare arrays as `const arr: Array<T> = [...]`
|
||||
- Declare records as `const rec: Record<K, V> = {...}`
|
||||
|
||||
---
|
||||
|
||||
## 9. Queries
|
||||
|
||||
- **Never use `.filter()`** — define an index in the schema and use `.withIndex()` instead
|
||||
- Use `.unique()` to assert a single result (throws if multiple match)
|
||||
- Use `.take(n)` or `.paginate()` to limit results
|
||||
- Prefer `for await (const row of query)` for async iteration over `.collect()` + loop
|
||||
- Convex does **not** support `.delete()` on queries — collect results then `ctx.db.delete(row._id)` each one
|
||||
- Default sort order is ascending `_creationTime`; use `.order("asc" | "desc")` to override
|
||||
|
||||
### Full-text search
|
||||
|
||||
```typescript
|
||||
const results = await ctx.db
|
||||
.query("products")
|
||||
.withSearchIndex("search_name", (q) =>
|
||||
q.search("name", "dog food").eq("category", "dogs"),
|
||||
)
|
||||
.take(10);
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
```typescript
|
||||
import { paginationOptsValidator } from "convex/server";
|
||||
|
||||
export const list = query({
|
||||
args: { paginationOpts: paginationOptsValidator, category: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db
|
||||
.query("products")
|
||||
.withIndex("by_category", (q) => q.eq("category", args.category))
|
||||
.order("desc")
|
||||
.paginate(args.paginationOpts);
|
||||
},
|
||||
});
|
||||
// Returns: { page, isDone, continueCursor }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Mutations
|
||||
|
||||
- `ctx.db.patch(id, fields)` — shallow merge; throws if document does not exist
|
||||
- `ctx.db.replace(id, doc)` — full replace; throws if document does not exist
|
||||
- `ctx.db.insert(table, doc)` — insert new document, returns `Id`
|
||||
- `ctx.db.delete(id)` — delete document by id
|
||||
|
||||
---
|
||||
|
||||
## 11. Actions
|
||||
|
||||
- Add `"use node";` at the top of files whose actions use Node.js built-ins
|
||||
- **Never** put `"use node";` in a file that also exports queries or mutations — separate the action into its own file
|
||||
- `fetch()` is available in the default Convex (V8) runtime — no `"use node"` needed just for fetch
|
||||
- **Never** use `ctx.db` inside an action — actions have no database access; use `ctx.runQuery` / `ctx.runMutation` instead
|
||||
- Only call an action from another action when you need to cross runtimes; otherwise extract shared logic into a plain async helper
|
||||
|
||||
```typescript
|
||||
"use node";
|
||||
import { internalAction } from "./_generated/server";
|
||||
|
||||
export const myAction = internalAction({
|
||||
args: {},
|
||||
handler: async (ctx, args) => {
|
||||
// Node.js built-ins available here
|
||||
return null;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Scheduling & Crons
|
||||
|
||||
- Use only `crons.interval` or `crons.cron` — do NOT use `crons.hourly`, `crons.daily`, or `crons.weekly`
|
||||
- Both methods take a FunctionReference — never pass the function directly
|
||||
- Export `crons` as `default` from `convex/crons.ts`
|
||||
- Always import `internal` from `_generated/api` when a cron calls an internal function, even in the same file
|
||||
|
||||
```typescript
|
||||
import { cronJobs } from "convex/server";
|
||||
import { internal } from "./_generated/api";
|
||||
|
||||
const crons = cronJobs();
|
||||
|
||||
crons.interval("cleanup expired carts", { hours: 24 }, internal.carts.cleanupExpired, {});
|
||||
|
||||
export default crons;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. File Storage
|
||||
|
||||
- `ctx.storage.getUrl(storageId)` — returns a signed URL or `null` if the file doesn't exist
|
||||
- Do **not** use the deprecated `ctx.storage.getMetadata()` — query the `_storage` system table instead:
|
||||
|
||||
```typescript
|
||||
const metadata = await ctx.db.system.get("_storage", args.fileId);
|
||||
```
|
||||
|
||||
- Storage items are `Blob` objects — convert to/from `Blob` when reading or writing
|
||||
|
||||
---
|
||||
|
||||
## 14. API Design & File Organisation
|
||||
|
||||
- Use file-based routing to organise public functions logically: `convex/products.ts`, `convex/orders.ts`, etc.
|
||||
- Extract shared business logic into `convex/model/*.ts` helpers and import them into public function files
|
||||
- Keep `internalQuery`/`internalMutation`/`internalAction` in the same file as the public function that calls them, unless the file would exceed a manageable size
|
||||
- Avoid chaining many `ctx.runQuery`/`ctx.runMutation` calls from a single action — race conditions become likely
|
||||
Reference in New Issue
Block a user