convex-verify 1.0.5 → 1.2.2
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 +271 -235
- package/dist/core/index.d.mts +14 -85
- package/dist/core/index.d.ts +14 -85
- package/dist/core/index.js +520 -83
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +516 -80
- package/dist/core/index.mjs.map +1 -1
- package/dist/index.d.mts +9 -6
- package/dist/index.d.ts +9 -6
- package/dist/index.js +386 -233
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +383 -226
- package/dist/index.mjs.map +1 -1
- package/dist/types-B8ZkLuJ2.d.mts +141 -0
- package/dist/types-B8ZkLuJ2.d.ts +141 -0
- package/dist/utils/index.d.mts +3 -2
- package/dist/utils/index.d.ts +3 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/utils/index.mjs.map +1 -1
- package/dist/verifyConfig-CTrtqMr_.d.ts +94 -0
- package/dist/verifyConfig-Kn3Ikj00.d.mts +94 -0
- package/package.json +5 -22
- package/dist/configs/index.d.mts +0 -51
- package/dist/configs/index.d.ts +0 -51
- package/dist/configs/index.js +0 -38
- package/dist/configs/index.js.map +0 -1
- package/dist/configs/index.mjs +0 -11
- package/dist/configs/index.mjs.map +0 -1
- package/dist/plugin-BjJ7yjrc.d.ts +0 -141
- package/dist/plugin-mHMV2-SG.d.mts +0 -141
- package/dist/plugins/index.d.mts +0 -85
- package/dist/plugins/index.d.ts +0 -85
- package/dist/plugins/index.js +0 -312
- package/dist/plugins/index.js.map +0 -1
- package/dist/plugins/index.mjs +0 -284
- package/dist/plugins/index.mjs.map +0 -1
- package/dist/transforms/index.d.mts +0 -38
- package/dist/transforms/index.d.ts +0 -38
- package/dist/transforms/index.js +0 -46
- package/dist/transforms/index.js.map +0 -1
- package/dist/transforms/index.mjs +0 -19
- package/dist/transforms/index.mjs.map +0 -1
- package/dist/types-_64SXyva.d.mts +0 -151
- package/dist/types-_64SXyva.d.ts +0 -151
package/README.md
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Type-safe verification and validation for Convex database operations.
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
## Features
|
|
6
7
|
|
|
7
8
|
- **Type-safe insert/patch** - Full TypeScript inference for your schema
|
|
8
9
|
- **Default values** - Make fields optional in `insert()` with automatic defaults
|
|
9
10
|
- **Protected columns** - Prevent accidental updates to critical fields in `patch()`
|
|
10
|
-
- **
|
|
11
|
-
- **Extensible** - Create your own validation
|
|
11
|
+
- **Unique constraints** - Enforce unique rows and columns using your indexes
|
|
12
|
+
- **Extensible** - Create your own validation extensions
|
|
12
13
|
|
|
13
14
|
## Installation
|
|
14
15
|
|
|
@@ -18,48 +19,35 @@ pnpm install convex-verify
|
|
|
18
19
|
|
|
19
20
|
**Peer Dependencies:**
|
|
20
21
|
|
|
21
|
-
- `convex` >= 1.
|
|
22
|
+
- `convex` >= 1.34.1
|
|
22
23
|
|
|
23
24
|
## Quick Start
|
|
24
25
|
|
|
25
26
|
```ts
|
|
26
|
-
import {
|
|
27
|
-
defaultValuesConfig,
|
|
28
|
-
protectedColumnsConfig,
|
|
29
|
-
uniqueColumnConfig,
|
|
30
|
-
uniqueRowConfig,
|
|
31
|
-
verifyConfig,
|
|
32
|
-
} from "convex-verify";
|
|
27
|
+
import { verifyConfig } from "convex-verify";
|
|
33
28
|
|
|
34
29
|
import schema from "./schema";
|
|
35
30
|
|
|
36
|
-
export const { insert, patch, dangerouslyPatch } = verifyConfig(schema, {
|
|
37
|
-
|
|
38
|
-
defaultValues: defaultValuesConfig(schema, () => ({
|
|
31
|
+
export const { insert, patch, dangerouslyPatch, verify, config } = verifyConfig(schema, {
|
|
32
|
+
defaultValues: {
|
|
39
33
|
posts: { status: "draft", views: 0 },
|
|
40
|
-
}
|
|
34
|
+
},
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
protectedColumns: protectedColumnsConfig(schema, {
|
|
36
|
+
protectedColumns: {
|
|
44
37
|
posts: ["authorId"],
|
|
45
|
-
}
|
|
38
|
+
},
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
uniqueRow: uniqueRowConfig(schema, {
|
|
40
|
+
uniqueRow: {
|
|
49
41
|
posts: ["by_author_slug"],
|
|
50
|
-
}
|
|
42
|
+
},
|
|
51
43
|
|
|
52
|
-
|
|
53
|
-
uniqueColumn: uniqueColumnConfig(schema, {
|
|
44
|
+
uniqueColumn: {
|
|
54
45
|
users: ["by_email", "by_username"],
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Custom/third-party plugins (optional)
|
|
58
|
-
plugins: [],
|
|
46
|
+
},
|
|
59
47
|
});
|
|
60
48
|
```
|
|
61
49
|
|
|
62
|
-
|
|
50
|
+
Use the returned helpers in mutations:
|
|
63
51
|
|
|
64
52
|
```ts
|
|
65
53
|
import { insert, patch } from "./verify";
|
|
@@ -67,7 +55,7 @@ import { insert, patch } from "./verify";
|
|
|
67
55
|
export const createPost = mutation({
|
|
68
56
|
args: { title: v.string(), content: v.string() },
|
|
69
57
|
handler: async (ctx, args) => {
|
|
70
|
-
// status and views are optional
|
|
58
|
+
// status and views are optional since defaults have been set
|
|
71
59
|
return await insert(ctx, "posts", {
|
|
72
60
|
title: args.title,
|
|
73
61
|
content: args.content,
|
|
@@ -82,337 +70,385 @@ export const updatePost = mutation({
|
|
|
82
70
|
// authorId is protected - TypeScript won't allow it here
|
|
83
71
|
await patch(ctx, "posts", args.id, {
|
|
84
72
|
title: args.title,
|
|
73
|
+
// authorId: "someone_else", // TypeScript error!
|
|
85
74
|
});
|
|
86
75
|
},
|
|
87
76
|
});
|
|
88
77
|
```
|
|
89
78
|
|
|
79
|
+
And use the returned verifier surface directly when you need schema-aware built-in checks outside the insert/patch helpers:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
await verify.uniqueRow(ctx, "posts", {
|
|
83
|
+
title: "Hello",
|
|
84
|
+
slug: "hello",
|
|
85
|
+
authorId: "author-1",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
config.uniqueRow.posts; // typed configured options
|
|
89
|
+
```
|
|
90
|
+
|
|
90
91
|
---
|
|
91
92
|
|
|
92
93
|
## API Reference
|
|
93
94
|
|
|
94
95
|
### `verifyConfig(schema, config)`
|
|
95
96
|
|
|
96
|
-
Main configuration function
|
|
97
|
+
Main configuration function. It accepts inline schema-aware config and returns typed mutation helpers plus a direct `verify` registry.
|
|
97
98
|
|
|
98
99
|
```ts
|
|
99
|
-
const { insert, patch, dangerouslyPatch,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
100
|
+
const { insert, patch, dangerouslyPatch, verify, config } = verifyConfig(schema, {
|
|
101
|
+
defaultValues?: {
|
|
102
|
+
posts?: { status?: "draft"; views?: number };
|
|
103
|
+
} | (() => { ... } | Promise<{ ... }>),
|
|
104
|
+
protectedColumns?: {
|
|
105
|
+
posts?: ["authorId"];
|
|
106
|
+
},
|
|
107
|
+
uniqueRow?: {
|
|
108
|
+
posts?: ["by_author_slug"];
|
|
109
|
+
},
|
|
110
|
+
uniqueColumn?: {
|
|
111
|
+
users?: ["by_email", "by_username"];
|
|
112
|
+
},
|
|
113
|
+
extensions?: Extension[],
|
|
110
114
|
});
|
|
111
115
|
```
|
|
112
116
|
|
|
113
117
|
#### Returns
|
|
114
118
|
|
|
115
|
-
| Function
|
|
119
|
+
| Function/Value | Description |
|
|
116
120
|
| ------------------ | ----------------------------------------------------------------------------------- |
|
|
117
|
-
| `insert` | Insert with default values applied and
|
|
118
|
-
| `patch` | Patch with protected columns removed
|
|
121
|
+
| `insert` | Insert with default values applied and extensions run |
|
|
122
|
+
| `patch` | Patch with protected columns removed and extensions run |
|
|
119
123
|
| `dangerouslyPatch` | Patch with full access to all columns (bypasses protected columns type restriction) |
|
|
120
|
-
| `
|
|
124
|
+
| `verify` | Built-in verifier functions for configured features only |
|
|
125
|
+
| `config` | Passive typed snapshot of the built-in config that was passed in |
|
|
121
126
|
|
|
122
127
|
---
|
|
123
128
|
|
|
124
|
-
##
|
|
125
|
-
|
|
126
|
-
Transforms modify the input type of `insert()`.
|
|
129
|
+
## `defaultValues`
|
|
127
130
|
|
|
128
|
-
|
|
131
|
+
Makes specified fields optional in `insert()` by providing default values. The types update automatically.
|
|
129
132
|
|
|
130
|
-
|
|
133
|
+
### Static Values
|
|
131
134
|
|
|
132
135
|
```ts
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
import { defaultValuesConfig } from "convex-verify/transforms";
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
#### Static Config
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
const defaults = defaultValuesConfig(schema, {
|
|
136
|
+
const { insert } = verifyConfig(schema, {
|
|
137
|
+
defaultValues: {
|
|
142
138
|
posts: { status: "draft", views: 0 },
|
|
143
139
|
comments: { likes: 0 },
|
|
140
|
+
},
|
|
144
141
|
});
|
|
145
142
|
```
|
|
146
143
|
|
|
147
|
-
|
|
144
|
+
### Dynamic Values
|
|
148
145
|
|
|
149
|
-
Use a function
|
|
146
|
+
Use a function when values should be generated fresh on each insert:
|
|
150
147
|
|
|
151
148
|
```ts
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
149
|
+
const { insert } = verifyConfig(schema, {
|
|
150
|
+
defaultValues: () => ({
|
|
151
|
+
posts: {
|
|
152
|
+
status: "draft",
|
|
153
|
+
slug: generateRandomSlug(),
|
|
154
|
+
createdAt: Date.now(),
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
159
158
|
```
|
|
160
159
|
|
|
161
|
-
|
|
160
|
+
### Async Values
|
|
162
161
|
|
|
163
162
|
```ts
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
163
|
+
const { insert } = verifyConfig(schema, {
|
|
164
|
+
defaultValues: async () => ({
|
|
165
|
+
posts: {
|
|
166
|
+
category: await fetchDefaultCategory(),
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
169
170
|
```
|
|
170
171
|
|
|
171
|
-
|
|
172
|
+
### Direct Verifier Calls
|
|
172
173
|
|
|
173
|
-
|
|
174
|
+
```ts
|
|
175
|
+
const { verify } = verifyConfig(schema, {
|
|
176
|
+
defaultValues: {
|
|
177
|
+
users: { status: "pending" },
|
|
178
|
+
},
|
|
179
|
+
});
|
|
174
180
|
|
|
175
|
-
|
|
181
|
+
const user = await verify.defaultValues("users", {
|
|
182
|
+
email: "alice@example.com",
|
|
183
|
+
username: "alice",
|
|
184
|
+
});
|
|
185
|
+
```
|
|
176
186
|
|
|
177
|
-
|
|
187
|
+
---
|
|
178
188
|
|
|
179
|
-
|
|
189
|
+
## `protectedColumns`
|
|
180
190
|
|
|
181
|
-
|
|
182
|
-
import { protectedColumnsConfig } from "convex-verify";
|
|
183
|
-
// or
|
|
184
|
-
import { protectedColumnsConfig } from "convex-verify/configs";
|
|
185
|
-
```
|
|
191
|
+
Removes specified columns from the `patch()` input type, preventing accidental updates to critical fields like `authorId` or `createdAt`.
|
|
186
192
|
|
|
187
|
-
|
|
193
|
+
### Usage
|
|
188
194
|
|
|
189
195
|
```ts
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
196
|
+
const { patch, dangerouslyPatch } = verifyConfig(schema, {
|
|
197
|
+
protectedColumns: {
|
|
198
|
+
posts: ["authorId", "createdAt"],
|
|
199
|
+
comments: ["postId", "authorId"],
|
|
200
|
+
},
|
|
193
201
|
});
|
|
194
202
|
```
|
|
195
203
|
|
|
196
|
-
|
|
204
|
+
### Bypassing Protection
|
|
197
205
|
|
|
198
|
-
Use `dangerouslyPatch()` when you need to update protected columns:
|
|
206
|
+
Use `dangerouslyPatch()` when you legitimately need to update protected columns:
|
|
199
207
|
|
|
200
208
|
```ts
|
|
201
209
|
// Regular patch - authorId not allowed
|
|
202
210
|
await patch(ctx, "posts", id, {
|
|
203
|
-
authorId: newAuthorId, //
|
|
204
|
-
title: "New Title", //
|
|
211
|
+
authorId: newAuthorId, // TypeScript error!
|
|
212
|
+
title: "New Title", // OK
|
|
205
213
|
});
|
|
206
214
|
|
|
207
215
|
// Dangerous patch - full access
|
|
208
216
|
await dangerouslyPatch(ctx, "posts", id, {
|
|
209
|
-
authorId: newAuthorId, //
|
|
217
|
+
authorId: newAuthorId, // OK (bypasses type restriction)
|
|
210
218
|
title: "New Title",
|
|
211
219
|
});
|
|
212
220
|
```
|
|
213
221
|
|
|
214
|
-
**Note:** `dangerouslyPatch()` still runs validation
|
|
222
|
+
**Note:** `dangerouslyPatch()` still runs validation extensions - only the type restriction is bypassed.
|
|
215
223
|
|
|
216
224
|
---
|
|
217
225
|
|
|
218
|
-
##
|
|
226
|
+
## `uniqueRow`
|
|
219
227
|
|
|
220
|
-
|
|
228
|
+
Enforces uniqueness across multiple columns using composite indexes. Useful for things like "unique slug per author" or "unique name per organization".
|
|
221
229
|
|
|
222
|
-
###
|
|
223
|
-
|
|
224
|
-
Enforces uniqueness across multiple columns using composite indexes.
|
|
230
|
+
### Usage
|
|
225
231
|
|
|
226
232
|
```ts
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
#### Usage
|
|
233
|
-
|
|
234
|
-
```ts
|
|
235
|
-
// As a named config key (recommended)
|
|
236
|
-
verifyConfig(schema, {
|
|
237
|
-
uniqueRow: uniqueRowConfig(schema, {
|
|
238
|
-
posts: ["by_author_slug"],
|
|
239
|
-
}),
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// Or in the plugins array
|
|
243
|
-
verifyConfig(schema, {
|
|
244
|
-
plugins: [
|
|
245
|
-
uniqueRowConfig(schema, {
|
|
246
|
-
posts: ["by_author_slug"],
|
|
247
|
-
}),
|
|
248
|
-
],
|
|
233
|
+
const { verify } = verifyConfig(schema, {
|
|
234
|
+
uniqueRow: {
|
|
235
|
+
posts: ["by_author_slug"], // Unique author + slug combination
|
|
236
|
+
projects: ["by_org_name"], // Unique org + name combination
|
|
237
|
+
},
|
|
249
238
|
});
|
|
250
239
|
```
|
|
251
240
|
|
|
252
|
-
|
|
241
|
+
### With Options
|
|
253
242
|
|
|
254
243
|
```ts
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
244
|
+
const { verify } = verifyConfig(schema, {
|
|
245
|
+
uniqueRow: {
|
|
246
|
+
posts: [
|
|
247
|
+
{
|
|
248
|
+
index: "by_author_slug",
|
|
249
|
+
identifiers: ["_id", "authorId"], // Fields that identify "same document"
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
},
|
|
258
253
|
});
|
|
259
254
|
```
|
|
260
255
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
```ts
|
|
264
|
-
const uniqueRows = uniqueRowConfig(schema, {
|
|
265
|
-
posts: [
|
|
266
|
-
{
|
|
267
|
-
index: "by_author_slug",
|
|
268
|
-
identifiers: ["_id", "authorId"], // Fields to check for "same document"
|
|
269
|
-
},
|
|
270
|
-
],
|
|
271
|
-
});
|
|
272
|
-
```
|
|
256
|
+
The `identifiers` option controls which fields are checked when determining if a conflicting row is actually the same document (useful during patch operations).
|
|
273
257
|
|
|
274
|
-
|
|
258
|
+
---
|
|
275
259
|
|
|
276
|
-
|
|
260
|
+
## `uniqueColumn`
|
|
277
261
|
|
|
278
|
-
|
|
279
|
-
import { uniqueColumnConfig } from "convex-verify";
|
|
280
|
-
// or
|
|
281
|
-
import { uniqueColumnConfig } from "convex-verify/plugins";
|
|
282
|
-
```
|
|
262
|
+
Enforces uniqueness on single columns using indexes. Useful for email addresses, usernames, slugs, etc.
|
|
283
263
|
|
|
284
|
-
|
|
264
|
+
### Usage
|
|
285
265
|
|
|
286
266
|
```ts
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
uniqueColumn: uniqueColumnConfig(schema, {
|
|
267
|
+
const { verify } = verifyConfig(schema, {
|
|
268
|
+
uniqueColumn: {
|
|
290
269
|
users: ["by_email", "by_username"],
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Or in the plugins array
|
|
295
|
-
verifyConfig(schema, {
|
|
296
|
-
plugins: [
|
|
297
|
-
uniqueColumnConfig(schema, {
|
|
298
|
-
users: ["by_email", "by_username"],
|
|
299
|
-
}),
|
|
300
|
-
],
|
|
270
|
+
organizations: ["by_slug"],
|
|
271
|
+
},
|
|
301
272
|
});
|
|
302
273
|
```
|
|
303
274
|
|
|
304
|
-
The column name is derived from the index name by removing `by_` prefix:
|
|
275
|
+
The column name is derived from the index name by removing the `by_` prefix:
|
|
305
276
|
|
|
306
277
|
- `by_username` → checks `username` column
|
|
307
278
|
- `by_email` → checks `email` column
|
|
308
279
|
|
|
309
|
-
|
|
280
|
+
### With Options
|
|
310
281
|
|
|
311
282
|
```ts
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
283
|
+
const { verify } = verifyConfig(schema, {
|
|
284
|
+
uniqueColumn: {
|
|
285
|
+
users: [
|
|
286
|
+
"by_username",
|
|
287
|
+
{ index: "by_email", identifiers: ["_id", "clerkId"] },
|
|
288
|
+
],
|
|
289
|
+
},
|
|
315
290
|
});
|
|
316
291
|
```
|
|
317
292
|
|
|
318
|
-
|
|
293
|
+
### Direct Verifier Calls
|
|
319
294
|
|
|
320
295
|
```ts
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
"by_username", // shorthand
|
|
324
|
-
{ index: "by_email", identifiers: ["_id", "clerkId"] }, // with options
|
|
325
|
-
],
|
|
296
|
+
await verify.uniqueColumn(ctx, "users", {
|
|
297
|
+
email: "alice@example.com",
|
|
326
298
|
});
|
|
327
299
|
```
|
|
328
300
|
|
|
329
|
-
|
|
301
|
+
Patch checks use the document id:
|
|
330
302
|
|
|
331
|
-
|
|
303
|
+
```ts
|
|
304
|
+
await verify.uniqueColumn(ctx, "users", userId, {
|
|
305
|
+
username: "alice",
|
|
306
|
+
});
|
|
307
|
+
```
|
|
332
308
|
|
|
333
|
-
|
|
309
|
+
Direct uniqueness verifier calls may use partial data. Only configured unique fields present in the payload are checked.
|
|
334
310
|
|
|
335
|
-
|
|
311
|
+
### Breaking Change
|
|
336
312
|
|
|
337
|
-
|
|
338
|
-
import { createValidatePlugin } from "convex-verify";
|
|
339
|
-
// or
|
|
340
|
-
import { createValidatePlugin } from "convex-verify/core";
|
|
341
|
-
```
|
|
313
|
+
Version `2.0.0` removes the old helper-wrapper API:
|
|
342
314
|
|
|
343
|
-
|
|
315
|
+
- `defaultValuesConfig`
|
|
316
|
+
- `protectedColumnsConfig`
|
|
317
|
+
- `uniqueRowConfig`
|
|
318
|
+
- `uniqueColumnConfig`
|
|
344
319
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
},
|
|
360
|
-
},
|
|
361
|
-
);
|
|
362
|
-
```
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Custom Extensions
|
|
323
|
+
|
|
324
|
+
Custom extensions let you add your own validation and transformation logic that runs during `insert()` and `patch()` operations.
|
|
325
|
+
|
|
326
|
+
### Use Cases
|
|
327
|
+
|
|
328
|
+
- **Authorization checks** - Verify the user has permission to create/modify a document
|
|
329
|
+
- **Data validation** - Check that values meet business rules (e.g., positive numbers, valid URLs)
|
|
330
|
+
- **Cross-field validation** - Ensure fields are consistent with each other
|
|
331
|
+
- **Normalization / sanitization** - Lowercase emails, trim slugs, clean incoming strings
|
|
332
|
+
- **External validation** - Check against external APIs or services
|
|
333
|
+
- **Audit logging** - Log operations before they complete
|
|
363
334
|
|
|
364
|
-
|
|
335
|
+
### Limitations
|
|
336
|
+
|
|
337
|
+
- Extensions run **after** type-affecting configs (like `defaultValues`) have been applied
|
|
338
|
+
- Extensions **cannot modify types** - they can change runtime data, but not the TypeScript types
|
|
339
|
+
- Extensions may **return modified data** - use this to sanitize, normalize, or enrich payloads
|
|
340
|
+
- Custom extensions from `extensions: []` run **before** built-in `uniqueRow` / `uniqueColumn` configs
|
|
341
|
+
- `patch()` still strips protected columns at runtime; use `dangerouslyPatch()` if an extension must change them
|
|
342
|
+
- Extension errors should use `ConvexError` for proper error handling on the client
|
|
343
|
+
|
|
344
|
+
### Execution Order
|
|
345
|
+
|
|
346
|
+
Custom extensions always run before built-in uniqueness checks.
|
|
347
|
+
|
|
348
|
+
- `insert()`: `defaultValues` → custom `extensions` → `uniqueRow` → `uniqueColumn`
|
|
349
|
+
- `patch()`: protected-column strip → custom `extensions` → `uniqueRow` → `uniqueColumn` → protected-column strip again
|
|
350
|
+
- `dangerouslyPatch()`: custom `extensions` → `uniqueRow` → `uniqueColumn`
|
|
351
|
+
|
|
352
|
+
`defaultValues` and protected-column stripping are preprocessing steps, not entries in the custom `extensions` array.
|
|
353
|
+
|
|
354
|
+
### Creating an Extension
|
|
355
|
+
|
|
356
|
+
Use `createExtension`. For schema-aware typing in the callback, pass the schema as the first argument:
|
|
365
357
|
|
|
366
358
|
```ts
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
359
|
+
import { createExtension } from "convex-verify";
|
|
360
|
+
import { ConvexError } from "convex/values";
|
|
361
|
+
|
|
362
|
+
const normalizeEmail = createExtension(schema, (input) => {
|
|
363
|
+
if (input.tableName !== "users") {
|
|
364
|
+
return input.data;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (input.operation === "insert") {
|
|
368
|
+
return {
|
|
369
|
+
...input.data,
|
|
370
|
+
email: input.data.email.toLowerCase().trim(),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
...input.data,
|
|
376
|
+
...(input.data.email !== undefined && {
|
|
377
|
+
email: input.data.email.toLowerCase().trim(),
|
|
378
|
+
}),
|
|
379
|
+
};
|
|
380
|
+
});
|
|
384
381
|
```
|
|
385
382
|
|
|
386
|
-
|
|
383
|
+
Use a single `input` parameter instead of destructuring when you want narrowing.
|
|
384
|
+
That lets TypeScript narrow `data` from both `tableName` and `operation`.
|
|
387
385
|
|
|
388
|
-
|
|
386
|
+
### Extension Context
|
|
387
|
+
|
|
388
|
+
Your extension function receives:
|
|
389
389
|
|
|
390
390
|
```ts
|
|
391
|
-
type
|
|
392
|
-
ctx: GenericMutationCtx; // Convex mutation context
|
|
391
|
+
type ExtensionInput = {
|
|
392
|
+
ctx: GenericMutationCtx; // Convex mutation context (has ctx.db, etc.)
|
|
393
393
|
tableName: string; // Table being operated on
|
|
394
394
|
operation: "insert" | "patch";
|
|
395
395
|
patchId?: GenericId; // Document ID (patch only)
|
|
396
|
-
|
|
397
|
-
|
|
396
|
+
schema: SchemaDefinition; // Schema reference
|
|
397
|
+
data: unknown;
|
|
398
398
|
};
|
|
399
399
|
```
|
|
400
400
|
|
|
401
|
-
|
|
401
|
+
### Example: Required Fields
|
|
402
402
|
|
|
403
|
-
|
|
403
|
+
```ts
|
|
404
|
+
const requiredFields = createExtension<typeof schema>((input) => {
|
|
405
|
+
if (input.tableName !== "posts" || input.operation === "patch") {
|
|
406
|
+
return input.data;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
for (const field of ["title", "content"]) {
|
|
410
|
+
if (!input.data[field]) {
|
|
411
|
+
throw new ConvexError({
|
|
412
|
+
code: "VALIDATION_ERROR",
|
|
413
|
+
message: `Missing required field: ${field}`,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return input.data;
|
|
419
|
+
});
|
|
420
|
+
```
|
|
404
421
|
|
|
405
|
-
|
|
422
|
+
### Example: Async Authorization Check
|
|
406
423
|
|
|
407
424
|
```ts
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
425
|
+
const ownership = createExtension<typeof schema>(async (input) => {
|
|
426
|
+
if (input.tableName !== "posts" || input.operation !== "patch") {
|
|
427
|
+
return input.data;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const doc = await input.ctx.db.get(input.patchId);
|
|
431
|
+
const identity = await input.ctx.auth.getUserIdentity();
|
|
432
|
+
|
|
433
|
+
if (doc?.ownerId !== identity?.subject) {
|
|
434
|
+
throw new ConvexError({
|
|
435
|
+
code: "UNAUTHORIZED",
|
|
436
|
+
message: "You don't have permission to edit this document",
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return input.data;
|
|
441
|
+
});
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Using Custom Extensions
|
|
445
|
+
|
|
446
|
+
Add extensions to the `extensions` array in your config:
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
const { insert, patch } = verifyConfig(schema, {
|
|
450
|
+
extensions: [requiredFields, ownership],
|
|
451
|
+
});
|
|
416
452
|
```
|
|
417
453
|
|
|
418
454
|
---
|
|
@@ -421,7 +457,7 @@ import { getTableIndexes } from "convex-verify/utils";
|
|
|
421
457
|
|
|
422
458
|
### `onFail` Callback
|
|
423
459
|
|
|
424
|
-
|
|
460
|
+
Operations accept an optional `onFail` callback for handling validation failures:
|
|
425
461
|
|
|
426
462
|
```ts
|
|
427
463
|
await insert(ctx, "posts", data, {
|
|
@@ -438,7 +474,7 @@ await insert(ctx, "posts", data, {
|
|
|
438
474
|
|
|
439
475
|
### Error Types
|
|
440
476
|
|
|
441
|
-
|
|
477
|
+
Built-in validation extensions throw `ConvexError` with these codes:
|
|
442
478
|
|
|
443
479
|
- `UNIQUE_ROW_VERIFICATION_ERROR` - Duplicate row detected
|
|
444
480
|
- `UNIQUE_COLUMN_VERIFICATION_ERROR` - Duplicate column value detected
|
|
@@ -447,11 +483,11 @@ Validation plugins throw `ConvexError` with specific codes:
|
|
|
447
483
|
|
|
448
484
|
## TypeScript
|
|
449
485
|
|
|
450
|
-
This library
|
|
486
|
+
This library provides full type inference:
|
|
451
487
|
|
|
452
488
|
- `insert()` types reflect optional fields from `defaultValues`
|
|
453
489
|
- `patch()` types exclude protected columns
|
|
454
|
-
-
|
|
490
|
+
- All configs are type-checked against your schema
|
|
455
491
|
- Index names are validated against your schema's indexes
|
|
456
492
|
|
|
457
493
|
---
|