convex-helpers
Version:
A collection of useful code to complement the official convex package.
296 lines (295 loc) • 9.87 kB
JavaScript
/**
* 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 };
}