UNPKG

convex-helpers

Version:

A collection of useful code to complement the official convex package.

296 lines (295 loc) 9.87 kB
/** * A helper for defining a Mod when your mod doesn't need to add or remove * anything from args. * @param mod A function that defines how to modify the ctx. * @returns A ctx delta to be applied to the original ctx. */ export function customCtx(mod) { return { args: {}, input: async (ctx) => ({ ctx: await mod(ctx), args: {} }), }; } /** * A Mod that doesn't add or remove any context or args. */ export const NoOp = { args: {}, input() { return { args: {}, ctx: {} }; }, }; /** * customQuery helps define custom behavior on top of `query` or `internalQuery` * by passing a function that modifies the ctx and args. * * Example usage: * ```js * const myQueryBuilder = customQuery(query, { * args: { sessionId: v.id("sessions") }, * input: async (ctx, args) => { * const user = await getUserOrNull(ctx); * const session = await db.get(sessionId); * const db = wrapDatabaseReader({ user }, ctx.db, rlsRules); * return { ctx: { db, user, session }, args: {} }; * }, * }); * * // Using the custom builder * export const getSomeData = myQueryBuilder({ * args: { someArg: v.string() }, * handler: async (ctx, args) => { * const { db, user, session, scheduler } = ctx; * const { someArg } = args; * // ... * } * }); * ``` * * Simple usage only modifying ctx: * ```js * const myInternalQuery = customQuery( * internalQuery, * customCtx(async (ctx) => { * return { * // Throws an exception if the user isn't logged in * user: await getUserByTokenIdentifier(ctx), * }; * }) * ); * * // Using it * export const getUser = myInternalQuery({ * args: {}, * handler: async (ctx, args) => { * return ctx.user; * }, * }); * * @param query The query to be modified. Usually `query` or `internalQuery` * from `_generated/server`. * @param mod The modifier to be applied to the query, changing ctx and args. * @returns A new query builder to define queries with modified ctx and args. */ export function customQuery(query, mod) { function customQueryBuilder(fn) { // Looking forward to when input / args / ... are optional const inputMod = mod.input ?? NoOp.input; const inputArgs = mod.args ?? NoOp.args; if ("args" in fn) { return query({ args: { ...fn.args, ...inputArgs, }, handler: async (ctx, allArgs) => { const { split, rest } = splitArgs(inputArgs, allArgs); const added = await inputMod(ctx, split); return await fn.handler({ ...ctx, ...added.ctx }, { ...rest, ...added.args }); }, }); } if (Object.keys(inputArgs).length > 0) { throw new Error("If you're using a custom function with arguments for the input " + "modifier, you must declare the arguments for the function too."); } const handler = fn.handler ?? fn; return query({ handler: async (ctx, args) => { const { ctx: modCtx } = await inputMod(ctx, args); return await handler({ ...ctx, ...modCtx }, args); }, }); } return customQueryBuilder; } /** * customMutation helps define custom behavior on top of `mutation` * or `internalMutation` by passing a function that modifies the ctx and args. * * Example usage: * ```js * const myMutationBuilder = customMutation(mutation, { * args: { sessionId: v.id("sessions") }, * input: async (ctx, args) => { * const user = await getUserOrNull(ctx); * const session = await db.get(sessionId); * const db = wrapDatabaseReader({ user }, ctx.db, rlsRules); * return { ctx: { db, user, session }, args: {} }; * }, * }); * * // Using the custom builder * export const setSomeData = myMutationBuilder({ * args: { someArg: v.string() }, * handler: async (ctx, args) => { * const { db, user, session, scheduler } = ctx; * const { someArg } = args; * // ... * } * }); * ``` * * Simple usage only modifying ctx: * ```js * const myUserMutation = customMutation( * mutation, * customCtx(async (ctx) => { * return { * // Throws an exception if the user isn't logged in * user: await getUserByTokenIdentifier(ctx), * }; * }) * ); * * // Using it * export const setMyName = myUserMutation({ * args: { name: v.string() }, * handler: async (ctx, args) => { * await ctx.db.patch(ctx.user._id, { name: args.name }); * }, * }); * * @param mutation The mutation to be modified. Usually `mutation` or `internalMutation` * from `_generated/server`. * @param mod The modifier to be applied to the mutation, changing ctx and args. * @returns A new mutation builder to define queries with modified ctx and args. */ export function customMutation(mutation, mod) { function customMutationBuilder(fn) { // Looking forward to when input / args / ... are optional const inputMod = mod.input ?? NoOp.input; const inputArgs = mod.args ?? NoOp.args; if ("args" in fn) { return mutation({ args: { ...fn.args, ...inputArgs, }, handler: async (ctx, allArgs) => { const { split, rest } = splitArgs(inputArgs, allArgs); const added = await inputMod(ctx, split); return await fn.handler({ ...ctx, ...added.ctx }, { ...rest, ...added.args }); }, }); } if (Object.keys(inputArgs).length > 0) { throw new Error("If you're using a custom function with arguments for the input " + "modifier, you must declare the arguments for the function too."); } const handler = fn.handler ?? fn; return mutation({ handler: async (ctx, args) => { const { ctx: modCtx } = await inputMod(ctx, args); return await handler({ ...ctx, ...modCtx }, args); }, }); } return customMutationBuilder; } /** * customAction helps define custom behavior on top of `action` * or `internalAction` by passing a function that modifies the ctx and args. * * Example usage: * ```js * const myActionBuilder = customAction(action, { * args: { secretKey: v.string() }, * input: async (ctx, args) => { * // Very basic authorization, e.g. from trusted backends. * if (args.secretKey !== process.env.SECRET_KEY) { * throw new Error("Invalid secret key"); * } * const user = await ctx.runQuery(internal.users.getUser, {}); * return { ctx: { user }, args: {} }; * }, * }); * * // Using the custom builder * export const runSomeAction = myActionBuilder({ * args: { someArg: v.string() }, * handler: async (ctx, args) => { * const { user, scheduler } = ctx; * const { someArg } = args; * // ... * } * }); * ``` * * Simple usage only modifying ctx: * ```js * const myUserAction = customAction( * internalAction, * customCtx(async (ctx) => { * return { * // Throws an exception if the user isn't logged in * user: await ctx.runQuery(internal.users.getUser, {}); * }; * }) * ); * * // Using it * export const sendUserEmail = myUserAction({ * args: { subject: v.string(), body: v.string() }, * handler: async (ctx, args) => { * await sendEmail(ctx.user.email, args.subject, args.body); * }, * }); * * @param action The action to be modified. Usually `action` or `internalAction` * from `_generated/server`. * @param mod The modifier to be applied to the action, changing ctx and args. * @returns A new action builder to define queries with modified ctx and args. */ export function customAction(action, mod) { function customActionBuilder(fn) { // Looking forward to when input / args / ... are optional const inputMod = mod.input ?? NoOp.input; const inputArgs = mod.args ?? NoOp.args; if ("args" in fn) { return action({ args: { ...fn.args, ...inputArgs, }, handler: async (ctx, allArgs) => { const { split, rest } = splitArgs(inputArgs, allArgs); const added = await inputMod(ctx, split); return await fn.handler({ ...ctx, ...added.ctx }, { ...rest, ...added.args }); }, }); } if (Object.keys(inputArgs).length > 0) { throw new Error("If you're using a custom function with arguments for the input " + "modifier, you must declare the arguments for the function too."); } const handler = fn.handler ?? fn; return action({ handler: async (ctx, args) => { const { ctx: modCtx } = await inputMod(ctx, args); return await handler({ ...ctx, ...modCtx }, args); }, }); } return customActionBuilder; } /** * * @param splitArgsValidator The args that should be split out from the rest. * As an object mapping arg names to validators (v.* from convex/values). * @param args The arguments to a function, including values to be split out. * @returns The args split into two objects: `split` and `rest` based on keys. */ export function splitArgs(splitArgsValidator, args) { const rest = {}; const split = {}; for (const arg in args) { if (arg in splitArgsValidator) { split[arg] = args[arg]; } else { rest[arg] = args[arg]; } } return { split, rest }; }