diff --git a/.changeset/custom-mutation-builder.md b/.changeset/custom-mutation-builder.md new file mode 100644 index 0000000..6fa7364 --- /dev/null +++ b/.changeset/custom-mutation-builder.md @@ -0,0 +1,30 @@ +--- +'better-auth-convex': patch +--- + +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). + +**Usage:** + +```ts +const internalMutation = customMutation( + internalMutationGeneric, + customCtx(async (ctx) => ({ + db: triggers.wrapDB(ctx).db, + })) +); + +// Pass to createClient +createClient({ + authFunctions, + schema, + internalMutation, + triggers, +}); + +// Pass to createApi +createApi(schema, { + ...auth.options, + internalMutation, +}); +``` diff --git a/README.md b/README.md index a5df5b5..ce0d4f3 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,20 @@ export const { updateMany, updateOne, } = createApi(schema, auth.options); + +// Optional: If you need custom mutation builders (e.g., for custom context) +// Pass internalMutation to both createClient and createApi +// export const authClient = createClient({ +// authFunctions, +// schema, +// internalMutation: myCustomInternalMutation, +// triggers: { ... } +// }); +// +// export const { create, ... } = createApi(schema, { +// ...auth.options, +// internalMutation: myCustomInternalMutation, +// }); ``` 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); const headers = await getHeaders(ctx); ``` +## Custom Mutation Builders + +Both `createClient` and `createApi` accept an optional `internalMutation` parameter, allowing you to wrap internal mutations with custom context or behavior. + +### Use Cases + +This is useful when you need to: +- Wrap database operations with custom context (e.g., triggers, logging) +- Apply middleware to all auth mutations +- Inject dependencies or configuration + +### Example with Triggers + +```ts +import { customMutation, customCtx } from 'convex-helpers/server/customFunctions'; +import { internalMutationGeneric } from 'convex/server'; +import { registerTriggers } from '@convex/triggers'; + +const triggers = registerTriggers(); + +// Wrap mutations to include trigger-wrapped database +const internalMutation = customMutation( + internalMutationGeneric, + customCtx(async (ctx) => ({ + db: triggers.wrapDB(ctx).db, + })) +); + +// Pass to createClient +export const authClient = createClient({ + authFunctions, + schema, + internalMutation, // Use custom mutation builder + triggers: { ... } +}); + +// Pass to createApi +export const { create, updateOne, ... } = createApi(schema, { + ...auth.options, + internalMutation, // Use same custom mutation builder +}); +``` + +This ensures all auth operations (CRUD + triggers) use your wrapped database context. + ## Updating the Schema 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: diff --git a/src/api.ts b/src/api.ts index f3da6a4..faf5811 100644 --- a/src/api.ts +++ b/src/api.ts @@ -395,12 +395,18 @@ export const deleteManyHandler = async ( export const createApi = >( schema: Schema, - authOptions: BetterAuthOptions + { + internalMutation, + ...authOptions + }: BetterAuthOptions & { + internalMutation?: typeof internalMutationGeneric; + } ) => { const betterAuthSchema = getAuthTables(authOptions); + const mutationBuilder = internalMutation ?? internalMutationGeneric; return { - create: internalMutationGeneric({ + create: mutationBuilder({ args: { beforeCreateHandle: v.optional(v.string()), input: v.union( @@ -419,7 +425,7 @@ export const createApi = >( handler: async (ctx, args) => createHandler(ctx, args, schema, betterAuthSchema), }), - deleteMany: internalMutationGeneric({ + deleteMany: mutationBuilder({ args: { beforeDeleteHandle: v.optional(v.string()), input: v.union( @@ -440,7 +446,7 @@ export const createApi = >( handler: async (ctx, args) => deleteManyHandler(ctx, args, schema, betterAuthSchema), }), - deleteOne: internalMutationGeneric({ + deleteOne: mutationBuilder({ args: { beforeDeleteHandle: v.optional(v.string()), input: v.union( @@ -490,7 +496,7 @@ export const createApi = >( handler: async (ctx, args) => findOneHandler(ctx, args, schema, betterAuthSchema), }), - updateMany: internalMutationGeneric({ + updateMany: mutationBuilder({ args: { beforeUpdateHandle: v.optional(v.string()), input: v.union( @@ -512,7 +518,7 @@ export const createApi = >( handler: async (ctx, args) => updateManyHandler(ctx, args, schema, betterAuthSchema), }), - updateOne: internalMutationGeneric({ + updateOne: mutationBuilder({ args: { beforeUpdateHandle: v.optional(v.string()), input: v.union( diff --git a/src/client.ts b/src/client.ts index f5fcb1e..39b4aca 100644 --- a/src/client.ts +++ b/src/client.ts @@ -79,6 +79,7 @@ export const createClient = < >(config: { authFunctions: AuthFunctions; schema: Schema; + internalMutation?: typeof internalMutationGeneric; triggers?: Triggers; }) => { return { @@ -87,83 +88,88 @@ export const createClient = < adapter: (ctx: GenericCtx, options: BetterAuthOptions) => dbAdapter(ctx, options, config), httpAdapter: (ctx: GenericCtx) => httpAdapter(ctx, config), - triggersApi: () => ({ - beforeCreate: internalMutationGeneric({ - args: { - data: v.any(), - model: v.string(), - }, - handler: async (ctx, args) => { - return ( - (await config?.triggers?.[args.model]?.beforeCreate?.( - ctx, - args.data - )) ?? args.data - ); - }, - }), - beforeDelete: internalMutationGeneric({ - args: { - doc: v.any(), - model: v.string(), - }, - handler: async (ctx, args) => { - return ( - (await config?.triggers?.[args.model]?.beforeDelete?.( - ctx, - args.doc - )) ?? args.doc - ); - }, - }), - beforeUpdate: internalMutationGeneric({ - args: { - doc: v.any(), - model: v.string(), - update: v.any(), - }, - handler: async (ctx, args) => { - return ( - (await config?.triggers?.[args.model]?.beforeUpdate?.( + triggersApi: () => { + const mutationBuilder = + config.internalMutation ?? internalMutationGeneric; + + return { + beforeCreate: mutationBuilder({ + args: { + data: v.any(), + model: v.string(), + }, + handler: async (ctx, args) => { + return ( + (await config?.triggers?.[args.model]?.beforeCreate?.( + ctx, + args.data + )) ?? args.data + ); + }, + }), + beforeDelete: mutationBuilder({ + args: { + doc: v.any(), + model: v.string(), + }, + handler: async (ctx, args) => { + return ( + (await config?.triggers?.[args.model]?.beforeDelete?.( + ctx, + args.doc + )) ?? args.doc + ); + }, + }), + beforeUpdate: mutationBuilder({ + args: { + doc: v.any(), + model: v.string(), + update: v.any(), + }, + handler: async (ctx, args) => { + return ( + (await config?.triggers?.[args.model]?.beforeUpdate?.( + ctx, + args.doc, + args.update + )) ?? args.update + ); + }, + }), + onCreate: mutationBuilder({ + args: { + doc: v.any(), + model: v.string(), + }, + handler: async (ctx, args) => { + await config?.triggers?.[args.model]?.onCreate?.(ctx, args.doc); + }, + }), + onDelete: mutationBuilder({ + args: { + doc: v.any(), + model: v.string(), + }, + handler: async (ctx, args) => { + await config?.triggers?.[args.model]?.onDelete?.(ctx, args.doc); + }, + }), + onUpdate: mutationBuilder({ + args: { + model: v.string(), + newDoc: v.any(), + oldDoc: v.any(), + }, + handler: async (ctx, args) => { + await config?.triggers?.[args.model]?.onUpdate?.( ctx, - args.doc, - args.update - )) ?? args.update - ); - }, - }), - onCreate: internalMutationGeneric({ - args: { - doc: v.any(), - model: v.string(), - }, - handler: async (ctx, args) => { - await config?.triggers?.[args.model]?.onCreate?.(ctx, args.doc); - }, - }), - onDelete: internalMutationGeneric({ - args: { - doc: v.any(), - model: v.string(), - }, - handler: async (ctx, args) => { - await config?.triggers?.[args.model]?.onDelete?.(ctx, args.doc); - }, - }), - onUpdate: internalMutationGeneric({ - args: { - model: v.string(), - newDoc: v.any(), - oldDoc: v.any(), - }, - handler: async (ctx, args) => { - await config?.triggers?.[args.model]?.onUpdate?.( - ctx, - args.newDoc, - args.oldDoc - ); - }, - }), - }), + args.newDoc, + args.oldDoc + ); + }, + }), + }; + }, }; };