Skip to content

Commit d58bb8f

Browse files
authored
Merge pull request #17 from udecode/feat/custom-mutation-builder
2 parents eae39e9 + df53a30 commit d58bb8f

File tree

4 files changed

+184
-83
lines changed

4 files changed

+184
-83
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'better-auth-convex': patch
3+
---
4+
5+
Add support for custom mutation builders in `createClient` and `createApi`. Both functions now accept an optional `internalMutation` parameter, allowing you to wrap internal mutations with custom context (e.g., triggers, aggregates, middleware).
6+
7+
**Usage:**
8+
9+
```ts
10+
const internalMutation = customMutation(
11+
internalMutationGeneric,
12+
customCtx(async (ctx) => ({
13+
db: triggers.wrapDB(ctx).db,
14+
}))
15+
);
16+
17+
// Pass to createClient
18+
createClient({
19+
authFunctions,
20+
schema,
21+
internalMutation,
22+
triggers,
23+
});
24+
25+
// Pass to createApi
26+
createApi(schema, {
27+
...auth.options,
28+
internalMutation,
29+
});
30+
```

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,20 @@ export const {
169169
updateMany,
170170
updateOne,
171171
} = createApi(schema, auth.options);
172+
173+
// Optional: If you need custom mutation builders (e.g., for custom context)
174+
// Pass internalMutation to both createClient and createApi
175+
// export const authClient = createClient<DataModel, typeof schema>({
176+
// authFunctions,
177+
// schema,
178+
// internalMutation: myCustomInternalMutation,
179+
// triggers: { ... }
180+
// });
181+
//
182+
// export const { create, ... } = createApi(schema, {
183+
// ...auth.options,
184+
// internalMutation: myCustomInternalMutation,
185+
// });
172186
```
173187

174188
The trigger API exposes both `before*` and `on*` hooks. The `before` variants run inside the same Convex transaction just ahead of the database write, letting you normalize input, enforce invariants, or perform cleanup and return any transformed payload that should be persisted.
@@ -240,6 +254,51 @@ const session = await getSession(ctx);
240254
const headers = await getHeaders(ctx);
241255
```
242256

257+
## Custom Mutation Builders
258+
259+
Both `createClient` and `createApi` accept an optional `internalMutation` parameter, allowing you to wrap internal mutations with custom context or behavior.
260+
261+
### Use Cases
262+
263+
This is useful when you need to:
264+
- Wrap database operations with custom context (e.g., triggers, logging)
265+
- Apply middleware to all auth mutations
266+
- Inject dependencies or configuration
267+
268+
### Example with Triggers
269+
270+
```ts
271+
import { customMutation, customCtx } from 'convex-helpers/server/customFunctions';
272+
import { internalMutationGeneric } from 'convex/server';
273+
import { registerTriggers } from '@convex/triggers';
274+
275+
const triggers = registerTriggers();
276+
277+
// Wrap mutations to include trigger-wrapped database
278+
const internalMutation = customMutation(
279+
internalMutationGeneric,
280+
customCtx(async (ctx) => ({
281+
db: triggers.wrapDB(ctx).db,
282+
}))
283+
);
284+
285+
// Pass to createClient
286+
export const authClient = createClient<DataModel, typeof schema>({
287+
authFunctions,
288+
schema,
289+
internalMutation, // Use custom mutation builder
290+
triggers: { ... }
291+
});
292+
293+
// Pass to createApi
294+
export const { create, updateOne, ... } = createApi(schema, {
295+
...auth.options,
296+
internalMutation, // Use same custom mutation builder
297+
});
298+
```
299+
300+
This ensures all auth operations (CRUD + triggers) use your wrapped database context.
301+
243302
## Updating the Schema
244303

245304
Better Auth configuration changes may require schema updates. The Better Auth docs will often note when this is the case. To regenerate the schema (it's generally safe to do), run:

src/api.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -395,12 +395,18 @@ export const deleteManyHandler = async (
395395

396396
export const createApi = <Schema extends SchemaDefinition<any, any>>(
397397
schema: Schema,
398-
authOptions: BetterAuthOptions
398+
{
399+
internalMutation,
400+
...authOptions
401+
}: BetterAuthOptions & {
402+
internalMutation?: typeof internalMutationGeneric;
403+
}
399404
) => {
400405
const betterAuthSchema = getAuthTables(authOptions);
406+
const mutationBuilder = internalMutation ?? internalMutationGeneric;
401407

402408
return {
403-
create: internalMutationGeneric({
409+
create: mutationBuilder({
404410
args: {
405411
beforeCreateHandle: v.optional(v.string()),
406412
input: v.union(
@@ -419,7 +425,7 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
419425
handler: async (ctx, args) =>
420426
createHandler(ctx, args, schema, betterAuthSchema),
421427
}),
422-
deleteMany: internalMutationGeneric({
428+
deleteMany: mutationBuilder({
423429
args: {
424430
beforeDeleteHandle: v.optional(v.string()),
425431
input: v.union(
@@ -440,7 +446,7 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
440446
handler: async (ctx, args) =>
441447
deleteManyHandler(ctx, args, schema, betterAuthSchema),
442448
}),
443-
deleteOne: internalMutationGeneric({
449+
deleteOne: mutationBuilder({
444450
args: {
445451
beforeDeleteHandle: v.optional(v.string()),
446452
input: v.union(
@@ -490,7 +496,7 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
490496
handler: async (ctx, args) =>
491497
findOneHandler(ctx, args, schema, betterAuthSchema),
492498
}),
493-
updateMany: internalMutationGeneric({
499+
updateMany: mutationBuilder({
494500
args: {
495501
beforeUpdateHandle: v.optional(v.string()),
496502
input: v.union(
@@ -512,7 +518,7 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
512518
handler: async (ctx, args) =>
513519
updateManyHandler(ctx, args, schema, betterAuthSchema),
514520
}),
515-
updateOne: internalMutationGeneric({
521+
updateOne: mutationBuilder({
516522
args: {
517523
beforeUpdateHandle: v.optional(v.string()),
518524
input: v.union(

src/client.ts

Lines changed: 83 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export const createClient = <
7979
>(config: {
8080
authFunctions: AuthFunctions;
8181
schema: Schema;
82+
internalMutation?: typeof internalMutationGeneric;
8283
triggers?: Triggers<DataModel, Schema>;
8384
}) => {
8485
return {
@@ -87,83 +88,88 @@ export const createClient = <
8788
adapter: (ctx: GenericCtx<DataModel>, options: BetterAuthOptions) =>
8889
dbAdapter(ctx, options, config),
8990
httpAdapter: (ctx: GenericCtx<DataModel>) => httpAdapter(ctx, config),
90-
triggersApi: () => ({
91-
beforeCreate: internalMutationGeneric({
92-
args: {
93-
data: v.any(),
94-
model: v.string(),
95-
},
96-
handler: async (ctx, args) => {
97-
return (
98-
(await config?.triggers?.[args.model]?.beforeCreate?.(
99-
ctx,
100-
args.data
101-
)) ?? args.data
102-
);
103-
},
104-
}),
105-
beforeDelete: internalMutationGeneric({
106-
args: {
107-
doc: v.any(),
108-
model: v.string(),
109-
},
110-
handler: async (ctx, args) => {
111-
return (
112-
(await config?.triggers?.[args.model]?.beforeDelete?.(
113-
ctx,
114-
args.doc
115-
)) ?? args.doc
116-
);
117-
},
118-
}),
119-
beforeUpdate: internalMutationGeneric({
120-
args: {
121-
doc: v.any(),
122-
model: v.string(),
123-
update: v.any(),
124-
},
125-
handler: async (ctx, args) => {
126-
return (
127-
(await config?.triggers?.[args.model]?.beforeUpdate?.(
91+
triggersApi: () => {
92+
const mutationBuilder =
93+
config.internalMutation ?? internalMutationGeneric;
94+
95+
return {
96+
beforeCreate: mutationBuilder({
97+
args: {
98+
data: v.any(),
99+
model: v.string(),
100+
},
101+
handler: async (ctx, args) => {
102+
return (
103+
(await config?.triggers?.[args.model]?.beforeCreate?.(
104+
ctx,
105+
args.data
106+
)) ?? args.data
107+
);
108+
},
109+
}),
110+
beforeDelete: mutationBuilder({
111+
args: {
112+
doc: v.any(),
113+
model: v.string(),
114+
},
115+
handler: async (ctx, args) => {
116+
return (
117+
(await config?.triggers?.[args.model]?.beforeDelete?.(
118+
ctx,
119+
args.doc
120+
)) ?? args.doc
121+
);
122+
},
123+
}),
124+
beforeUpdate: mutationBuilder({
125+
args: {
126+
doc: v.any(),
127+
model: v.string(),
128+
update: v.any(),
129+
},
130+
handler: async (ctx, args) => {
131+
return (
132+
(await config?.triggers?.[args.model]?.beforeUpdate?.(
133+
ctx,
134+
args.doc,
135+
args.update
136+
)) ?? args.update
137+
);
138+
},
139+
}),
140+
onCreate: mutationBuilder({
141+
args: {
142+
doc: v.any(),
143+
model: v.string(),
144+
},
145+
handler: async (ctx, args) => {
146+
await config?.triggers?.[args.model]?.onCreate?.(ctx, args.doc);
147+
},
148+
}),
149+
onDelete: mutationBuilder({
150+
args: {
151+
doc: v.any(),
152+
model: v.string(),
153+
},
154+
handler: async (ctx, args) => {
155+
await config?.triggers?.[args.model]?.onDelete?.(ctx, args.doc);
156+
},
157+
}),
158+
onUpdate: mutationBuilder({
159+
args: {
160+
model: v.string(),
161+
newDoc: v.any(),
162+
oldDoc: v.any(),
163+
},
164+
handler: async (ctx, args) => {
165+
await config?.triggers?.[args.model]?.onUpdate?.(
128166
ctx,
129-
args.doc,
130-
args.update
131-
)) ?? args.update
132-
);
133-
},
134-
}),
135-
onCreate: internalMutationGeneric({
136-
args: {
137-
doc: v.any(),
138-
model: v.string(),
139-
},
140-
handler: async (ctx, args) => {
141-
await config?.triggers?.[args.model]?.onCreate?.(ctx, args.doc);
142-
},
143-
}),
144-
onDelete: internalMutationGeneric({
145-
args: {
146-
doc: v.any(),
147-
model: v.string(),
148-
},
149-
handler: async (ctx, args) => {
150-
await config?.triggers?.[args.model]?.onDelete?.(ctx, args.doc);
151-
},
152-
}),
153-
onUpdate: internalMutationGeneric({
154-
args: {
155-
model: v.string(),
156-
newDoc: v.any(),
157-
oldDoc: v.any(),
158-
},
159-
handler: async (ctx, args) => {
160-
await config?.triggers?.[args.model]?.onUpdate?.(
161-
ctx,
162-
args.newDoc,
163-
args.oldDoc
164-
);
165-
},
166-
}),
167-
}),
167+
args.newDoc,
168+
args.oldDoc
169+
);
170+
},
171+
}),
172+
};
173+
},
168174
};
169175
};

0 commit comments

Comments
 (0)