feat: Implement core Discord clone functionality including Convex backend, Electron frontend, and UI components for chat, voice, and settings.
This commit is contained in:
254
CONVEX_RULES.md
Normal file
254
CONVEX_RULES.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user