create-react-native-airborne 0.0.1
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/README.md +24 -0
- package/package.json +21 -0
- package/src/index.mjs +103 -0
- package/template/.agents/skills/convex-best-practices/SKILL.md +333 -0
- package/template/.agents/skills/convex-file-storage/SKILL.md +466 -0
- package/template/.agents/skills/convex-security-audit/SKILL.md +538 -0
- package/template/.agents/skills/convex-security-check/SKILL.md +377 -0
- package/template/.github/workflows/ci.yml +130 -0
- package/template/.prettierignore +8 -0
- package/template/.prettierrc.json +6 -0
- package/template/AGENTS.md +156 -0
- package/template/Justfile +48 -0
- package/template/README.md +94 -0
- package/template/client/.env.example +3 -0
- package/template/client/.vscode/extensions.json +1 -0
- package/template/client/.vscode/settings.json +7 -0
- package/template/client/README.md +33 -0
- package/template/client/app/(app)/_layout.tsx +34 -0
- package/template/client/app/(app)/index.tsx +66 -0
- package/template/client/app/(app)/push.tsx +75 -0
- package/template/client/app/(app)/settings.tsx +36 -0
- package/template/client/app/(auth)/_layout.tsx +22 -0
- package/template/client/app/(auth)/sign-in.tsx +358 -0
- package/template/client/app/(auth)/sign-up.tsx +237 -0
- package/template/client/app/_layout.tsx +30 -0
- package/template/client/app/index.tsx +127 -0
- package/template/client/app.config.ts +30 -0
- package/template/client/assets/images/android-icon-background.png +0 -0
- package/template/client/assets/images/android-icon-foreground.png +0 -0
- package/template/client/assets/images/android-icon-monochrome.png +0 -0
- package/template/client/assets/images/favicon.png +0 -0
- package/template/client/assets/images/icon.png +0 -0
- package/template/client/assets/images/partial-react-logo.png +0 -0
- package/template/client/assets/images/react-logo.png +0 -0
- package/template/client/assets/images/react-logo@2x.png +0 -0
- package/template/client/assets/images/react-logo@3x.png +0 -0
- package/template/client/assets/images/splash-icon.png +0 -0
- package/template/client/eslint.config.js +10 -0
- package/template/client/global.css +2 -0
- package/template/client/metro.config.js +9 -0
- package/template/client/package.json +51 -0
- package/template/client/src/components/auth-shell.tsx +63 -0
- package/template/client/src/components/form-input.tsx +62 -0
- package/template/client/src/components/primary-button.tsx +37 -0
- package/template/client/src/components/screen.tsx +17 -0
- package/template/client/src/components/sign-out-button.tsx +32 -0
- package/template/client/src/hooks/use-theme-sync.ts +11 -0
- package/template/client/src/lib/convex.ts +6 -0
- package/template/client/src/lib/env-schema.ts +13 -0
- package/template/client/src/lib/env.test.ts +24 -0
- package/template/client/src/lib/env.ts +19 -0
- package/template/client/src/lib/notifications.ts +47 -0
- package/template/client/src/store/preferences-store.ts +42 -0
- package/template/client/src/types/theme.ts +1 -0
- package/template/client/tsconfig.json +18 -0
- package/template/client/uniwind-types.d.ts +10 -0
- package/template/client/vitest.config.ts +7 -0
- package/template/package.json +22 -0
- package/template/server/.env.example +8 -0
- package/template/server/README.md +31 -0
- package/template/server/convex/_generated/api.d.ts +55 -0
- package/template/server/convex/_generated/api.js +23 -0
- package/template/server/convex/_generated/dataModel.d.ts +60 -0
- package/template/server/convex/_generated/server.d.ts +143 -0
- package/template/server/convex/_generated/server.js +93 -0
- package/template/server/convex/auth.config.ts +11 -0
- package/template/server/convex/env.ts +18 -0
- package/template/server/convex/lib.ts +12 -0
- package/template/server/convex/push.ts +148 -0
- package/template/server/convex/schema.ts +22 -0
- package/template/server/convex/users.ts +54 -0
- package/template/server/convex.json +3 -0
- package/template/server/eslint.config.js +51 -0
- package/template/server/package.json +29 -0
- package/template/server/tests/convex.test.ts +52 -0
- package/template/server/tests/import-meta.d.ts +3 -0
- package/template/server/tsconfig.json +15 -0
- package/template/server/vitest.config.ts +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# create-react-native-airborne
|
|
2
|
+
|
|
3
|
+
Create projects from the React Native Airborne template.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun create react-native-airborne my-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Flags
|
|
12
|
+
|
|
13
|
+
- `--skip-install`: skip `bun install --workspaces`
|
|
14
|
+
- `--no-git`: skip `git init`
|
|
15
|
+
|
|
16
|
+
## Development
|
|
17
|
+
|
|
18
|
+
From repo root:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd tooling/create-react-native-airborne && bun run sync-template
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This refreshes `template/` from the current root starter files.
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-react-native-airborne",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Scaffold a react-native-airborne starter project",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-react-native-airborne": "./src/index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"sync-template": "bun ./scripts/sync-template.ts",
|
|
15
|
+
"prepare": "bun run sync-template",
|
|
16
|
+
"prepack": "bun run sync-template"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"bun-types": "^1.3.4"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readdir, readFile, writeFile, copyFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const flags = new Set(args.filter((arg) => arg.startsWith("--")));
|
|
9
|
+
const projectNameArg = args.find((arg) => !arg.startsWith("--"));
|
|
10
|
+
|
|
11
|
+
if (!projectNameArg) {
|
|
12
|
+
console.error("Usage: bun create react-native-airborne <project-name> [--skip-install] [--no-git]");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const projectName = projectNameArg.trim();
|
|
17
|
+
const projectSlug = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
18
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const templateDir = path.resolve(__dirname, "../template");
|
|
21
|
+
|
|
22
|
+
async function exists(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
await stat(filePath);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function copyDirectory(src, dest) {
|
|
32
|
+
await mkdir(dest, { recursive: true });
|
|
33
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
34
|
+
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const srcPath = path.join(src, entry.name);
|
|
37
|
+
const destPath = path.join(dest, entry.name);
|
|
38
|
+
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
await copyDirectory(srcPath, destPath);
|
|
41
|
+
} else {
|
|
42
|
+
await copyFile(srcPath, destPath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function replaceTokens(dir) {
|
|
48
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const filePath = path.join(dir, entry.name);
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
await replaceTokens(filePath);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!/\.(json|md|ts|tsx|js|css|yml|yaml|env|toml|txt)$/.test(entry.name)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const content = await readFile(filePath, "utf8");
|
|
61
|
+
const updated = content
|
|
62
|
+
.replaceAll("__APP_NAME__", projectName)
|
|
63
|
+
.replaceAll("__APP_SLUG__", projectSlug)
|
|
64
|
+
.replaceAll("__APP_BUNDLE_ID__", `com.${projectSlug.replace(/-/g, "")}.app`);
|
|
65
|
+
|
|
66
|
+
if (updated !== content) {
|
|
67
|
+
await writeFile(filePath, updated, "utf8");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (await exists(targetDir)) {
|
|
73
|
+
const contents = await readdir(targetDir);
|
|
74
|
+
if (contents.length > 0) {
|
|
75
|
+
console.error(`Target directory is not empty: ${targetDir}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await copyDirectory(templateDir, targetDir);
|
|
81
|
+
await replaceTokens(targetDir);
|
|
82
|
+
|
|
83
|
+
if (!flags.has("--skip-install")) {
|
|
84
|
+
const install = spawnSync("bun", ["install", "--workspaces"], {
|
|
85
|
+
cwd: targetDir,
|
|
86
|
+
stdio: "inherit",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (install.status !== 0) {
|
|
90
|
+
process.exit(install.status ?? 1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!flags.has("--no-git")) {
|
|
95
|
+
spawnSync("git", ["init"], { cwd: targetDir, stdio: "inherit" });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(`\nCreated ${projectName} at ${targetDir}`);
|
|
99
|
+
console.log("\nNext steps:");
|
|
100
|
+
console.log(` cd ${projectName}`);
|
|
101
|
+
console.log(" cp client/.env.example client/.env");
|
|
102
|
+
console.log(" cp server/.env.example server/.env");
|
|
103
|
+
console.log(" just dev");
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Convex Best Practices
|
|
3
|
+
description: Guidelines for building production-ready Convex apps covering function organization, query patterns, validation, TypeScript usage, error handling, and the Zen of Convex design philosophy
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
author: Convex
|
|
6
|
+
tags: [convex, best-practices, typescript, production, error-handling]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Convex Best Practices
|
|
10
|
+
|
|
11
|
+
Build production-ready Convex applications by following established patterns for function organization, query optimization, validation, TypeScript usage, and error handling.
|
|
12
|
+
|
|
13
|
+
## Documentation Sources
|
|
14
|
+
|
|
15
|
+
Before implementing, do not assume; fetch the latest documentation:
|
|
16
|
+
|
|
17
|
+
- Primary: https://docs.convex.dev/understanding/best-practices/
|
|
18
|
+
- Error Handling: https://docs.convex.dev/functions/error-handling
|
|
19
|
+
- Write Conflicts: https://docs.convex.dev/error#1
|
|
20
|
+
- For broader context: https://docs.convex.dev/llms.txt
|
|
21
|
+
|
|
22
|
+
## Instructions
|
|
23
|
+
|
|
24
|
+
### The Zen of Convex
|
|
25
|
+
|
|
26
|
+
1. **Convex manages the hard parts** - Let Convex handle caching, real-time sync, and consistency
|
|
27
|
+
2. **Functions are the API** - Design your functions as your application's interface
|
|
28
|
+
3. **Schema is truth** - Define your data model explicitly in schema.ts
|
|
29
|
+
4. **TypeScript everywhere** - Leverage end-to-end type safety
|
|
30
|
+
5. **Queries are reactive** - Think in terms of subscriptions, not requests
|
|
31
|
+
|
|
32
|
+
### Function Organization
|
|
33
|
+
|
|
34
|
+
Organize your Convex functions by domain:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// convex/users.ts - User-related functions
|
|
38
|
+
import { query, mutation } from "./_generated/server";
|
|
39
|
+
import { v } from "convex/values";
|
|
40
|
+
|
|
41
|
+
export const get = query({
|
|
42
|
+
args: { userId: v.id("users") },
|
|
43
|
+
returns: v.union(v.object({
|
|
44
|
+
_id: v.id("users"),
|
|
45
|
+
_creationTime: v.number(),
|
|
46
|
+
name: v.string(),
|
|
47
|
+
email: v.string(),
|
|
48
|
+
}), v.null()),
|
|
49
|
+
handler: async (ctx, args) => {
|
|
50
|
+
return await ctx.db.get(args.userId);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Argument and Return Validation
|
|
56
|
+
|
|
57
|
+
Always define validators for arguments AND return types:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
export const createTask = mutation({
|
|
61
|
+
args: {
|
|
62
|
+
title: v.string(),
|
|
63
|
+
description: v.optional(v.string()),
|
|
64
|
+
priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),
|
|
65
|
+
},
|
|
66
|
+
returns: v.id("tasks"),
|
|
67
|
+
handler: async (ctx, args) => {
|
|
68
|
+
return await ctx.db.insert("tasks", {
|
|
69
|
+
title: args.title,
|
|
70
|
+
description: args.description,
|
|
71
|
+
priority: args.priority,
|
|
72
|
+
completed: false,
|
|
73
|
+
createdAt: Date.now(),
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Query Patterns
|
|
80
|
+
|
|
81
|
+
Use indexes instead of filters for efficient queries:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// Schema with index
|
|
85
|
+
export default defineSchema({
|
|
86
|
+
tasks: defineTable({
|
|
87
|
+
userId: v.id("users"),
|
|
88
|
+
status: v.string(),
|
|
89
|
+
createdAt: v.number(),
|
|
90
|
+
})
|
|
91
|
+
.index("by_user", ["userId"])
|
|
92
|
+
.index("by_user_and_status", ["userId", "status"]),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Query using index
|
|
96
|
+
export const getTasksByUser = query({
|
|
97
|
+
args: { userId: v.id("users") },
|
|
98
|
+
returns: v.array(v.object({
|
|
99
|
+
_id: v.id("tasks"),
|
|
100
|
+
_creationTime: v.number(),
|
|
101
|
+
userId: v.id("users"),
|
|
102
|
+
status: v.string(),
|
|
103
|
+
createdAt: v.number(),
|
|
104
|
+
})),
|
|
105
|
+
handler: async (ctx, args) => {
|
|
106
|
+
return await ctx.db
|
|
107
|
+
.query("tasks")
|
|
108
|
+
.withIndex("by_user", (q) => q.eq("userId", args.userId))
|
|
109
|
+
.order("desc")
|
|
110
|
+
.collect();
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Error Handling
|
|
116
|
+
|
|
117
|
+
Use ConvexError for user-facing errors:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { ConvexError } from "convex/values";
|
|
121
|
+
|
|
122
|
+
export const updateTask = mutation({
|
|
123
|
+
args: {
|
|
124
|
+
taskId: v.id("tasks"),
|
|
125
|
+
title: v.string(),
|
|
126
|
+
},
|
|
127
|
+
returns: v.null(),
|
|
128
|
+
handler: async (ctx, args) => {
|
|
129
|
+
const task = await ctx.db.get(args.taskId);
|
|
130
|
+
|
|
131
|
+
if (!task) {
|
|
132
|
+
throw new ConvexError({
|
|
133
|
+
code: "NOT_FOUND",
|
|
134
|
+
message: "Task not found",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await ctx.db.patch(args.taskId, { title: args.title });
|
|
139
|
+
return null;
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Avoiding Write Conflicts (Optimistic Concurrency Control)
|
|
145
|
+
|
|
146
|
+
Convex uses OCC. Follow these patterns to minimize conflicts:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// GOOD: Make mutations idempotent
|
|
150
|
+
export const completeTask = mutation({
|
|
151
|
+
args: { taskId: v.id("tasks") },
|
|
152
|
+
returns: v.null(),
|
|
153
|
+
handler: async (ctx, args) => {
|
|
154
|
+
const task = await ctx.db.get(args.taskId);
|
|
155
|
+
|
|
156
|
+
// Early return if already complete (idempotent)
|
|
157
|
+
if (!task || task.status === "completed") {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await ctx.db.patch(args.taskId, {
|
|
162
|
+
status: "completed",
|
|
163
|
+
completedAt: Date.now(),
|
|
164
|
+
});
|
|
165
|
+
return null;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// GOOD: Patch directly without reading first when possible
|
|
170
|
+
export const updateNote = mutation({
|
|
171
|
+
args: { id: v.id("notes"), content: v.string() },
|
|
172
|
+
returns: v.null(),
|
|
173
|
+
handler: async (ctx, args) => {
|
|
174
|
+
// Patch directly - ctx.db.patch throws if document doesn't exist
|
|
175
|
+
await ctx.db.patch(args.id, { content: args.content });
|
|
176
|
+
return null;
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// GOOD: Use Promise.all for parallel independent updates
|
|
181
|
+
export const reorderItems = mutation({
|
|
182
|
+
args: { itemIds: v.array(v.id("items")) },
|
|
183
|
+
returns: v.null(),
|
|
184
|
+
handler: async (ctx, args) => {
|
|
185
|
+
const updates = args.itemIds.map((id, index) =>
|
|
186
|
+
ctx.db.patch(id, { order: index })
|
|
187
|
+
);
|
|
188
|
+
await Promise.all(updates);
|
|
189
|
+
return null;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### TypeScript Best Practices
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { Id, Doc } from "./_generated/dataModel";
|
|
198
|
+
|
|
199
|
+
// Use Id type for document references
|
|
200
|
+
type UserId = Id<"users">;
|
|
201
|
+
|
|
202
|
+
// Use Doc type for full documents
|
|
203
|
+
type User = Doc<"users">;
|
|
204
|
+
|
|
205
|
+
// Define Record types properly
|
|
206
|
+
const userScores: Record<Id<"users">, number> = {};
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Internal vs Public Functions
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
// Public function - exposed to clients
|
|
213
|
+
export const getUser = query({
|
|
214
|
+
args: { userId: v.id("users") },
|
|
215
|
+
returns: v.union(v.null(), v.object({ /* ... */ })),
|
|
216
|
+
handler: async (ctx, args) => {
|
|
217
|
+
// ...
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Internal function - only callable from other Convex functions
|
|
222
|
+
export const _updateUserStats = internalMutation({
|
|
223
|
+
args: { userId: v.id("users") },
|
|
224
|
+
returns: v.null(),
|
|
225
|
+
handler: async (ctx, args) => {
|
|
226
|
+
// ...
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Examples
|
|
232
|
+
|
|
233
|
+
### Complete CRUD Pattern
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// convex/tasks.ts
|
|
237
|
+
import { query, mutation } from "./_generated/server";
|
|
238
|
+
import { v } from "convex/values";
|
|
239
|
+
import { ConvexError } from "convex/values";
|
|
240
|
+
|
|
241
|
+
const taskValidator = v.object({
|
|
242
|
+
_id: v.id("tasks"),
|
|
243
|
+
_creationTime: v.number(),
|
|
244
|
+
title: v.string(),
|
|
245
|
+
completed: v.boolean(),
|
|
246
|
+
userId: v.id("users"),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
export const list = query({
|
|
250
|
+
args: { userId: v.id("users") },
|
|
251
|
+
returns: v.array(taskValidator),
|
|
252
|
+
handler: async (ctx, args) => {
|
|
253
|
+
return await ctx.db
|
|
254
|
+
.query("tasks")
|
|
255
|
+
.withIndex("by_user", (q) => q.eq("userId", args.userId))
|
|
256
|
+
.collect();
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
export const create = mutation({
|
|
261
|
+
args: {
|
|
262
|
+
title: v.string(),
|
|
263
|
+
userId: v.id("users"),
|
|
264
|
+
},
|
|
265
|
+
returns: v.id("tasks"),
|
|
266
|
+
handler: async (ctx, args) => {
|
|
267
|
+
return await ctx.db.insert("tasks", {
|
|
268
|
+
title: args.title,
|
|
269
|
+
completed: false,
|
|
270
|
+
userId: args.userId,
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
export const update = mutation({
|
|
276
|
+
args: {
|
|
277
|
+
taskId: v.id("tasks"),
|
|
278
|
+
title: v.optional(v.string()),
|
|
279
|
+
completed: v.optional(v.boolean()),
|
|
280
|
+
},
|
|
281
|
+
returns: v.null(),
|
|
282
|
+
handler: async (ctx, args) => {
|
|
283
|
+
const { taskId, ...updates } = args;
|
|
284
|
+
|
|
285
|
+
// Remove undefined values
|
|
286
|
+
const cleanUpdates = Object.fromEntries(
|
|
287
|
+
Object.entries(updates).filter(([_, v]) => v !== undefined)
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
if (Object.keys(cleanUpdates).length > 0) {
|
|
291
|
+
await ctx.db.patch(taskId, cleanUpdates);
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
export const remove = mutation({
|
|
298
|
+
args: { taskId: v.id("tasks") },
|
|
299
|
+
returns: v.null(),
|
|
300
|
+
handler: async (ctx, args) => {
|
|
301
|
+
await ctx.db.delete(args.taskId);
|
|
302
|
+
return null;
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Best Practices
|
|
308
|
+
|
|
309
|
+
- Never run `npx convex deploy` unless explicitly instructed
|
|
310
|
+
- Never run any git commands unless explicitly instructed
|
|
311
|
+
- Always define return validators for functions
|
|
312
|
+
- Use indexes for all queries that filter data
|
|
313
|
+
- Make mutations idempotent to handle retries gracefully
|
|
314
|
+
- Use ConvexError for user-facing error messages
|
|
315
|
+
- Organize functions by domain (users.ts, tasks.ts, etc.)
|
|
316
|
+
- Use internal functions for sensitive operations
|
|
317
|
+
- Leverage TypeScript's Id and Doc types
|
|
318
|
+
|
|
319
|
+
## Common Pitfalls
|
|
320
|
+
|
|
321
|
+
1. **Using filter instead of withIndex** - Always define indexes and use withIndex
|
|
322
|
+
2. **Missing return validators** - Always specify the returns field
|
|
323
|
+
3. **Non-idempotent mutations** - Check current state before updating
|
|
324
|
+
4. **Reading before patching unnecessarily** - Patch directly when possible
|
|
325
|
+
5. **Not handling null returns** - Document IDs might not exist
|
|
326
|
+
|
|
327
|
+
## References
|
|
328
|
+
|
|
329
|
+
- Convex Documentation: https://docs.convex.dev/
|
|
330
|
+
- Convex LLMs.txt: https://docs.convex.dev/llms.txt
|
|
331
|
+
- Best Practices: https://docs.convex.dev/understanding/best-practices/
|
|
332
|
+
- Error Handling: https://docs.convex.dev/functions/error-handling
|
|
333
|
+
- Write Conflicts: https://docs.convex.dev/error#1
|