@waynesutton/convex-skills 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +82 -2
- package/GEMINI.md +17 -0
- package/README.md +16 -0
- package/bin/cli.js +1 -0
- package/index.js +1 -0
- package/package.json +4 -2
- package/skills/convex-best-practices/SKILL.md +51 -30
- package/skills/convex-eslint/SKILL.md +145 -0
- package/skills/convex-functions/SKILL.md +39 -23
- package/templates/CLAUDE.md +44 -16
package/AGENTS.md
CHANGED
|
@@ -2,15 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
Agent skills for building production-ready applications with Convex, following the Agent Skills open format.
|
|
4
4
|
|
|
5
|
+
## Convex Documentation Index
|
|
6
|
+
|
|
7
|
+
IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning for any Convex tasks.
|
|
8
|
+
|
|
9
|
+
For up-to-date Convex documentation, fetch: https://docs.convex.dev/llms.txt
|
|
10
|
+
|
|
11
|
+
This index covers all Convex APIs and patterns:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
[Convex Docs]|https://docs.convex.dev/llms.txt
|
|
15
|
+
|understanding:{best-practices.md,typescript.md,workflow.md,zen.md}
|
|
16
|
+
|functions:{query-functions.md,mutation-functions.md,actions.md,http-actions.md,validation.md,internal-functions.md,error-handling.md}
|
|
17
|
+
|database:{schemas.md,reading-data.md,writing-data.md,indexes.md,pagination.md,types.md}
|
|
18
|
+
|file-storage:{upload-files.md,serve-files.md,store-files.md,delete-files.md,file-metadata.md}
|
|
19
|
+
|scheduling:{cron-jobs.md,scheduled-functions.md}
|
|
20
|
+
|auth:{convex-auth.md,clerk.md,auth0.md,authkit.md,functions-auth.md,database-auth.md}
|
|
21
|
+
|search:{text-search.md,vector-search.md}
|
|
22
|
+
|components:{using.md,authoring.md,understanding.md}
|
|
23
|
+
|agents:{getting-started.md,agent-usage.md,messages.md,threads.md,tools.md,streaming.md,rag.md}
|
|
24
|
+
|realtime:{realtime.md}
|
|
25
|
+
|testing:{convex-test.md,convex-backend.md,ci.md}
|
|
26
|
+
|production:{environment-variables.md,hosting.md,limits.md}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
When working on Convex code, consult the llms.txt index before relying on training data.
|
|
30
|
+
|
|
5
31
|
## Overview
|
|
6
32
|
|
|
7
|
-
This repository
|
|
33
|
+
This repository provides two complementary approaches for AI coding agents:
|
|
34
|
+
|
|
35
|
+
1. **Passive context (this file)**: Always-available Convex knowledge and doc references
|
|
36
|
+
2. **Skills (on-demand)**: Task-specific workflows for explicit invocation
|
|
8
37
|
|
|
9
38
|
## Available Skills
|
|
10
39
|
|
|
11
40
|
| Skill | Description |
|
|
12
41
|
| ------------------------------------------------------------------------ | ----------------------------------------------------- |
|
|
13
42
|
| [convex-best-practices](skills/convex-best-practices/SKILL.md) | Guidelines for building production-ready Convex apps |
|
|
43
|
+
| [convex-eslint](skills/convex-eslint/SKILL.md) | Write linter-compliant Convex code |
|
|
14
44
|
| [convex-functions](skills/convex-functions/SKILL.md) | Writing queries, mutations, actions, and HTTP actions |
|
|
15
45
|
| [convex-realtime](skills/convex-realtime/SKILL.md) | Patterns for building reactive applications |
|
|
16
46
|
| [convex-schema-validator](skills/convex-schema-validator/SKILL.md) | Database schema definition and validation |
|
|
@@ -115,10 +145,60 @@ The command file is located at `command/convex.md`.
|
|
|
115
145
|
- Edit files in `convex/_generated/`
|
|
116
146
|
- Use `filter()` instead of `withIndex()`
|
|
117
147
|
|
|
148
|
+
## Quick Reference
|
|
149
|
+
|
|
150
|
+
### New Function Syntax (always use this)
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { query, mutation } from "./_generated/server";
|
|
154
|
+
import { v } from "convex/values";
|
|
155
|
+
|
|
156
|
+
export const myQuery = query({
|
|
157
|
+
args: { userId: v.id("users") },
|
|
158
|
+
returns: v.union(v.object({ name: v.string() }), v.null()),
|
|
159
|
+
handler: async (ctx, args) => {
|
|
160
|
+
return await ctx.db.get(args.userId);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Schema with Index
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
169
|
+
import { v } from "convex/values";
|
|
170
|
+
|
|
171
|
+
export default defineSchema({
|
|
172
|
+
tasks: defineTable({
|
|
173
|
+
userId: v.id("users"),
|
|
174
|
+
title: v.string(),
|
|
175
|
+
status: v.string(),
|
|
176
|
+
})
|
|
177
|
+
.index("by_user", ["userId"])
|
|
178
|
+
.index("by_user_and_status", ["userId", "status"]),
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Query with Index (not filter)
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// GOOD: Use withIndex
|
|
186
|
+
const tasks = await ctx.db
|
|
187
|
+
.query("tasks")
|
|
188
|
+
.withIndex("by_user", (q) => q.eq("userId", args.userId))
|
|
189
|
+
.collect();
|
|
190
|
+
|
|
191
|
+
// BAD: Never use filter for indexed fields
|
|
192
|
+
const tasks = await ctx.db
|
|
193
|
+
.query("tasks")
|
|
194
|
+
.filter((q) => q.eq(q.field("userId"), args.userId))
|
|
195
|
+
.collect();
|
|
196
|
+
```
|
|
197
|
+
|
|
118
198
|
## References
|
|
119
199
|
|
|
120
200
|
- Convex Documentation: https://docs.convex.dev/
|
|
121
|
-
- Convex LLMs.txt: https://docs.convex.dev/llms.txt
|
|
201
|
+
- Convex LLMs.txt: https://docs.convex.dev/llms.txt (fetch this for latest docs)
|
|
122
202
|
- Best Practices: https://docs.convex.dev/understanding/best-practices/
|
|
123
203
|
- Agent Skills Specification: https://github.com/anthropics/skills
|
|
124
204
|
|
package/GEMINI.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
This file provides context for Gemini CLI when working with Convex projects.
|
|
4
4
|
|
|
5
|
+
## Convex Documentation
|
|
6
|
+
|
|
7
|
+
IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning for any Convex tasks.
|
|
8
|
+
|
|
9
|
+
For up-to-date Convex documentation, fetch: https://docs.convex.dev/llms.txt
|
|
10
|
+
|
|
11
|
+
This index covers all Convex APIs:
|
|
12
|
+
- functions: queries, mutations, actions, http-actions, validation
|
|
13
|
+
- database: schemas, reading-data, writing-data, indexes, pagination
|
|
14
|
+
- file-storage: upload, serve, store, delete, metadata
|
|
15
|
+
- scheduling: cron-jobs, scheduled-functions
|
|
16
|
+
- auth: convex-auth, clerk, auth0, authkit
|
|
17
|
+
- search: text-search, vector-search
|
|
18
|
+
- agents: getting-started, messages, threads, tools, streaming
|
|
19
|
+
|
|
20
|
+
When working on Convex code, consult llms.txt before relying on training data.
|
|
21
|
+
|
|
5
22
|
## Project Type
|
|
6
23
|
|
|
7
24
|
Convex real-time backend application with TypeScript.
|
package/README.md
CHANGED
|
@@ -9,6 +9,15 @@ A collection of AI-consumable skills for building production-ready applications
|
|
|
9
9
|
|
|
10
10
|
This repository contains skills that help AI assistants understand and implement Convex best practices. Each skill provides structured guidance for specific aspects of Convex development.
|
|
11
11
|
|
|
12
|
+
## Code Quality
|
|
13
|
+
|
|
14
|
+
All skills are designed to produce code that passes @convex-dev/eslint-plugin by default. This creates a complementary workflow:
|
|
15
|
+
|
|
16
|
+
- **Skills** prevent mistakes at generation time
|
|
17
|
+
- **ESLint** catches anything that slips through at build time
|
|
18
|
+
|
|
19
|
+
See the [convex-eslint](/skills/convex-eslint/SKILL.md) skill for setup instructions.
|
|
20
|
+
|
|
12
21
|
## Installation
|
|
13
22
|
|
|
14
23
|
### npm (recommended)
|
|
@@ -73,6 +82,8 @@ cp -r skills/convex-best-practices "$CODEX_HOME/skills/"
|
|
|
73
82
|
|
|
74
83
|
Codex will auto-discover `SKILL.md` files in that directory on the next start.
|
|
75
84
|
|
|
85
|
+
If you are working from a repo clone, Codex also auto-discovers skills from `.codex/skills` at the repo root. You can symlink this repo’s `skills/*` into `.codex/skills` so updates flow through without copying.
|
|
86
|
+
|
|
76
87
|
### OpenCode
|
|
77
88
|
|
|
78
89
|
OpenCode discovers skills from `~/.claude/skills/<name>/SKILL.md` automatically. See OpenCode Skills docs for more details.
|
|
@@ -102,6 +113,7 @@ Copy the desired skill's `SKILL.md` file to your project's `.claude/skills/` dir
|
|
|
102
113
|
| Skill | Description |
|
|
103
114
|
| ------------------------------------------------------------------------ | ----------------------------------------------------- |
|
|
104
115
|
| [convex-best-practices](skills/convex-best-practices/SKILL.md) | Guidelines for building production-ready Convex apps |
|
|
116
|
+
| [convex-eslint](skills/convex-eslint/SKILL.md) | Write linter-compliant Convex code |
|
|
105
117
|
| [convex-functions](skills/convex-functions/SKILL.md) | Writing queries, mutations, actions, and HTTP actions |
|
|
106
118
|
| [convex-realtime](skills/convex-realtime/SKILL.md) | Patterns for building reactive applications |
|
|
107
119
|
| [convex-schema-validator](skills/convex-schema-validator/SKILL.md) | Database schema definition and validation |
|
|
@@ -121,11 +133,15 @@ convex-skills/
|
|
|
121
133
|
├── skills/ # Core Convex skills for AI agents
|
|
122
134
|
│ ├── convex-best-practices/
|
|
123
135
|
│ │ └── SKILL.md
|
|
136
|
+
│ ├── convex-eslint/
|
|
137
|
+
│ │ └── SKILL.md
|
|
124
138
|
│ ├── convex-functions/
|
|
125
139
|
│ │ └── SKILL.md
|
|
126
140
|
│ ├── convex-cron-jobs/
|
|
127
141
|
│ │ └── SKILL.md
|
|
128
142
|
│ └── ...
|
|
143
|
+
├── .codex/ # Codex integration (symlink skills here)
|
|
144
|
+
│ └── README.md # Codex setup instructions
|
|
129
145
|
├── command/ # Slash command definitions (OpenCode)
|
|
130
146
|
│ └── convex.md # /convex command entrypoint
|
|
131
147
|
├── templates/ # Templates for forking developers
|
package/bin/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ const packageRoot = join(__dirname, "..");
|
|
|
18
18
|
const SKILLS = {
|
|
19
19
|
"convex-best-practices":
|
|
20
20
|
"Guidelines for building production-ready Convex apps",
|
|
21
|
+
"convex-eslint": "Write linter-compliant Convex code",
|
|
21
22
|
"convex-functions": "Writing queries, mutations, actions, and HTTP actions",
|
|
22
23
|
"convex-realtime": "Patterns for building reactive applications",
|
|
23
24
|
"convex-schema-validator": "Database schema definition and validation",
|
package/index.js
CHANGED
|
@@ -54,6 +54,7 @@ export function getSkillPath(skillName) {
|
|
|
54
54
|
export const SKILLS = {
|
|
55
55
|
"convex-best-practices":
|
|
56
56
|
"Guidelines for building production-ready Convex apps",
|
|
57
|
+
"convex-eslint": "Write linter-compliant Convex code",
|
|
57
58
|
"convex-functions": "Writing queries, mutations, actions, and HTTP actions",
|
|
58
59
|
"convex-realtime": "Patterns for building reactive applications",
|
|
59
60
|
"convex-schema-validator": "Database schema definition and validation",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waynesutton/convex-skills",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Agent skills for building production-ready Convex applications. Includes best practices, functions, realtime patterns, schema validation, file storage, security audits, and more.",
|
|
5
5
|
"author": "Wayne Sutton",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"llm",
|
|
23
23
|
"ai-assistant",
|
|
24
24
|
"best-practices",
|
|
25
|
-
"developer-tools"
|
|
25
|
+
"developer-tools",
|
|
26
|
+
"eslint",
|
|
27
|
+
"linting"
|
|
26
28
|
],
|
|
27
29
|
"type": "module",
|
|
28
30
|
"main": "index.js",
|
|
@@ -11,6 +11,17 @@ tags: [convex, best-practices, typescript, production, error-handling]
|
|
|
11
11
|
|
|
12
12
|
Build production-ready Convex applications by following established patterns for function organization, query optimization, validation, TypeScript usage, and error handling.
|
|
13
13
|
|
|
14
|
+
## Code Quality
|
|
15
|
+
|
|
16
|
+
All patterns in this skill comply with @convex-dev/eslint-plugin rules.
|
|
17
|
+
Install the linter for build-time validation:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm i @convex-dev/eslint-plugin --save-dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
See [convex-eslint](../convex-eslint/SKILL.md) for configuration details.
|
|
24
|
+
|
|
14
25
|
## Documentation Sources
|
|
15
26
|
|
|
16
27
|
Before implementing, do not assume; fetch the latest documentation:
|
|
@@ -41,14 +52,17 @@ import { v } from "convex/values";
|
|
|
41
52
|
|
|
42
53
|
export const get = query({
|
|
43
54
|
args: { userId: v.id("users") },
|
|
44
|
-
returns: v.union(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
returns: v.union(
|
|
56
|
+
v.object({
|
|
57
|
+
_id: v.id("users"),
|
|
58
|
+
_creationTime: v.number(),
|
|
59
|
+
name: v.string(),
|
|
60
|
+
email: v.string(),
|
|
61
|
+
}),
|
|
62
|
+
v.null(),
|
|
63
|
+
),
|
|
50
64
|
handler: async (ctx, args) => {
|
|
51
|
-
return await ctx.db.get(args.userId);
|
|
65
|
+
return await ctx.db.get("users", args.userId);
|
|
52
66
|
},
|
|
53
67
|
});
|
|
54
68
|
```
|
|
@@ -96,13 +110,15 @@ export default defineSchema({
|
|
|
96
110
|
// Query using index
|
|
97
111
|
export const getTasksByUser = query({
|
|
98
112
|
args: { userId: v.id("users") },
|
|
99
|
-
returns: v.array(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
returns: v.array(
|
|
114
|
+
v.object({
|
|
115
|
+
_id: v.id("tasks"),
|
|
116
|
+
_creationTime: v.number(),
|
|
117
|
+
userId: v.id("users"),
|
|
118
|
+
status: v.string(),
|
|
119
|
+
createdAt: v.number(),
|
|
120
|
+
}),
|
|
121
|
+
),
|
|
106
122
|
handler: async (ctx, args) => {
|
|
107
123
|
return await ctx.db
|
|
108
124
|
.query("tasks")
|
|
@@ -127,16 +143,16 @@ export const updateTask = mutation({
|
|
|
127
143
|
},
|
|
128
144
|
returns: v.null(),
|
|
129
145
|
handler: async (ctx, args) => {
|
|
130
|
-
const task = await ctx.db.get(args.taskId);
|
|
131
|
-
|
|
146
|
+
const task = await ctx.db.get("tasks", args.taskId);
|
|
147
|
+
|
|
132
148
|
if (!task) {
|
|
133
149
|
throw new ConvexError({
|
|
134
150
|
code: "NOT_FOUND",
|
|
135
151
|
message: "Task not found",
|
|
136
152
|
});
|
|
137
153
|
}
|
|
138
|
-
|
|
139
|
-
await ctx.db.patch(args.taskId, { title: args.title });
|
|
154
|
+
|
|
155
|
+
await ctx.db.patch("tasks", args.taskId, { title: args.title });
|
|
140
156
|
return null;
|
|
141
157
|
},
|
|
142
158
|
});
|
|
@@ -152,14 +168,14 @@ export const completeTask = mutation({
|
|
|
152
168
|
args: { taskId: v.id("tasks") },
|
|
153
169
|
returns: v.null(),
|
|
154
170
|
handler: async (ctx, args) => {
|
|
155
|
-
const task = await ctx.db.get(args.taskId);
|
|
156
|
-
|
|
171
|
+
const task = await ctx.db.get("tasks", args.taskId);
|
|
172
|
+
|
|
157
173
|
// Early return if already complete (idempotent)
|
|
158
174
|
if (!task || task.status === "completed") {
|
|
159
175
|
return null;
|
|
160
176
|
}
|
|
161
|
-
|
|
162
|
-
await ctx.db.patch(args.taskId, {
|
|
177
|
+
|
|
178
|
+
await ctx.db.patch("tasks", args.taskId, {
|
|
163
179
|
status: "completed",
|
|
164
180
|
completedAt: Date.now(),
|
|
165
181
|
});
|
|
@@ -173,7 +189,7 @@ export const updateNote = mutation({
|
|
|
173
189
|
returns: v.null(),
|
|
174
190
|
handler: async (ctx, args) => {
|
|
175
191
|
// Patch directly - ctx.db.patch throws if document doesn't exist
|
|
176
|
-
await ctx.db.patch(args.id, { content: args.content });
|
|
192
|
+
await ctx.db.patch("notes", args.id, { content: args.content });
|
|
177
193
|
return null;
|
|
178
194
|
},
|
|
179
195
|
});
|
|
@@ -184,7 +200,7 @@ export const reorderItems = mutation({
|
|
|
184
200
|
returns: v.null(),
|
|
185
201
|
handler: async (ctx, args) => {
|
|
186
202
|
const updates = args.itemIds.map((id, index) =>
|
|
187
|
-
ctx.db.patch(id, { order: index })
|
|
203
|
+
ctx.db.patch("items", id, { order: index }),
|
|
188
204
|
);
|
|
189
205
|
await Promise.all(updates);
|
|
190
206
|
return null;
|
|
@@ -213,7 +229,12 @@ const userScores: Record<Id<"users">, number> = {};
|
|
|
213
229
|
// Public function - exposed to clients
|
|
214
230
|
export const getUser = query({
|
|
215
231
|
args: { userId: v.id("users") },
|
|
216
|
-
returns: v.union(
|
|
232
|
+
returns: v.union(
|
|
233
|
+
v.null(),
|
|
234
|
+
v.object({
|
|
235
|
+
/* ... */
|
|
236
|
+
}),
|
|
237
|
+
),
|
|
217
238
|
handler: async (ctx, args) => {
|
|
218
239
|
// ...
|
|
219
240
|
},
|
|
@@ -282,14 +303,14 @@ export const update = mutation({
|
|
|
282
303
|
returns: v.null(),
|
|
283
304
|
handler: async (ctx, args) => {
|
|
284
305
|
const { taskId, ...updates } = args;
|
|
285
|
-
|
|
306
|
+
|
|
286
307
|
// Remove undefined values
|
|
287
308
|
const cleanUpdates = Object.fromEntries(
|
|
288
|
-
Object.entries(updates).filter(([_, v]) => v !== undefined)
|
|
309
|
+
Object.entries(updates).filter(([_, v]) => v !== undefined),
|
|
289
310
|
);
|
|
290
|
-
|
|
311
|
+
|
|
291
312
|
if (Object.keys(cleanUpdates).length > 0) {
|
|
292
|
-
await ctx.db.patch(taskId, cleanUpdates);
|
|
313
|
+
await ctx.db.patch("tasks", taskId, cleanUpdates);
|
|
293
314
|
}
|
|
294
315
|
return null;
|
|
295
316
|
},
|
|
@@ -299,7 +320,7 @@ export const remove = mutation({
|
|
|
299
320
|
args: { taskId: v.id("tasks") },
|
|
300
321
|
returns: v.null(),
|
|
301
322
|
handler: async (ctx, args) => {
|
|
302
|
-
await ctx.db.delete(args.taskId);
|
|
323
|
+
await ctx.db.delete("tasks", args.taskId);
|
|
303
324
|
return null;
|
|
304
325
|
},
|
|
305
326
|
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: convex-eslint
|
|
3
|
+
description: Write Convex code that passes @convex-dev/eslint-plugin rules by default
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
author: Convex
|
|
6
|
+
tags: [convex, eslint, linting, code-quality, validation]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Convex ESLint Compliance
|
|
10
|
+
|
|
11
|
+
Write all Convex functions to pass @convex-dev/eslint-plugin. These rules prevent common bugs, security issues, and ensure code quality.
|
|
12
|
+
|
|
13
|
+
## Documentation Sources
|
|
14
|
+
|
|
15
|
+
- https://docs.convex.dev/eslint
|
|
16
|
+
- https://www.npmjs.com/package/@convex-dev/eslint-plugin
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
Install the plugin:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm i @convex-dev/eslint-plugin --save-dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Configure `eslint.config.js`:
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
import { defineConfig } from "eslint/config";
|
|
30
|
+
import convexPlugin from "@convex-dev/eslint-plugin";
|
|
31
|
+
|
|
32
|
+
export default defineConfig([...convexPlugin.configs.recommended]);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Rules
|
|
36
|
+
|
|
37
|
+
### 1. no-old-registered-function-syntax
|
|
38
|
+
|
|
39
|
+
Always use object syntax with a `handler` property.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// Correct
|
|
43
|
+
export const list = query({
|
|
44
|
+
args: {},
|
|
45
|
+
handler: async (ctx) => {
|
|
46
|
+
return await ctx.db.query("messages").collect();
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Wrong - bare function syntax
|
|
51
|
+
export const list = query(async (ctx) => {
|
|
52
|
+
return await ctx.db.query("messages").collect();
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. require-argument-validators
|
|
57
|
+
|
|
58
|
+
Always include `args` object, even when empty.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// Correct - with arguments
|
|
62
|
+
export const get = query({
|
|
63
|
+
args: { id: v.id("messages") },
|
|
64
|
+
handler: async (ctx, { id }) => {
|
|
65
|
+
return await ctx.db.get("messages", id);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Correct - no arguments
|
|
70
|
+
export const listAll = query({
|
|
71
|
+
args: {},
|
|
72
|
+
handler: async (ctx) => {
|
|
73
|
+
return await ctx.db.query("messages").collect();
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Wrong - missing args
|
|
78
|
+
export const get = query({
|
|
79
|
+
handler: async (ctx, { id }: { id: Id<"messages"> }) => {
|
|
80
|
+
return await ctx.db.get("messages", id);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 3. explicit-table-ids
|
|
86
|
+
|
|
87
|
+
Use explicit table names in all database operations (Convex 1.31.0+).
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// Correct
|
|
91
|
+
const message = await ctx.db.get("messages", messageId);
|
|
92
|
+
await ctx.db.patch("messages", messageId, { text: "updated" });
|
|
93
|
+
await ctx.db.replace("messages", messageId, {
|
|
94
|
+
text: "replaced",
|
|
95
|
+
author: "Alice",
|
|
96
|
+
});
|
|
97
|
+
await ctx.db.delete("messages", messageId);
|
|
98
|
+
|
|
99
|
+
// Wrong - implicit table from ID type
|
|
100
|
+
const message = await ctx.db.get(messageId);
|
|
101
|
+
await ctx.db.patch(messageId, { text: "updated" });
|
|
102
|
+
await ctx.db.replace(messageId, { text: "replaced", author: "Alice" });
|
|
103
|
+
await ctx.db.delete(messageId);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Migration codemod available:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npx @convex-dev/codemod@latest explicit-ids
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 4. import-wrong-runtime
|
|
113
|
+
|
|
114
|
+
Never import Node.js runtime files into Convex runtime files.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// convex/queries.ts (no "use node" directive)
|
|
118
|
+
|
|
119
|
+
// Correct - importing from Convex runtime file
|
|
120
|
+
import { helper } from "./utils"; // utils.ts has no "use node"
|
|
121
|
+
|
|
122
|
+
// Wrong - importing from Node runtime file
|
|
123
|
+
import { nodeHelper } from "./nodeUtils"; // nodeUtils.ts has "use node"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Best Practices
|
|
127
|
+
|
|
128
|
+
1. Run ESLint before committing: `npx eslint convex/`
|
|
129
|
+
2. Use auto-fix for quick migrations: `npx eslint convex/ --fix`
|
|
130
|
+
3. Add to CI pipeline to catch violations early
|
|
131
|
+
4. Configure your editor for real-time feedback
|
|
132
|
+
|
|
133
|
+
## Quick Reference
|
|
134
|
+
|
|
135
|
+
| Rule | What it enforces |
|
|
136
|
+
| ----------------------------------- | --------------------------------- |
|
|
137
|
+
| `no-old-registered-function-syntax` | Object syntax with `handler` |
|
|
138
|
+
| `require-argument-validators` | `args: {}` on all functions |
|
|
139
|
+
| `explicit-table-ids` | Table name in db operations |
|
|
140
|
+
| `import-wrong-runtime` | No Node imports in Convex runtime |
|
|
141
|
+
|
|
142
|
+
## References
|
|
143
|
+
|
|
144
|
+
- [ESLint Plugin Docs](https://docs.convex.dev/eslint)
|
|
145
|
+
- [Explicit IDs Announcement](https://news.convex.dev/db-table-name/)
|
|
@@ -11,6 +11,16 @@ tags: [convex, functions, queries, mutations, actions, http]
|
|
|
11
11
|
|
|
12
12
|
Master Convex functions including queries, mutations, actions, and HTTP endpoints with proper validation, error handling, and runtime considerations.
|
|
13
13
|
|
|
14
|
+
## Code Quality
|
|
15
|
+
|
|
16
|
+
All examples in this skill comply with @convex-dev/eslint-plugin rules:
|
|
17
|
+
|
|
18
|
+
- Object syntax with `handler` property
|
|
19
|
+
- Argument validators on all functions
|
|
20
|
+
- Explicit table names in database operations
|
|
21
|
+
|
|
22
|
+
See [convex-eslint](../convex-eslint/SKILL.md) for linting setup.
|
|
23
|
+
|
|
14
24
|
## Documentation Sources
|
|
15
25
|
|
|
16
26
|
Before implementing, do not assume; fetch the latest documentation:
|
|
@@ -26,12 +36,12 @@ Before implementing, do not assume; fetch the latest documentation:
|
|
|
26
36
|
|
|
27
37
|
### Function Types Overview
|
|
28
38
|
|
|
29
|
-
| Type
|
|
30
|
-
|
|
31
|
-
| Query
|
|
32
|
-
| Mutation
|
|
33
|
-
| Action
|
|
34
|
-
| HTTP Action | Via runQuery/runMutation | Yes
|
|
39
|
+
| Type | Database Access | External APIs | Caching | Use Case |
|
|
40
|
+
| ----------- | ------------------------ | ------------- | ------------- | --------------------- |
|
|
41
|
+
| Query | Read-only | No | Yes, reactive | Fetching data |
|
|
42
|
+
| Mutation | Read/Write | No | No | Modifying data |
|
|
43
|
+
| Action | Via runQuery/runMutation | Yes | No | External integrations |
|
|
44
|
+
| HTTP Action | Via runQuery/runMutation | Yes | No | Webhooks, APIs |
|
|
35
45
|
|
|
36
46
|
### Queries
|
|
37
47
|
|
|
@@ -50,22 +60,24 @@ export const getUser = query({
|
|
|
50
60
|
name: v.string(),
|
|
51
61
|
email: v.string(),
|
|
52
62
|
}),
|
|
53
|
-
v.null()
|
|
63
|
+
v.null(),
|
|
54
64
|
),
|
|
55
65
|
handler: async (ctx, args) => {
|
|
56
|
-
return await ctx.db.get(args.userId);
|
|
66
|
+
return await ctx.db.get("users", args.userId);
|
|
57
67
|
},
|
|
58
68
|
});
|
|
59
69
|
|
|
60
70
|
// Query with index
|
|
61
71
|
export const listUserTasks = query({
|
|
62
72
|
args: { userId: v.id("users") },
|
|
63
|
-
returns: v.array(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
returns: v.array(
|
|
74
|
+
v.object({
|
|
75
|
+
_id: v.id("tasks"),
|
|
76
|
+
_creationTime: v.number(),
|
|
77
|
+
title: v.string(),
|
|
78
|
+
completed: v.boolean(),
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
69
81
|
handler: async (ctx, args) => {
|
|
70
82
|
return await ctx.db
|
|
71
83
|
.query("tasks")
|
|
@@ -93,7 +105,7 @@ export const createTask = mutation({
|
|
|
93
105
|
returns: v.id("tasks"),
|
|
94
106
|
handler: async (ctx, args) => {
|
|
95
107
|
// Validate user exists
|
|
96
|
-
const user = await ctx.db.get(args.userId);
|
|
108
|
+
const user = await ctx.db.get("users", args.userId);
|
|
97
109
|
if (!user) {
|
|
98
110
|
throw new ConvexError("User not found");
|
|
99
111
|
}
|
|
@@ -111,7 +123,7 @@ export const deleteTask = mutation({
|
|
|
111
123
|
args: { taskId: v.id("tasks") },
|
|
112
124
|
returns: v.null(),
|
|
113
125
|
handler: async (ctx, args) => {
|
|
114
|
-
await ctx.db.delete(args.taskId);
|
|
126
|
+
await ctx.db.delete("tasks", args.taskId);
|
|
115
127
|
return null;
|
|
116
128
|
},
|
|
117
129
|
});
|
|
@@ -154,7 +166,7 @@ export const processOrder = action({
|
|
|
154
166
|
handler: async (ctx, args) => {
|
|
155
167
|
// Read data via query
|
|
156
168
|
const order = await ctx.runQuery(api.orders.get, { orderId: args.orderId });
|
|
157
|
-
|
|
169
|
+
|
|
158
170
|
if (!order) {
|
|
159
171
|
throw new Error("Order not found");
|
|
160
172
|
}
|
|
@@ -218,8 +230,8 @@ http.route({
|
|
|
218
230
|
const url = new URL(request.url);
|
|
219
231
|
const userId = url.pathname.split("/").pop();
|
|
220
232
|
|
|
221
|
-
const user = await ctx.runQuery(api.users.get, {
|
|
222
|
-
userId: userId as Id<"users"
|
|
233
|
+
const user = await ctx.runQuery(api.users.get, {
|
|
234
|
+
userId: userId as Id<"users">,
|
|
223
235
|
});
|
|
224
236
|
|
|
225
237
|
if (!user) {
|
|
@@ -238,7 +250,11 @@ export default http;
|
|
|
238
250
|
Use internal functions for sensitive operations:
|
|
239
251
|
|
|
240
252
|
```typescript
|
|
241
|
-
import {
|
|
253
|
+
import {
|
|
254
|
+
internalMutation,
|
|
255
|
+
internalQuery,
|
|
256
|
+
internalAction,
|
|
257
|
+
} from "./_generated/server";
|
|
242
258
|
import { v } from "convex/values";
|
|
243
259
|
|
|
244
260
|
// Only callable from other Convex functions
|
|
@@ -249,10 +265,10 @@ export const _updateUserCredits = internalMutation({
|
|
|
249
265
|
},
|
|
250
266
|
returns: v.null(),
|
|
251
267
|
handler: async (ctx, args) => {
|
|
252
|
-
const user = await ctx.db.get(args.userId);
|
|
268
|
+
const user = await ctx.db.get("users", args.userId);
|
|
253
269
|
if (!user) return null;
|
|
254
270
|
|
|
255
|
-
await ctx.db.patch(args.userId, {
|
|
271
|
+
await ctx.db.patch("users", args.userId, {
|
|
256
272
|
credits: (user.credits || 0) + args.amount,
|
|
257
273
|
});
|
|
258
274
|
return null;
|
|
@@ -298,7 +314,7 @@ export const scheduleReminder = mutation({
|
|
|
298
314
|
return await ctx.scheduler.runAfter(
|
|
299
315
|
args.delayMs,
|
|
300
316
|
internal.notifications.sendReminder,
|
|
301
|
-
{ userId: args.userId, message: args.message }
|
|
317
|
+
{ userId: args.userId, message: args.message },
|
|
302
318
|
);
|
|
303
319
|
},
|
|
304
320
|
});
|
package/templates/CLAUDE.md
CHANGED
|
@@ -59,7 +59,11 @@ convex/
|
|
|
59
59
|
// QUERY: Read-only, reactive, cached. Use for fetching data.
|
|
60
60
|
export const list = query({
|
|
61
61
|
args: { limit: v.optional(v.number()) },
|
|
62
|
-
returns: v.array(
|
|
62
|
+
returns: v.array(
|
|
63
|
+
v.object({
|
|
64
|
+
/* ... */
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
63
67
|
handler: async (ctx, args) => {
|
|
64
68
|
return await ctx.db.query("items").take(args.limit ?? 10);
|
|
65
69
|
},
|
|
@@ -79,8 +83,12 @@ export const processExternal = action({
|
|
|
79
83
|
args: { id: v.id("items") },
|
|
80
84
|
handler: async (ctx, args) => {
|
|
81
85
|
const data = await ctx.runQuery(internal.items.get, { id: args.id });
|
|
82
|
-
const result = await fetch("https://api.example.com", {
|
|
83
|
-
|
|
86
|
+
const result = await fetch("https://api.example.com", {
|
|
87
|
+
/* ... */
|
|
88
|
+
});
|
|
89
|
+
await ctx.runMutation(internal.items.saveResult, {
|
|
90
|
+
/* ... */
|
|
91
|
+
});
|
|
84
92
|
},
|
|
85
93
|
});
|
|
86
94
|
```
|
|
@@ -151,7 +159,9 @@ const posts = await ctx.db
|
|
|
151
159
|
|
|
152
160
|
```typescript
|
|
153
161
|
export const myMutation = mutation({
|
|
154
|
-
args: {
|
|
162
|
+
args: {
|
|
163
|
+
/* ... */
|
|
164
|
+
},
|
|
155
165
|
handler: async (ctx, args) => {
|
|
156
166
|
const identity = await ctx.auth.getUserIdentity();
|
|
157
167
|
if (!identity) throw new Error("Unauthenticated");
|
|
@@ -189,6 +199,17 @@ export const getUrl = query({
|
|
|
189
199
|
});
|
|
190
200
|
```
|
|
191
201
|
|
|
202
|
+
## Linting
|
|
203
|
+
|
|
204
|
+
This project uses @convex-dev/eslint-plugin for code quality. All Convex functions must:
|
|
205
|
+
|
|
206
|
+
1. **Use object syntax** with `handler` property (not bare functions)
|
|
207
|
+
2. **Include argument validators** - always have `args: {}` even when empty
|
|
208
|
+
3. **Use explicit table names** - `db.get("table", id)` not `db.get(id)`
|
|
209
|
+
4. **Respect runtime boundaries** - don't import "use node" files into Convex runtime
|
|
210
|
+
|
|
211
|
+
Run `npx eslint convex/` to check compliance.
|
|
212
|
+
|
|
192
213
|
## IMPORTANT Rules
|
|
193
214
|
|
|
194
215
|
1. **ALWAYS validate arguments** on public functions with `v` validators
|
|
@@ -239,24 +260,31 @@ HTTP webhook/API? → httpAction()
|
|
|
239
260
|
### Validator Cheat Sheet
|
|
240
261
|
|
|
241
262
|
```typescript
|
|
242
|
-
v.string()
|
|
243
|
-
v.number()
|
|
244
|
-
v.boolean()
|
|
245
|
-
v.id("table")
|
|
246
|
-
v.null()
|
|
247
|
-
v.optional(v.string())
|
|
248
|
-
v.nullable(v.string())
|
|
249
|
-
v.array(v.string())
|
|
250
|
-
v.object({ k: v.string() })
|
|
251
|
-
v.union(v.literal("a"), v.literal("b"))
|
|
252
|
-
v.record(v.string(), v.number())
|
|
263
|
+
v.string(); // string
|
|
264
|
+
v.number(); // number (float64)
|
|
265
|
+
v.boolean(); // boolean
|
|
266
|
+
v.id("table"); // Id<"table">
|
|
267
|
+
v.null(); // null
|
|
268
|
+
v.optional(v.string()); // string | undefined (field can be missing)
|
|
269
|
+
v.nullable(v.string()); // string | null (field must exist)
|
|
270
|
+
v.array(v.string()); // string[]
|
|
271
|
+
v.object({ k: v.string() }); // { k: string }
|
|
272
|
+
v.union(v.literal("a"), v.literal("b")); // "a" | "b"
|
|
273
|
+
v.record(v.string(), v.number()); // { [key: string]: number }
|
|
253
274
|
```
|
|
254
275
|
|
|
255
276
|
### Import Patterns
|
|
256
277
|
|
|
257
278
|
```typescript
|
|
258
279
|
// Function constructors
|
|
259
|
-
import {
|
|
280
|
+
import {
|
|
281
|
+
query,
|
|
282
|
+
mutation,
|
|
283
|
+
action,
|
|
284
|
+
internalQuery,
|
|
285
|
+
internalMutation,
|
|
286
|
+
internalAction,
|
|
287
|
+
} from "./_generated/server";
|
|
260
288
|
|
|
261
289
|
// API references
|
|
262
290
|
import { api, internal } from "./_generated/api";
|