diff --git a/.changeset/add-before-hooks.md b/.changeset/add-before-hooks.md new file mode 100644 index 0000000..117eb92 --- /dev/null +++ b/.changeset/add-before-hooks.md @@ -0,0 +1,5 @@ +--- +"better-auth-convex": minor +--- + +Add `beforeCreate`, `beforeUpdate`, and `beforeDelete` hook support across the Convex adapter so triggers can transform payloads before database writes. diff --git a/src/adapter.ts b/src/adapter.ts index 7f5c546..93dee61 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -196,8 +196,15 @@ export const httpAdapter = < authFunctions.onCreate )) as FunctionHandle<'mutation'>) : undefined; + const beforeCreateHandle = + authFunctions.beforeCreate && triggers?.[model]?.beforeCreate + ? ((await createFunctionHandle( + authFunctions.beforeCreate + )) as FunctionHandle<'mutation'>) + : undefined; return ctx.runMutation(authFunctions.create, { + beforeCreateHandle: beforeCreateHandle, input: { data, model }, select, onCreateHandle: onCreateHandle, @@ -214,7 +221,14 @@ export const httpAdapter = < authFunctions.onDelete )) as FunctionHandle<'mutation'>) : undefined; + const beforeDeleteHandle = + authFunctions.beforeDelete && triggers?.[data.model]?.beforeDelete + ? ((await createFunctionHandle( + authFunctions.beforeDelete + )) as FunctionHandle<'mutation'>) + : undefined; await ctx.runMutation(authFunctions.deleteOne, { + beforeDeleteHandle: beforeDeleteHandle, input: { model: data.model, where: parseWhere(data.where), @@ -233,8 +247,15 @@ export const httpAdapter = < authFunctions.onDelete )) as FunctionHandle<'mutation'>) : undefined; + const beforeDeleteHandle = + authFunctions.beforeDelete && triggers?.[data.model]?.beforeDelete + ? ((await createFunctionHandle( + authFunctions.beforeDelete + )) as FunctionHandle<'mutation'>) + : undefined; const result = await handlePagination(async ({ paginationOpts }) => { return await ctx.runMutation(authFunctions.deleteMany, { + beforeDeleteHandle: beforeDeleteHandle, input: { ...data, where: parseWhere(data.where), @@ -297,8 +318,15 @@ export const httpAdapter = < authFunctions.onUpdate )) as FunctionHandle<'mutation'>) : undefined; + const beforeUpdateHandle = + authFunctions.beforeUpdate && triggers?.[data.model]?.beforeUpdate + ? ((await createFunctionHandle( + authFunctions.beforeUpdate + )) as FunctionHandle<'mutation'>) + : undefined; return ctx.runMutation(authFunctions.updateOne, { + beforeUpdateHandle: beforeUpdateHandle, input: { model: data.model as any, update: data.update as any, @@ -321,9 +349,16 @@ export const httpAdapter = < authFunctions.onUpdate )) as FunctionHandle<'mutation'>) : undefined; + const beforeUpdateHandle = + authFunctions.beforeUpdate && triggers?.[data.model]?.beforeUpdate + ? ((await createFunctionHandle( + authFunctions.beforeUpdate + )) as FunctionHandle<'mutation'>) + : undefined; const result = await handlePagination(async ({ paginationOpts }) => { return await ctx.runMutation(authFunctions.updateMany, { + beforeUpdateHandle: beforeUpdateHandle, input: { ...(data as any), where: parseWhere(data.where), @@ -398,10 +433,17 @@ export const dbAdapter = < authFunctions.onCreate )) as FunctionHandle<'mutation'>) : undefined; + const beforeCreateHandle = + authFunctions.beforeCreate && triggers?.[model]?.beforeCreate + ? ((await createFunctionHandle( + authFunctions.beforeCreate + )) as FunctionHandle<'mutation'>) + : undefined; return createHandler( ctx, { + beforeCreateHandle: beforeCreateHandle, input: { data, model }, select, onCreateHandle: onCreateHandle, @@ -417,10 +459,17 @@ export const dbAdapter = < authFunctions.onDelete )) as FunctionHandle<'mutation'>) : undefined; + const beforeDeleteHandle = + authFunctions.beforeDelete && triggers?.[data.model]?.beforeDelete + ? ((await createFunctionHandle( + authFunctions.beforeDelete + )) as FunctionHandle<'mutation'>) + : undefined; await deleteOneHandler( ctx, { + beforeDeleteHandle: beforeDeleteHandle, input: { model: data.model, where: parseWhere(data.where), @@ -438,11 +487,18 @@ export const dbAdapter = < authFunctions.onDelete )) as FunctionHandle<'mutation'>) : undefined; + const beforeDeleteHandle = + authFunctions.beforeDelete && triggers?.[data.model]?.beforeDelete + ? ((await createFunctionHandle( + authFunctions.beforeDelete + )) as FunctionHandle<'mutation'>) + : undefined; const result = await handlePagination(async ({ paginationOpts }) => { return await deleteManyHandler( ctx, { + beforeDeleteHandle: beforeDeleteHandle, input: { ...data, where: parseWhere(data.where), @@ -520,10 +576,17 @@ export const dbAdapter = < authFunctions.onUpdate )) as FunctionHandle<'mutation'>) : undefined; + const beforeUpdateHandle = + authFunctions.beforeUpdate && triggers?.[data.model]?.beforeUpdate + ? ((await createFunctionHandle( + authFunctions.beforeUpdate + )) as FunctionHandle<'mutation'>) + : undefined; return updateOneHandler( ctx, { + beforeUpdateHandle: beforeUpdateHandle, input: { model: data.model as any, update: data.update as any, @@ -545,11 +608,18 @@ export const dbAdapter = < authFunctions.onUpdate )) as FunctionHandle<'mutation'>) : undefined; + const beforeUpdateHandle = + authFunctions.beforeUpdate && triggers?.[data.model]?.beforeUpdate + ? ((await createFunctionHandle( + authFunctions.beforeUpdate + )) as FunctionHandle<'mutation'>) + : undefined; const result = await handlePagination(async ({ paginationOpts }) => { return await updateManyHandler( ctx, { + beforeUpdateHandle: beforeUpdateHandle, input: { ...(data as any), where: parseWhere(data.where), diff --git a/src/api.ts b/src/api.ts index a7922e3..4a820a3 100644 --- a/src/api.ts +++ b/src/api.ts @@ -65,20 +65,37 @@ export const createHandler = async ( data: any; model: string; }; + beforeCreateHandle?: string; select?: string[]; onCreateHandle?: string; }, schema: Schema, betterAuthSchema: any ) => { + let data = args.input.data; + + if (args.beforeCreateHandle) { + const transformedData = await ctx.runMutation( + args.beforeCreateHandle as FunctionHandle<'mutation'>, + { + data, + model: args.input.model, + } + ); + + if (transformedData !== undefined) { + data = transformedData; + } + } + await checkUniqueFields( ctx, schema, betterAuthSchema, args.input.model, - args.input.data + data ); - const id = await ctx.db.insert(args.input.model as any, args.input.data); + const id = await ctx.db.insert(args.input.model as any, data); const doc = await ctx.db.get(id); if (!doc) { @@ -137,6 +154,7 @@ export const updateOneHandler = async ( update: any; where?: any[]; }; + beforeUpdateHandle?: string; onUpdateHandle?: string; }, schema: Schema, @@ -148,15 +166,32 @@ export const updateOneHandler = async ( throw new Error(`Failed to update ${args.input.model}`); } + let update = args.input.update; + + if (args.beforeUpdateHandle) { + const transformedUpdate = await ctx.runMutation( + args.beforeUpdateHandle as FunctionHandle<'mutation'>, + { + doc, + model: args.input.model, + update, + } + ); + + if (transformedUpdate !== undefined) { + update = transformedUpdate; + } + } + await checkUniqueFields( ctx, schema, betterAuthSchema, args.input.model, - args.input.update, + update, doc ); - await ctx.db.patch(doc._id as GenericId, args.input.update as any); + await ctx.db.patch(doc._id as GenericId, update as any); const updatedDoc = await ctx.db.get(doc._id as GenericId); if (!updatedDoc) { @@ -182,6 +217,7 @@ export const updateManyHandler = async ( where?: any[]; }; paginationOpts: any; + beforeUpdateHandle?: string; onUpdateHandle?: string; }, schema: Schema, @@ -207,25 +243,40 @@ export const updateManyHandler = async ( } await asyncMap(page, async (doc: any) => { + let update = args.input.update; + + if (args.beforeUpdateHandle) { + const transformedUpdate = await ctx.runMutation( + args.beforeUpdateHandle as FunctionHandle<'mutation'>, + { + doc, + model: args.input.model, + update, + } + ); + + if (transformedUpdate !== undefined) { + update = transformedUpdate; + } + } + await checkUniqueFields( ctx, schema, betterAuthSchema, args.input.model, - args.input.update ?? {}, + update ?? {}, doc ); - await ctx.db.patch( - doc._id as GenericId, - args.input.update as any - ); + await ctx.db.patch(doc._id as GenericId, update as any); if (args.onUpdateHandle) { + const newDoc = await ctx.db.get(doc._id as GenericId); await ctx.runMutation( args.onUpdateHandle as FunctionHandle<'mutation'>, { model: args.input.model, - newDoc: await ctx.db.get(doc._id as GenericId), + newDoc, oldDoc: doc, } ); @@ -247,6 +298,7 @@ export const deleteOneHandler = async ( model: string; where?: any[]; }; + beforeDeleteHandle?: string; onDeleteHandle?: string; }, schema: Schema, @@ -258,16 +310,32 @@ export const deleteOneHandler = async ( return; } + let hookDoc = doc; + + if (args.beforeDeleteHandle) { + const transformedDoc = await ctx.runMutation( + args.beforeDeleteHandle as FunctionHandle<'mutation'>, + { + doc, + model: args.input.model, + } + ); + + if (transformedDoc !== undefined) { + hookDoc = transformedDoc; + } + } + await ctx.db.delete(doc._id as GenericId); if (args.onDeleteHandle) { await ctx.runMutation(args.onDeleteHandle as FunctionHandle<'mutation'>, { - doc, + doc: hookDoc, model: args.input.model, }); } - return doc; + return hookDoc; }; export const deleteManyHandler = async ( @@ -278,6 +346,7 @@ export const deleteManyHandler = async ( where?: any[]; }; paginationOpts: any; + beforeDeleteHandle?: string; onDeleteHandle?: string; }, schema: Schema, @@ -288,14 +357,30 @@ export const deleteManyHandler = async ( paginationOpts: args.paginationOpts, }); await asyncMap(page, async (doc: any) => { + let hookDoc = doc; + + if (args.beforeDeleteHandle) { + const transformedDoc = await ctx.runMutation( + args.beforeDeleteHandle as FunctionHandle<'mutation'>, + { + doc, + model: args.input.model, + } + ); + + if (transformedDoc !== undefined) { + hookDoc = transformedDoc; + } + } + + await ctx.db.delete(doc._id as GenericId); + if (args.onDeleteHandle) { await ctx.runMutation(args.onDeleteHandle as FunctionHandle<'mutation'>, { - doc, + doc: hookDoc, model: args.input.model, }); } - - await ctx.db.delete(doc._id as GenericId); }); return { @@ -314,6 +399,7 @@ export const createApi = >( return { create: internalMutationGeneric({ args: { + beforeCreateHandle: v.optional(v.string()), input: v.union( ...Object.entries(schema.tables).map(([model, table]) => v.object({ @@ -330,6 +416,7 @@ export const createApi = >( }), deleteMany: internalMutationGeneric({ args: { + beforeDeleteHandle: v.optional(v.string()), input: v.union( ...Object.keys(schema.tables).map((tableName) => { return v.object({ @@ -350,6 +437,7 @@ export const createApi = >( }), deleteOne: internalMutationGeneric({ args: { + beforeDeleteHandle: v.optional(v.string()), input: v.union( ...Object.keys(schema.tables).map((tableName) => { return v.object({ @@ -399,6 +487,7 @@ export const createApi = >( }), updateMany: internalMutationGeneric({ args: { + beforeUpdateHandle: v.optional(v.string()), input: v.union( ...Object.entries(schema.tables).map( ([tableName, table]: [string, Schema['tables'][string]]) => { @@ -420,6 +509,7 @@ export const createApi = >( }), updateOne: internalMutationGeneric({ args: { + beforeUpdateHandle: v.optional(v.string()), input: v.union( ...Object.entries(schema.tables).map( ([tableName, table]: [string, Schema['tables'][string]]) => { diff --git a/src/client.ts b/src/client.ts index 1e86372..fe0cfb0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -26,6 +26,9 @@ export type AuthFunctions = { onCreate: FunctionReference<'mutation', 'internal', Record>; onDelete: FunctionReference<'mutation', 'internal', Record>; onUpdate: FunctionReference<'mutation', 'internal', Record>; + beforeCreate?: FunctionReference<'mutation', 'internal', Record>; + beforeDelete?: FunctionReference<'mutation', 'internal', Record>; + beforeUpdate?: FunctionReference<'mutation', 'internal', Record>; }; export type Triggers< @@ -33,6 +36,22 @@ export type Triggers< Schema extends SchemaDefinition, > = { [K in keyof Schema['tables'] & string]?: { + beforeCreate?: ( + ctx: GenericMutationCtx, + data: Infer + ) => Promise | void>; + beforeDelete?: ( + ctx: GenericMutationCtx, + doc: Infer & IdField & SystemFields + ) => Promise< + | (Infer & IdField & SystemFields) + | void + >; + beforeUpdate?: ( + ctx: GenericMutationCtx, + doc: Infer & IdField & SystemFields, + update: Partial> + ) => Promise> | void>; onCreate?: ( ctx: GenericMutationCtx, doc: Infer & IdField & SystemFields @@ -69,6 +88,50 @@ export const createClient = < 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?.( + ctx, + args.doc, + args.update + )) ?? args.update + ); + }, + }), onCreate: internalMutationGeneric({ args: { doc: v.any(),