#can I wrap a mutation multiple times with custom mutations? To extend functionality

9 messages · Page 1 of 1 (latest)

neon ocean
#

can I wrap a mutation multiple times with custom mutations? To extend functionality

late copperBOT
#

Hi @neon ocean. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

You technically can nest custom function wrappers, but the recommended practice in the Convex ecosystem is not to stack customMutation (or other custom functions) on top of other custom functions.

Two places explicitly caution against this:

  • In the triggers + ents Discord thread, support suggests: “try not nesting customMutations, make one that creates the ents wrapping and the session stuff” and later “I’d try to avoid doing custom mutations on top of other custom mutations, and instead use the triggers.wrapDB explicitly in your sessionMutation, and make helper functions where applicable.” [triggers+ents thread]
  • The “Authorization in Practice” and “Custom functions” articles emphasize a design where you have one layer of wrapping and then compose additional behavior via regular helper functions, not by stacking wrappers. [Authorization practice; Custom functions]

The recommended pattern to “extend functionality” is:

  1. Define one custom mutation wrapper (e.g. myMutation) that does all context/args shaping you need (auth, triggers, ents, etc.).
  2. Put additional behavior into plain helper functions that you call from inside your handlers, or into the input/customCtx logic of that single custom mutation. [Authentication utilities; Triggers best practices]

So:

#
  • Possible? Yes, but not recommended.
  • Recommended? Use a single customMutation wrapper and compose behavior inside it or via helpers, rather than wrapping the same mutation multiple times.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@neon ocean Kapa.ai is still learning and improving, please let me know how I did by reacting below.

neon ocean
#

@late copper can I do this dynamic definition?

export const createProtectedMutation = <T>(extraArgs: T) =>
customMutation(convexMutation, {
args: { locale: v.optional(v.string()) },
input: async (
ctx,
args,
opts: {
permissions?: readonly Permission[];
getWorkspaceId?: (args: Record<string, string>) => string;
}
) => {
const locale = (args.locale ?? DEFAULT_LOCALE) as Locale;
const user = await getUserIdentity(ctx);

  return { ctx: { user, locale }, args: {} };
}

});

late copperBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The knowledge sources don’t show an example of a factory like createProtectedMutation that returns a customMutation-wrapped builder, so I can’t confirm this exact pattern is supported as written.

What is documented is:

  • customMutation is normally used to create a single replacement builder that you then reuse directly, e.g. export const authMutation = customMutation(mutation, customCtx(...)), and then authMutation({ args, handler }) in your functions. [Authorization practice; Auth utilities]
  • The input function receives (ctx, args, opts) where opts is whatever you pass at the call site of the builder (e.g. { role: "admin" } in the RBAC example). [Role-based access]

From those examples, the documented / supported pattern looks like:

// one shared builder
export const protectedMutation = customMutation(convexMutation, {
  args: { locale: v.optional(v.string()) },
  input: async (
    ctx,
    args,
    opts: {
      permissions?: readonly Permission[];
      getWorkspaceId?: (args: Record<string, string>) => string;
    }
  ) => {
    const locale = (args.locale ?? DEFAULT_LOCALE) as Locale;
    const user = await getUserIdentity(ctx);
    return { ctx: { user, locale }, args: {} };
  },
});

// usage: pass per-endpoint options via `opts`
export const someMutation = protectedMutation({
  permissions: ["read"],
  args: { /* endpoint-specific args */ },
  handler: async (ctx, args) => {
    // ctx.user, ctx.locale available
  },
});
#

This matches the RBAC example where teamMutation is a single builder and each endpoint passes role via the options object, not via a higher‑order factory. [Role-based access]

Because the sources don’t show customMutation being wrapped in a generic factory like <T>(extraArgs: T) => customMutation(...), I can’t say from the docs whether your createProtectedMutation generic is type‑correct or supported; only that the recommended and documented approach is to define one customMutation builder and parameterize behavior via the opts argument at each call site.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: