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.
Files changed (46) hide show
  1. package/README.md +271 -235
  2. package/dist/core/index.d.mts +14 -85
  3. package/dist/core/index.d.ts +14 -85
  4. package/dist/core/index.js +520 -83
  5. package/dist/core/index.js.map +1 -1
  6. package/dist/core/index.mjs +516 -80
  7. package/dist/core/index.mjs.map +1 -1
  8. package/dist/index.d.mts +9 -6
  9. package/dist/index.d.ts +9 -6
  10. package/dist/index.js +386 -233
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +383 -226
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/types-B8ZkLuJ2.d.mts +141 -0
  15. package/dist/types-B8ZkLuJ2.d.ts +141 -0
  16. package/dist/utils/index.d.mts +3 -2
  17. package/dist/utils/index.d.ts +3 -2
  18. package/dist/utils/index.js +1 -1
  19. package/dist/utils/index.js.map +1 -1
  20. package/dist/utils/index.mjs +1 -1
  21. package/dist/utils/index.mjs.map +1 -1
  22. package/dist/verifyConfig-CTrtqMr_.d.ts +94 -0
  23. package/dist/verifyConfig-Kn3Ikj00.d.mts +94 -0
  24. package/package.json +5 -22
  25. package/dist/configs/index.d.mts +0 -51
  26. package/dist/configs/index.d.ts +0 -51
  27. package/dist/configs/index.js +0 -38
  28. package/dist/configs/index.js.map +0 -1
  29. package/dist/configs/index.mjs +0 -11
  30. package/dist/configs/index.mjs.map +0 -1
  31. package/dist/plugin-BjJ7yjrc.d.ts +0 -141
  32. package/dist/plugin-mHMV2-SG.d.mts +0 -141
  33. package/dist/plugins/index.d.mts +0 -85
  34. package/dist/plugins/index.d.ts +0 -85
  35. package/dist/plugins/index.js +0 -312
  36. package/dist/plugins/index.js.map +0 -1
  37. package/dist/plugins/index.mjs +0 -284
  38. package/dist/plugins/index.mjs.map +0 -1
  39. package/dist/transforms/index.d.mts +0 -38
  40. package/dist/transforms/index.d.ts +0 -38
  41. package/dist/transforms/index.js +0 -46
  42. package/dist/transforms/index.js.map +0 -1
  43. package/dist/transforms/index.mjs +0 -19
  44. package/dist/transforms/index.mjs.map +0 -1
  45. package/dist/types-_64SXyva.d.mts +0 -151
  46. 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
- - **Validation plugins** - Unique row/column enforcement, custom validators
11
- - **Extensible** - Create your own validation plugins
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.31.3
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
- // Make fields optional with defaults
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
- // Prevent patching critical fields
43
- protectedColumns: protectedColumnsConfig(schema, {
36
+ protectedColumns: {
44
37
  posts: ["authorId"],
45
- }),
38
+ },
46
39
 
47
- // Enforce unique row combinations
48
- uniqueRow: uniqueRowConfig(schema, {
40
+ uniqueRow: {
49
41
  posts: ["by_author_slug"],
50
- }),
42
+ },
51
43
 
52
- // Enforce unique column values
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
- Then use in your mutations:
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 - defaults are applied
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 that returns typed `insert`, `patch`, and `dangerouslyPatch` functions.
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, configs } = verifyConfig(schema, {
100
- // Type-affecting configs
101
- defaultValues?: DefaultValuesConfig,
102
- protectedColumns?: ProtectedColumnsConfig,
103
-
104
- // Built-in validation configs
105
- uniqueRow?: UniqueRowConfig,
106
- uniqueColumn?: UniqueColumnConfig,
107
-
108
- // Custom/third-party plugins
109
- plugins?: ValidatePlugin[],
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 | Description |
119
+ | Function/Value | Description |
116
120
  | ------------------ | ----------------------------------------------------------------------------------- |
117
- | `insert` | Insert with default values applied and validation plugins run |
118
- | `patch` | Patch with protected columns removed from type and validation plugins run |
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
- | `configs` | The original config object (for debugging) |
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
- ## Transforms
125
-
126
- Transforms modify the input type of `insert()`.
129
+ ## `defaultValues`
127
130
 
128
- ### `defaultValuesConfig(schema, config)`
131
+ Makes specified fields optional in `insert()` by providing default values. The types update automatically.
129
132
 
130
- Makes specified fields optional in `insert()` by providing default values.
133
+ ### Static Values
131
134
 
132
135
  ```ts
133
- import { defaultValuesConfig } from "convex-verify";
134
- // or
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
- #### Dynamic Config (Fresh Values)
144
+ ### Dynamic Values
148
145
 
149
- Use a function for values that should be generated fresh on each insert:
146
+ Use a function when values should be generated fresh on each insert:
150
147
 
151
148
  ```ts
152
- const defaults = defaultValuesConfig(schema, () => ({
153
- posts: {
154
- status: "draft",
155
- slug: generateRandomSlug(),
156
- createdAt: Date.now(),
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
- #### Async Config
160
+ ### Async Values
162
161
 
163
162
  ```ts
164
- const defaults = defaultValuesConfig(schema, async () => ({
165
- posts: {
166
- category: await fetchDefaultCategory(),
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
- ## Configs
174
+ ```ts
175
+ const { verify } = verifyConfig(schema, {
176
+ defaultValues: {
177
+ users: { status: "pending" },
178
+ },
179
+ });
174
180
 
175
- Configs modify the input type of `patch()`.
181
+ const user = await verify.defaultValues("users", {
182
+ email: "alice@example.com",
183
+ username: "alice",
184
+ });
185
+ ```
176
186
 
177
- ### `protectedColumnsConfig(schema, config)`
187
+ ---
178
188
 
179
- Removes specified columns from the `patch()` input type, preventing accidental updates.
189
+ ## `protectedColumns`
180
190
 
181
- ```ts
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
- #### Example
193
+ ### Usage
188
194
 
189
195
  ```ts
190
- const protected = protectedColumnsConfig(schema, {
191
- posts: ["authorId", "createdAt"],
192
- comments: ["postId", "authorId"],
196
+ const { patch, dangerouslyPatch } = verifyConfig(schema, {
197
+ protectedColumns: {
198
+ posts: ["authorId", "createdAt"],
199
+ comments: ["postId", "authorId"],
200
+ },
193
201
  });
194
202
  ```
195
203
 
196
- #### Bypassing Protection
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, // TypeScript error
204
- title: "New Title", // OK
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, // OK (bypasses protection)
217
+ authorId: newAuthorId, // OK (bypasses type restriction)
210
218
  title: "New Title",
211
219
  });
212
220
  ```
213
221
 
214
- **Note:** `dangerouslyPatch()` still runs validation plugins - only the type restriction is bypassed.
222
+ **Note:** `dangerouslyPatch()` still runs validation extensions - only the type restriction is bypassed.
215
223
 
216
224
  ---
217
225
 
218
- ## Validation
226
+ ## `uniqueRow`
219
227
 
220
- Validation configs check data during `insert()` and `patch()` operations. They run after transforms and can throw errors to prevent the operation.
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
- ### `uniqueRowConfig(schema, config)`
223
-
224
- Enforces uniqueness across multiple columns using composite indexes.
230
+ ### Usage
225
231
 
226
232
  ```ts
227
- import { uniqueRowConfig } from "convex-verify";
228
- // or
229
- import { uniqueRowConfig } from "convex-verify/plugins";
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
- #### Shorthand (Index Names)
241
+ ### With Options
253
242
 
254
243
  ```ts
255
- const uniqueRows = uniqueRowConfig(schema, {
256
- posts: ["by_author_slug"], // Unique author + slug combo
257
- projects: ["by_org_slug"], // Unique org + slug combo
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
- #### With Options
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
- ### `uniqueColumnConfig(schema, config)`
258
+ ---
275
259
 
276
- Enforces uniqueness on single columns using indexes.
260
+ ## `uniqueColumn`
277
261
 
278
- ```ts
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
- #### Usage
264
+ ### Usage
285
265
 
286
266
  ```ts
287
- // As a named config key (recommended)
288
- verifyConfig(schema, {
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
- #### Shorthand (Index Names)
280
+ ### With Options
310
281
 
311
282
  ```ts
312
- const uniqueColumns = uniqueColumnConfig(schema, {
313
- users: ["by_username", "by_email"],
314
- organizations: ["by_slug"],
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
- #### With Options
293
+ ### Direct Verifier Calls
319
294
 
320
295
  ```ts
321
- const uniqueColumns = uniqueColumnConfig(schema, {
322
- users: [
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
- ## Custom Plugins
301
+ Patch checks use the document id:
330
302
 
331
- The `plugins` array accepts custom validation plugins for extensibility.
303
+ ```ts
304
+ await verify.uniqueColumn(ctx, "users", userId, {
305
+ username: "alice",
306
+ });
307
+ ```
332
308
 
333
- ### `createValidatePlugin(name, config, handlers)`
309
+ Direct uniqueness verifier calls may use partial data. Only configured unique fields present in the payload are checked.
334
310
 
335
- Create custom validation plugins.
311
+ ### Breaking Change
336
312
 
337
- ```ts
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
- #### Example: Required Fields Plugin
315
+ - `defaultValuesConfig`
316
+ - `protectedColumnsConfig`
317
+ - `uniqueRowConfig`
318
+ - `uniqueColumnConfig`
344
319
 
345
- ```ts
346
- const requiredFields = createValidatePlugin(
347
- "requiredFields",
348
- { fields: ["title", "content"] },
349
- {
350
- insert: (context, data) => {
351
- for (const field of context.config.fields) {
352
- if (!data[field]) {
353
- throw new ConvexError({
354
- message: `Missing required field: ${field}`,
355
- });
356
- }
357
- }
358
- return data;
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
- #### Example: Async Validation
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
- const checkOwnership = createValidatePlugin(
368
- "checkOwnership",
369
- {},
370
- {
371
- patch: async (context, data) => {
372
- const existing = await context.ctx.db.get(context.patchId);
373
- const user = await getCurrentUser(context.ctx);
374
-
375
- if (existing?.authorId !== user._id) {
376
- throw new ConvexError({
377
- message: "Not authorized to edit this document",
378
- });
379
- }
380
- return data;
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
- #### Plugin Context
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
- Plugins receive a `ValidateContext` object:
386
+ ### Extension Context
387
+
388
+ Your extension function receives:
389
389
 
390
390
  ```ts
391
- type ValidateContext = {
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
- onFail?: OnFailCallback; // Callback for failure details
397
- schema?: SchemaDefinition; // Schema reference
396
+ schema: SchemaDefinition; // Schema reference
397
+ data: unknown;
398
398
  };
399
399
  ```
400
400
 
401
- ---
401
+ ### Example: Required Fields
402
402
 
403
- ## Subpath Imports
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
- For smaller bundle sizes, you can import from specific subpaths:
422
+ ### Example: Async Authorization Check
406
423
 
407
424
  ```ts
408
- // Import everything from root
409
- import { uniqueRowConfig, verifyConfig } from "convex-verify";
410
- import { protectedColumnsConfig } from "convex-verify/configs";
411
- // Or import from specific subpaths
412
- import { createValidatePlugin, verifyConfig } from "convex-verify/core";
413
- import { uniqueColumnConfig, uniqueRowConfig } from "convex-verify/plugins";
414
- import { defaultValuesConfig } from "convex-verify/transforms";
415
- import { getTableIndexes } from "convex-verify/utils";
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
- All operations accept an optional `onFail` callback for handling validation failures:
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
- Validation plugins throw `ConvexError` with specific codes:
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 is written in TypeScript and provides full type inference:
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
- - Plugin configs are type-checked against your schema
490
+ - All configs are type-checked against your schema
455
491
  - Index names are validated against your schema's indexes
456
492
 
457
493
  ---