kitcn 0.0.1 → 0.12.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/bin/intent.js +3 -0
- package/dist/aggregate/index.d.ts +388 -0
- package/dist/aggregate/index.js +37 -0
- package/dist/api-entry-BckXqaLb.js +66 -0
- package/dist/auth/client/index.d.ts +37 -0
- package/dist/auth/client/index.js +217 -0
- package/dist/auth/config/index.d.ts +45 -0
- package/dist/auth/config/index.js +24 -0
- package/dist/auth/generated/index.d.ts +2 -0
- package/dist/auth/generated/index.js +3 -0
- package/dist/auth/http/index.d.ts +64 -0
- package/dist/auth/http/index.js +461 -0
- package/dist/auth/index.d.ts +221 -0
- package/dist/auth/index.js +1398 -0
- package/dist/auth/nextjs/index.d.ts +50 -0
- package/dist/auth/nextjs/index.js +81 -0
- package/dist/auth-store-Cljlmdmi.js +197 -0
- package/dist/builder-CBdG5W6A.js +1974 -0
- package/dist/caller-factory-cTXNvYdz.js +216 -0
- package/dist/cli.mjs +13264 -0
- package/dist/codegen-lF80HSWu.mjs +3416 -0
- package/dist/context-utils-HPC5nXzx.d.ts +17 -0
- package/dist/create-schema-odyF4kCy.js +156 -0
- package/dist/create-schema-orm-DOyiNDCx.js +246 -0
- package/dist/crpc/index.d.ts +105 -0
- package/dist/crpc/index.js +169 -0
- package/dist/customFunctions-C0voKmtx.js +144 -0
- package/dist/error-BZEnI7Sq.js +41 -0
- package/dist/generated-contract-disabled-Cih4eITO.js +50 -0
- package/dist/generated-contract-disabled-D-sOFy92.d.ts +354 -0
- package/dist/http-types-DqJubRPJ.d.ts +292 -0
- package/dist/meta-utils-0Pu0Nrap.js +117 -0
- package/dist/middleware-BUybuv9n.d.ts +34 -0
- package/dist/middleware-C2qTZ3V7.js +84 -0
- package/dist/orm/index.d.ts +17 -0
- package/dist/orm/index.js +10713 -0
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +3 -0
- package/dist/procedure-caller-DtxLmGwA.d.ts +1467 -0
- package/dist/procedure-caller-MWcxhQDv.js +349 -0
- package/dist/query-context-B8o6-8kC.js +1518 -0
- package/dist/query-context-CFZqIvD7.d.ts +42 -0
- package/dist/query-options-Dw7cOyXl.js +121 -0
- package/dist/ratelimit/index.d.ts +269 -0
- package/dist/ratelimit/index.js +856 -0
- package/dist/ratelimit/react/index.d.ts +76 -0
- package/dist/ratelimit/react/index.js +183 -0
- package/dist/react/index.d.ts +1284 -0
- package/dist/react/index.js +2526 -0
- package/dist/rsc/index.d.ts +276 -0
- package/dist/rsc/index.js +233 -0
- package/dist/runtime-CtvJPkur.js +2453 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +6 -0
- package/dist/solid/index.d.ts +1221 -0
- package/dist/solid/index.js +2940 -0
- package/dist/transformer-DtDhR3Lc.js +194 -0
- package/dist/types-BTb_4BaU.d.ts +42 -0
- package/dist/types-BiJE7qxR.d.ts +4 -0
- package/dist/types-DEJpkIhw.d.ts +88 -0
- package/dist/types-HhO_R6pd.d.ts +213 -0
- package/dist/validators-B7oIJCAp.js +279 -0
- package/dist/validators-vzRKjBJC.d.ts +88 -0
- package/dist/watcher.mjs +96 -0
- package/dist/where-clause-compiler-DdjN63Io.d.ts +4756 -0
- package/package.json +107 -34
- package/skills/convex/SKILL.md +486 -0
- package/skills/convex/references/features/aggregates.md +353 -0
- package/skills/convex/references/features/auth-admin.md +446 -0
- package/skills/convex/references/features/auth-organizations.md +1141 -0
- package/skills/convex/references/features/auth-polar.md +579 -0
- package/skills/convex/references/features/auth.md +470 -0
- package/skills/convex/references/features/create-plugins.md +153 -0
- package/skills/convex/references/features/http.md +676 -0
- package/skills/convex/references/features/migrations.md +162 -0
- package/skills/convex/references/features/orm.md +1166 -0
- package/skills/convex/references/features/react.md +657 -0
- package/skills/convex/references/features/scheduling.md +267 -0
- package/skills/convex/references/features/testing.md +209 -0
- package/skills/convex/references/setup/auth.md +501 -0
- package/skills/convex/references/setup/biome.md +190 -0
- package/skills/convex/references/setup/doc-guidelines.md +145 -0
- package/skills/convex/references/setup/index.md +761 -0
- package/skills/convex/references/setup/next.md +116 -0
- package/skills/convex/references/setup/react.md +175 -0
- package/skills/convex/references/setup/server.md +473 -0
- package/skills/convex/references/setup/start.md +67 -0
- package/LICENSE +0 -21
- package/README.md +0 -0
- package/dist/index.d.mts +0 -5
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -6
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Scheduling
|
|
2
|
+
|
|
3
|
+
> Prerequisites: `setup/server.md`
|
|
4
|
+
|
|
5
|
+
Cron jobs and scheduled functions for background processing in Convex. Basics → SKILL.md Section 10. This file adds cron expressions, handler templates, job status API, error handling detail.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
| Type | Use For |
|
|
10
|
+
|------|---------|
|
|
11
|
+
| Cron jobs | Recurring tasks on a fixed schedule |
|
|
12
|
+
| Scheduled functions | One-time delayed execution |
|
|
13
|
+
|
|
14
|
+
View scheduled jobs in [Dashboard](https://dashboard.convex.dev) → **Schedules** tab.
|
|
15
|
+
|
|
16
|
+
### When to Use
|
|
17
|
+
|
|
18
|
+
| Scenario | Cron Jobs | Scheduled Functions |
|
|
19
|
+
|----------|-----------|---------------------|
|
|
20
|
+
| Daily cleanup | Fixed schedule | |
|
|
21
|
+
| Send email after signup | | `caller.schedule.now.*` |
|
|
22
|
+
| Subscription expiration | | `caller.schedule.at(timestamp).*` |
|
|
23
|
+
| Hourly analytics | Fixed schedule | |
|
|
24
|
+
| Reminder notifications | | User-defined time |
|
|
25
|
+
| Order processing delay | | `caller.schedule.after(5000).*` |
|
|
26
|
+
|
|
27
|
+
**Tip:** Use `caller.schedule.now.*` to trigger work immediately after a mutation commits.
|
|
28
|
+
|
|
29
|
+
## Cron Jobs
|
|
30
|
+
|
|
31
|
+
### Setup
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// convex/functions/crons.ts
|
|
35
|
+
import { cronJobs } from 'convex/server';
|
|
36
|
+
import { internal } from './_generated/api';
|
|
37
|
+
|
|
38
|
+
const crons = cronJobs();
|
|
39
|
+
|
|
40
|
+
// Run every 2 hours
|
|
41
|
+
crons.interval('cleanup stale data', { hours: 2 }, internal.crons.cleanupStaleData, {});
|
|
42
|
+
|
|
43
|
+
// Run at specific times using cron syntax
|
|
44
|
+
crons.cron('daily report', '0 9 * * *', internal.crons.generateDailyReport, {});
|
|
45
|
+
|
|
46
|
+
export default crons;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Note:** Always import `internal` from `./_generated/api`, even for functions in the same file.
|
|
50
|
+
|
|
51
|
+
### Cron Expressions
|
|
52
|
+
|
|
53
|
+
| Pattern | Description |
|
|
54
|
+
|---------|-------------|
|
|
55
|
+
| `* * * * *` | Every minute |
|
|
56
|
+
| `*/15 * * * *` | Every 15 minutes |
|
|
57
|
+
| `0 * * * *` | Every hour |
|
|
58
|
+
| `0 0 * * *` | Daily at midnight |
|
|
59
|
+
| `0 9 * * *` | Daily at 9 AM |
|
|
60
|
+
| `0 9 * * 1-5` | Weekdays at 9 AM |
|
|
61
|
+
| `0 0 1 * *` | First day of month |
|
|
62
|
+
|
|
63
|
+
Format: `minute hour day-of-month month day-of-week`. Runs in **UTC**. Minimum interval is 1 minute.
|
|
64
|
+
|
|
65
|
+
### Handler Implementation
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// convex/functions/crons.ts
|
|
69
|
+
import { z } from 'zod';
|
|
70
|
+
import { privateMutation, privateAction } from '../lib/crpc';
|
|
71
|
+
import { createAnalyticsCaller } from './generated/analytics.runtime';
|
|
72
|
+
import { createReportsCaller } from './generated/reports.runtime';
|
|
73
|
+
|
|
74
|
+
export const cleanupStaleData = privateMutation
|
|
75
|
+
.input(z.object({}))
|
|
76
|
+
.output(z.object({ deletedCount: z.number() }))
|
|
77
|
+
.mutation(async ({ ctx }) => {
|
|
78
|
+
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
79
|
+
const staleSessions = await ctx.orm.query.session.findMany({
|
|
80
|
+
where: { lastActiveAt: { lt: thirtyDaysAgo } },
|
|
81
|
+
limit: 1000,
|
|
82
|
+
});
|
|
83
|
+
for (const sessionRow of staleSessions) {
|
|
84
|
+
await ctx.orm.delete(session).where(eq(session.id, sessionRow.id));
|
|
85
|
+
}
|
|
86
|
+
return { deletedCount: staleSessions.length };
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export const generateDailyReport = privateAction
|
|
90
|
+
.input(z.object({}))
|
|
91
|
+
|
|
92
|
+
.action(async ({ ctx }) => {
|
|
93
|
+
const analyticsCaller = createAnalyticsCaller(ctx);
|
|
94
|
+
const reportsCaller = createReportsCaller(ctx);
|
|
95
|
+
const stats = await analyticsCaller.actions.getDailyStats({});
|
|
96
|
+
await reportsCaller.schedule.now.create({ type: 'daily', data: stats });
|
|
97
|
+
return null;
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Scheduled Functions
|
|
102
|
+
|
|
103
|
+
### Key Concepts
|
|
104
|
+
|
|
105
|
+
| Concept | Description |
|
|
106
|
+
|---------|-------------|
|
|
107
|
+
| Atomicity | Scheduling from mutations is atomic — if mutation fails, nothing is scheduled |
|
|
108
|
+
| Non-atomic in actions | Scheduled functions from actions run even if the action fails |
|
|
109
|
+
| Limits | Single function can schedule up to 1000 functions with 8MB total argument size |
|
|
110
|
+
| Auth not propagated | Pass user info as arguments if needed |
|
|
111
|
+
| Results retention | Available for 7 days after completion |
|
|
112
|
+
|
|
113
|
+
**Warning:** Auth context is NOT available in scheduled functions. Pass `userId` or other auth data as arguments.
|
|
114
|
+
|
|
115
|
+
Use `caller.schedule.*` when scheduling cRPC procedures. Use `ctx.scheduler.*` only when you must schedule a raw `internal.*` Convex function.
|
|
116
|
+
|
|
117
|
+
### caller.schedule.after
|
|
118
|
+
|
|
119
|
+
Schedule after a delay (milliseconds):
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
export const processOrder = authMutation
|
|
123
|
+
.input(z.object({ orderId: z.string() }))
|
|
124
|
+
|
|
125
|
+
.mutation(async ({ ctx, input }) => {
|
|
126
|
+
const caller = createOrdersCaller(ctx);
|
|
127
|
+
await ctx.orm.update(orders).set({ status: 'processing' }).where(eq(orders.id, input.orderId));
|
|
128
|
+
|
|
129
|
+
// Run after 5 seconds
|
|
130
|
+
await caller.schedule.after(5000).charge({ orderId: input.orderId });
|
|
131
|
+
return null;
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Immediate Execution
|
|
136
|
+
|
|
137
|
+
`caller.schedule.now.*` triggers work immediately after mutation commits:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
export const createItem = authMutation
|
|
141
|
+
.input(z.object({ name: z.string() }))
|
|
142
|
+
.output(z.string())
|
|
143
|
+
.mutation(async ({ ctx, input }) => {
|
|
144
|
+
const caller = createItemsCaller(ctx);
|
|
145
|
+
const [row] = await ctx.orm.insert(items).values({ name: input.name }).returning({ id: items.id });
|
|
146
|
+
|
|
147
|
+
// Action runs immediately after mutation commits
|
|
148
|
+
await caller.schedule.now.sendNotification({ itemId: row.id });
|
|
149
|
+
return row.id;
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### caller.schedule.at
|
|
154
|
+
|
|
155
|
+
Schedule at a specific Unix timestamp (ms):
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
export const scheduleReminder = authMutation
|
|
159
|
+
.input(z.object({ message: z.string(), sendAt: z.number() }))
|
|
160
|
+
|
|
161
|
+
.mutation(async ({ ctx, input }) => {
|
|
162
|
+
const caller = createRemindersCaller(ctx);
|
|
163
|
+
if (input.sendAt <= Date.now()) {
|
|
164
|
+
throw new CRPCError({ code: 'BAD_REQUEST', message: 'Reminder time must be in the future' });
|
|
165
|
+
}
|
|
166
|
+
await caller.schedule.at(input.sendAt).send({ message: input.message });
|
|
167
|
+
return null;
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Canceling Scheduled Functions
|
|
172
|
+
|
|
173
|
+
Store the job ID to cancel later:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
export const createSubscription = authMutation
|
|
177
|
+
.input(z.object({ planId: z.string() }))
|
|
178
|
+
.output(z.string())
|
|
179
|
+
.mutation(async ({ ctx, input }) => {
|
|
180
|
+
const caller = createSubscriptionsCaller(ctx);
|
|
181
|
+
// Schedule expiration in 30 days
|
|
182
|
+
const expirationJobId = await caller.schedule.after(30 * 24 * 60 * 60 * 1000).expire({ userId: ctx.userId });
|
|
183
|
+
|
|
184
|
+
const [row] = await ctx.orm
|
|
185
|
+
.insert(subscriptions)
|
|
186
|
+
.values({ userId: ctx.userId, planId: input.planId, expirationJobId })
|
|
187
|
+
.returning({ id: subscriptions.id });
|
|
188
|
+
return row.id;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
export const cancelSubscription = authMutation
|
|
192
|
+
.input(z.object({ subscriptionId: z.string() }))
|
|
193
|
+
|
|
194
|
+
.mutation(async ({ ctx, input }) => {
|
|
195
|
+
const caller = createSubscriptionsCaller(ctx);
|
|
196
|
+
const subscription = await ctx.orm.query.subscriptions.findFirst({ where: { id: input.subscriptionId } });
|
|
197
|
+
if (!subscription) throw new CRPCError({ code: 'NOT_FOUND', message: 'Subscription not found' });
|
|
198
|
+
|
|
199
|
+
if (subscription.expirationJobId) {
|
|
200
|
+
await caller.schedule.cancel(subscription.expirationJobId);
|
|
201
|
+
}
|
|
202
|
+
await ctx.orm.delete(subscriptions).where(eq(subscriptions.id, subscription.id));
|
|
203
|
+
return null;
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Checking Status
|
|
208
|
+
|
|
209
|
+
Query `_scheduled_functions` system table:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
export const getJobStatus = publicQuery
|
|
213
|
+
.input(z.object({ jobId: z.string() }))
|
|
214
|
+
.output(z.object({
|
|
215
|
+
name: z.string(),
|
|
216
|
+
scheduledTime: z.number(),
|
|
217
|
+
completedTime: z.number().optional(),
|
|
218
|
+
state: z.object({ kind: z.enum(['pending', 'inProgress', 'success', 'failed', 'canceled']) }),
|
|
219
|
+
}).nullable())
|
|
220
|
+
.query(async ({ ctx, input }) => {
|
|
221
|
+
return await ctx.orm.system.get(input.jobId);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
export const listPendingJobs = publicQuery
|
|
225
|
+
.input(z.object({}))
|
|
226
|
+
.output(z.array(z.object({ id: z.string(), name: z.string(), scheduledTime: z.number() })))
|
|
227
|
+
.query(async ({ ctx }) => {
|
|
228
|
+
const jobs = await ctx.orm.system
|
|
229
|
+
.query('_scheduled_functions')
|
|
230
|
+
.filter((q) => q.eq(q.field('state.kind'), 'pending'))
|
|
231
|
+
.collect();
|
|
232
|
+
return jobs.map(({ id, name, scheduledTime }) => ({ id, name, scheduledTime }));
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Job States
|
|
237
|
+
|
|
238
|
+
| State | Description |
|
|
239
|
+
|-------|-------------|
|
|
240
|
+
| `pending` | Not started yet |
|
|
241
|
+
| `inProgress` | Currently running (actions only) |
|
|
242
|
+
| `success` | Completed successfully |
|
|
243
|
+
| `failed` | Hit an error |
|
|
244
|
+
| `canceled` | Canceled via dashboard or `caller.schedule.cancel()` |
|
|
245
|
+
|
|
246
|
+
## Error Handling
|
|
247
|
+
|
|
248
|
+
### Mutations
|
|
249
|
+
|
|
250
|
+
- **Automatic retry** for internal Convex errors
|
|
251
|
+
- **Guaranteed execution** — once scheduled, executes exactly once
|
|
252
|
+
- **Permanent failure** only on developer errors
|
|
253
|
+
|
|
254
|
+
### Actions
|
|
255
|
+
|
|
256
|
+
- **No automatic retry** — actions may have side effects
|
|
257
|
+
- **At most once** execution
|
|
258
|
+
- For critical actions, implement manual retry with exponential backoff
|
|
259
|
+
|
|
260
|
+
## Best Practices
|
|
261
|
+
|
|
262
|
+
1. **Use internal procedures/functions** — prevent external access to scheduled work
|
|
263
|
+
2. **Store job IDs** — when you need to cancel scheduled functions
|
|
264
|
+
3. **Check conditions** — target may be deleted before execution
|
|
265
|
+
4. **Consider idempotency** — scheduled functions might run multiple times
|
|
266
|
+
5. **Pass auth info** — auth not propagated, pass user data as arguments
|
|
267
|
+
6. **Use `caller.schedule.now.*`** — trigger work after mutation commits
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Testing (Consumer App Focus)
|
|
2
|
+
|
|
3
|
+
Use this for testing your app features built on kitcn.
|
|
4
|
+
This intentionally excludes internal package parity harnesses and deep type-matrix maintenance.
|
|
5
|
+
|
|
6
|
+
What to test + practical checklist → SKILL.md Section 11.
|
|
7
|
+
|
|
8
|
+
## Minimal Runtime Harness
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { test, expect } from "vitest";
|
|
12
|
+
import schema from "../schema";
|
|
13
|
+
import { convexTest, runCtx } from "../setup.testing";
|
|
14
|
+
|
|
15
|
+
test("feature", async () => {
|
|
16
|
+
const t = convexTest(schema);
|
|
17
|
+
await t.run(async (baseCtx) => {
|
|
18
|
+
const ctx = await runCtx(baseCtx);
|
|
19
|
+
// test logic
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Core Runtime Scenarios
|
|
25
|
+
|
|
26
|
+
### 1) Happy-path mutation
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
test("creates project", async () => {
|
|
30
|
+
const t = convexTest(schema);
|
|
31
|
+
await t.run(async (baseCtx) => {
|
|
32
|
+
const ctx = await runCtx(baseCtx);
|
|
33
|
+
|
|
34
|
+
const id = await ctx.orm.insert(project).values({
|
|
35
|
+
name: "Launch",
|
|
36
|
+
ownerId: "user_123",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const row = await ctx.orm.query.project.findFirstOrThrow({ where: { id } });
|
|
40
|
+
expect(row.name).toBe("Launch");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2) Auth required
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
test("rejects unauthenticated call", async () => {
|
|
49
|
+
const t = convexTest(schema);
|
|
50
|
+
await t.run(async (baseCtx) => {
|
|
51
|
+
await expect(
|
|
52
|
+
baseCtx.runMutation(api.project.renameProject, {
|
|
53
|
+
id: "proj_1",
|
|
54
|
+
name: "Renamed",
|
|
55
|
+
})
|
|
56
|
+
).rejects.toThrow(/UNAUTHORIZED/);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 3) Ownership / forbidden
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
test("rejects non-owner update", async () => {
|
|
65
|
+
const t = convexTest(schema);
|
|
66
|
+
await t.run(async (baseCtx) => {
|
|
67
|
+
const ctx = await runCtx(baseCtx);
|
|
68
|
+
|
|
69
|
+
const id = await ctx.orm.insert(project).values({
|
|
70
|
+
name: "Secret",
|
|
71
|
+
ownerId: "owner_1",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await expect(
|
|
75
|
+
baseCtx.runMutation(api.project.renameProject, {
|
|
76
|
+
id,
|
|
77
|
+
name: "Hacked",
|
|
78
|
+
})
|
|
79
|
+
).rejects.toThrow(/FORBIDDEN/);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 4) Not-found path
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
test("returns not found for missing row", async () => {
|
|
88
|
+
const t = convexTest(schema);
|
|
89
|
+
await t.run(async (baseCtx) => {
|
|
90
|
+
await expect(
|
|
91
|
+
baseCtx.runMutation(api.project.renameProject, {
|
|
92
|
+
id: "missing",
|
|
93
|
+
name: "x",
|
|
94
|
+
})
|
|
95
|
+
).rejects.toThrow(/NOT_FOUND/);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 5) Trigger side effects
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
test("updating message updates thread timestamp via trigger", async () => {
|
|
104
|
+
const t = convexTest(schema);
|
|
105
|
+
await t.run(async (baseCtx) => {
|
|
106
|
+
const ctx = await runCtx(baseCtx);
|
|
107
|
+
|
|
108
|
+
const threadId = await ctx.orm.insert(thread).values({ title: "T1" });
|
|
109
|
+
const messageId = await ctx.orm.insert(message).values({
|
|
110
|
+
threadId,
|
|
111
|
+
body: "hello",
|
|
112
|
+
authorId: "user_1",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await ctx.orm
|
|
116
|
+
.update(message)
|
|
117
|
+
.set({ body: "hello again" })
|
|
118
|
+
.where(eq(message.id, messageId));
|
|
119
|
+
|
|
120
|
+
const threadRow = await ctx.orm.query.thread.findFirstOrThrow({
|
|
121
|
+
where: { id: threadId },
|
|
122
|
+
});
|
|
123
|
+
expect(threadRow.lastMessageAt).toBeTruthy();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 6) Scheduled jobs
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { vi } from "vitest";
|
|
132
|
+
|
|
133
|
+
test("scheduled cleanup runs", async () => {
|
|
134
|
+
vi.useFakeTimers();
|
|
135
|
+
const t = convexTest(schema);
|
|
136
|
+
|
|
137
|
+
await t.run(async (baseCtx) => {
|
|
138
|
+
const ctx = await runCtx(baseCtx);
|
|
139
|
+
|
|
140
|
+
const caller = createJobsCaller(ctx);
|
|
141
|
+
await caller.schedule.after(1000).cleanup({ orgId: "org_1" });
|
|
142
|
+
vi.advanceTimersByTime(1000);
|
|
143
|
+
await t.finishAllScheduledFunctions();
|
|
144
|
+
|
|
145
|
+
const remaining = await ctx.orm.query.tempRecords.findMany({
|
|
146
|
+
where: { orgId: "org_1" },
|
|
147
|
+
limit: 10,
|
|
148
|
+
});
|
|
149
|
+
expect(remaining.length).toBe(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
vi.useRealTimers();
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Lightweight Type Checks (Optional)
|
|
157
|
+
|
|
158
|
+
Only keep app-facing type checks:
|
|
159
|
+
- procedure input/output DTOs
|
|
160
|
+
- key ORM return shapes used by UI
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { expectTypeOf } from "vitest";
|
|
164
|
+
|
|
165
|
+
test("list query result shape", async () => {
|
|
166
|
+
const t = convexTest(schema);
|
|
167
|
+
await t.run(async (baseCtx) => {
|
|
168
|
+
const ctx = await runCtx(baseCtx);
|
|
169
|
+
const rows = await ctx.orm.query.project.findMany({ limit: 5 });
|
|
170
|
+
|
|
171
|
+
expectTypeOf(rows[0]).toMatchTypeOf<{
|
|
172
|
+
id: string;
|
|
173
|
+
name: string;
|
|
174
|
+
createdAt: number;
|
|
175
|
+
}>();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Compile-Time Type Suites (Example-Parity Optional)
|
|
181
|
+
|
|
182
|
+
If you want parity with `example/convex` type hardening, add compile-time-only files:
|
|
183
|
+
|
|
184
|
+
- `convex/lib/crpc-test.ts`:
|
|
185
|
+
- procedure-builder type coverage (`public`, `optionalAuth`, `auth`, `private`)
|
|
186
|
+
- `.paginated(...)` cursor/limit type checks
|
|
187
|
+
- `@ts-expect-error` assertions for invalid usage
|
|
188
|
+
- `convex/shared/types-typecheck.ts`:
|
|
189
|
+
- `Select`/`Insert` alias integrity checks
|
|
190
|
+
- generated API input/output shape checks
|
|
191
|
+
- temporal field type assertions
|
|
192
|
+
|
|
193
|
+
These files are validated by `tsc`/`bun typecheck`; they are not runtime tests.
|
|
194
|
+
|
|
195
|
+
## Keep / Drop Guidance
|
|
196
|
+
|
|
197
|
+
Keep:
|
|
198
|
+
- feature tests tied to user-visible behavior
|
|
199
|
+
- auth/rules tests
|
|
200
|
+
- trigger and scheduler tests
|
|
201
|
+
- API contract checks at app boundary
|
|
202
|
+
|
|
203
|
+
Drop:
|
|
204
|
+
- internal ORM parity progress tracking
|
|
205
|
+
- assertion counting workflows
|
|
206
|
+
- package-internal generic/type torture suites
|
|
207
|
+
- duplicate runtime snippets with same intent
|
|
208
|
+
|
|
209
|
+
→ Practical test checklist: SKILL.md Section 11 (items 1–7).
|