convex-helpers 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  A collection of useful code to complement the official packages.
4
4
 
5
+ ## Custom Functions
6
+
7
+ Build your own customized versions of `query`, `mutation`, and `action` that
8
+ define custom behavior, allowing you to:
9
+
10
+ - Run authentication logic before the request starts.
11
+ - Look up commonly used data and add it to the ctx argument.
12
+ - Replace a ctx or argument field with a different value, such as a version
13
+ of `db` that runs custom functions on data access.
14
+ - Consume arguments from the client that are not passed to the action, such
15
+ as taking in an authentication parameter like an API key or session ID.
16
+ These arguments must be sent up by the client along with each request.
17
+
18
+ For example:
19
+ ```js
20
+ import { customQuery } from "convex-helpers/server/customFunctions.js
21
+
22
+ const myQueryBuilder = customQuery(query, {
23
+ args: { sessionId: v.id("sessions") },
24
+ input: async (ctx, args) => {
25
+ const user = await getUserOrNull(ctx);
26
+ const session = await db.get(sessionId);
27
+ const db = wrapDatabaseReader({ user }, ctx.db, rlsRules);
28
+ return { ctx: { db, user, session }, args: {} };
29
+ },
30
+ });
31
+
32
+ // Using the custom builder everywhere you would have used `query`
33
+ export const getSomeData = myQueryBuilder({
34
+ args: { someArg: v.string() },
35
+ handler: async (ctx, args) => {
36
+ const { db, user, session, scheduler } = ctx;
37
+ const { someArg } = args;
38
+ // ...
39
+ }
40
+ });
41
+ ```
42
+
5
43
  ## Row-level security
6
44
 
7
45
  See the [Stack post on row-level security](https://stack.convex.dev/row-level-security)
@@ -0,0 +1,14 @@
1
+ /**
2
+ * asyncMap returns the results of applying an async function over an list.
3
+ *
4
+ * @param list - Iterable object of items, e.g. an Array, Set, Object.keys
5
+ * @param asyncTransform
6
+ * @returns
7
+ */
8
+ export declare function asyncMap<FromType, ToType>(list: Iterable<FromType>, asyncTransform: (item: FromType) => Promise<ToType>): Promise<ToType[]>;
9
+ /**
10
+ * Filters out null elements from an array.
11
+ * @param list List of elements that might be null.
12
+ * @returns List of elements with nulls removed.
13
+ */
14
+ export declare function pruneNull<T>(list: (T | null)[]): T[];
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * asyncMap returns the results of applying an async function over an list.
3
+ *
4
+ * @param list - Iterable object of items, e.g. an Array, Set, Object.keys
5
+ * @param asyncTransform
6
+ * @returns
7
+ */
8
+ export async function asyncMap(list, asyncTransform) {
9
+ const promises = [];
10
+ for (const item of list) {
11
+ promises.push(asyncTransform(item));
12
+ }
13
+ return Promise.all(promises);
14
+ }
15
+ /**
16
+ * Filters out null elements from an array.
17
+ * @param list List of elements that might be null.
18
+ * @returns List of elements with nulls removed.
19
+ */
20
+ export function pruneNull(list) {
21
+ return list.filter((i) => i !== null);
22
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * This file contains helpers for defining custom functions that modify the
3
+ * context and arguments of a Convex function. Allows you to:
4
+ *
5
+ * - Run authentication logic before the request starts.
6
+ * - Look up commonly used data and add it to the ctx argument.
7
+ * - Replace a ctx or argument field with a different value, such as a version
8
+ * of `db` that runs custom functions on data access.
9
+ * - Consume arguments from the client that are not passed to the query, such
10
+ * as taking in an authentication parameter like an API key or session ID.
11
+ * These arguments must be sent up by the client along with each request.
12
+ */
13
+ import { ObjectType, PropertyValidators } from "convex/values";
14
+ import { ActionBuilder, FunctionVisibility, GenericActionCtx, GenericDataModel, GenericMutationCtx, GenericQueryCtx, MutationBuilder, QueryBuilder, RegisteredAction, RegisteredMutation, RegisteredQuery, UnvalidatedFunction } from "convex/server";
15
+ /**
16
+ * A modifier for a query, mutation, or action.
17
+ *
18
+ * This defines what arguments are required for the modifier, and how to modify
19
+ * the ctx and args. If the required args are not returned, they will not be
20
+ * provided for the modified function. All returned ctx and args will show up
21
+ * in the type signature for the modified function.
22
+ * To remove something from `ctx`, you can return it as `undefined`.
23
+ */
24
+ export type Mod<Ctx extends Record<string, any>, ModArgsValidator extends PropertyValidators, ModCtx extends Record<string, any>, ModMadeArgs extends Record<string, any>> = {
25
+ args: ModArgsValidator;
26
+ input: (ctx: Ctx, args: ObjectType<ModArgsValidator>) => Promise<{
27
+ ctx: ModCtx;
28
+ args: ModMadeArgs;
29
+ }> | {
30
+ ctx: ModCtx;
31
+ args: ModMadeArgs;
32
+ };
33
+ };
34
+ /**
35
+ * A helper for defining a Mod when your mod doesn't need to add or remove
36
+ * anything from args.
37
+ * @param mod A function that defines how to modify the ctx.
38
+ * @returns A ctx delta to be applied to the original ctx.
39
+ */
40
+ export declare function customCtx<InCtx extends Record<string, any>, OutCtx extends Record<string, any>>(mod: (original: InCtx) => Promise<OutCtx> | OutCtx): Mod<InCtx, {}, OutCtx, {}>;
41
+ /**
42
+ * A Mod that doesn't add or remove any context or args.
43
+ */
44
+ export declare const NoOp: {
45
+ args: {};
46
+ input(): {
47
+ args: {};
48
+ ctx: {};
49
+ };
50
+ };
51
+ /**
52
+ * customQuery helps define custom behavior on top of `query` or `internalQuery`
53
+ * by passing a function that modifies the ctx and args.
54
+ *
55
+ * Example usage:
56
+ * ```js
57
+ * const myQueryBuilder = customQuery(query, {
58
+ * args: { sessionId: v.id("sessions") },
59
+ * input: async (ctx, args) => {
60
+ * const user = await getUserOrNull(ctx);
61
+ * const session = await db.get(sessionId);
62
+ * const db = wrapDatabaseReader({ user }, ctx.db, rlsRules);
63
+ * return { ctx: { db, user, session }, args: {} };
64
+ * },
65
+ * });
66
+ *
67
+ * // Using the custom builder
68
+ * export const getSomeData = myQueryBuilder({
69
+ * args: { someArg: v.string() },
70
+ * handler: async (ctx, args) => {
71
+ * const { db, user, session, scheduler } = ctx;
72
+ * const { someArg } = args;
73
+ * // ...
74
+ * }
75
+ * });
76
+ * ```
77
+ *
78
+ * Simple usage only modifying ctx:
79
+ * ```js
80
+ * const myInternalQuery = customQuery(
81
+ * internalQuery,
82
+ * customCtx(async (ctx) => {
83
+ * return {
84
+ * // Throws an exception if the user isn't logged in
85
+ * user: await getUserByTokenIdentifier(ctx),
86
+ * };
87
+ * })
88
+ * );
89
+ *
90
+ * // Using it
91
+ * export const getUser = myInternalQuery({
92
+ * args: {},
93
+ * handler: async (ctx, args) => {
94
+ * return ctx.user;
95
+ * },
96
+ * });
97
+ *
98
+ * @param query The query to be modified. Usually `query` or `internalQuery`
99
+ * from `_generated/server`.
100
+ * @param mod The modifier to be applied to the query, changing ctx and args.
101
+ * @returns A new query builder to define queries with modified ctx and args.
102
+ */
103
+ export declare function customQuery<ModArgsValidator extends PropertyValidators, ModCtx extends Record<string, any>, ModMadeArgs extends Record<string, any>, Visibility extends FunctionVisibility, DataModel extends GenericDataModel>(query: QueryBuilder<DataModel, Visibility>, mod: Mod<GenericQueryCtx<DataModel>, ModArgsValidator, ModCtx, ModMadeArgs>): CustomBuilder<"query", ModArgsValidator, ModCtx, ModMadeArgs, GenericQueryCtx<DataModel>, Visibility>;
104
+ /**
105
+ * customMutation helps define custom behavior on top of `mutation`
106
+ * or `internalMutation` by passing a function that modifies the ctx and args.
107
+ *
108
+ * Example usage:
109
+ * ```js
110
+ * const myMutationBuilder = customMutation(mutation, {
111
+ * args: { sessionId: v.id("sessions") },
112
+ * input: async (ctx, args) => {
113
+ * const user = await getUserOrNull(ctx);
114
+ * const session = await db.get(sessionId);
115
+ * const db = wrapDatabaseReader({ user }, ctx.db, rlsRules);
116
+ * return { ctx: { db, user, session }, args: {} };
117
+ * },
118
+ * });
119
+ *
120
+ * // Using the custom builder
121
+ * export const setSomeData = myMutationBuilder({
122
+ * args: { someArg: v.string() },
123
+ * handler: async (ctx, args) => {
124
+ * const { db, user, session, scheduler } = ctx;
125
+ * const { someArg } = args;
126
+ * // ...
127
+ * }
128
+ * });
129
+ * ```
130
+ *
131
+ * Simple usage only modifying ctx:
132
+ * ```js
133
+ * const myUserMutation = customMutation(
134
+ * mutation,
135
+ * customCtx(async (ctx) => {
136
+ * return {
137
+ * // Throws an exception if the user isn't logged in
138
+ * user: await getUserByTokenIdentifier(ctx),
139
+ * };
140
+ * })
141
+ * );
142
+ *
143
+ * // Using it
144
+ * export const setMyName = myUserMutation({
145
+ * args: { name: v.string() },
146
+ * handler: async (ctx, args) => {
147
+ * await ctx.db.patch(ctx.user._id, { name: args.name });
148
+ * },
149
+ * });
150
+ *
151
+ * @param mutation The mutation to be modified. Usually `mutation` or `internalMutation`
152
+ * from `_generated/server`.
153
+ * @param mod The modifier to be applied to the mutation, changing ctx and args.
154
+ * @returns A new mutation builder to define queries with modified ctx and args.
155
+ */
156
+ export declare function customMutation<ModArgsValidator extends PropertyValidators, ModCtx extends Record<string, any>, ModMadeArgs extends Record<string, any>, Visibility extends FunctionVisibility, DataModel extends GenericDataModel>(mutation: MutationBuilder<DataModel, Visibility>, mod: Mod<GenericMutationCtx<DataModel>, ModArgsValidator, ModCtx, ModMadeArgs>): CustomBuilder<"mutation", ModArgsValidator, ModCtx, ModMadeArgs, GenericMutationCtx<DataModel>, Visibility>;
157
+ /**
158
+ * customAction helps define custom behavior on top of `action`
159
+ * or `internalAction` by passing a function that modifies the ctx and args.
160
+ *
161
+ * Example usage:
162
+ * ```js
163
+ * const myActionBuilder = customAction(action, {
164
+ * args: { secretKey: v.string() },
165
+ * input: async (ctx, args) => {
166
+ * // Very basic authorization, e.g. from trusted backends.
167
+ * if (args.secretKey !== process.env.SECRET_KEY) {
168
+ * throw new Error("Invalid secret key");
169
+ * }
170
+ * const user = await ctx.runQuery(internal.users.getUser, {});
171
+ * return { ctx: { user }, args: {} };
172
+ * },
173
+ * });
174
+ *
175
+ * // Using the custom builder
176
+ * export const runSomeAction = myActionBuilder({
177
+ * args: { someArg: v.string() },
178
+ * handler: async (ctx, args) => {
179
+ * const { user, scheduler } = ctx;
180
+ * const { someArg } = args;
181
+ * // ...
182
+ * }
183
+ * });
184
+ * ```
185
+ *
186
+ * Simple usage only modifying ctx:
187
+ * ```js
188
+ * const myUserAction = customAction(
189
+ * internalAction,
190
+ * customCtx(async (ctx) => {
191
+ * return {
192
+ * // Throws an exception if the user isn't logged in
193
+ * user: await ctx.runQuery(internal.users.getUser, {});
194
+ * };
195
+ * })
196
+ * );
197
+ *
198
+ * // Using it
199
+ * export const sendUserEmail = myUserAction({
200
+ * args: { subject: v.string(), body: v.string() },
201
+ * handler: async (ctx, args) => {
202
+ * await sendEmail(ctx.user.email, args.subject, args.body);
203
+ * },
204
+ * });
205
+ *
206
+ * @param action The action to be modified. Usually `action` or `internalAction`
207
+ * from `_generated/server`.
208
+ * @param mod The modifier to be applied to the action, changing ctx and args.
209
+ * @returns A new action builder to define queries with modified ctx and args.
210
+ */
211
+ export declare function customAction<ModArgsValidator extends PropertyValidators, ModCtx extends Record<string, any>, ModMadeArgs extends Record<string, any>, Visibility extends FunctionVisibility, DataModel extends GenericDataModel>(action: ActionBuilder<DataModel, Visibility>, mod: Mod<GenericActionCtx<DataModel>, ModArgsValidator, ModCtx, ModMadeArgs>): CustomBuilder<"action", ModArgsValidator, ModCtx, ModMadeArgs, GenericActionCtx<DataModel>, Visibility>;
212
+ /**
213
+ * A Convex function (query, mutation, or action) to be registered for the API.
214
+ * Convenience to specify the registration type based on function type.
215
+ */
216
+ type Registration<FuncType extends "query" | "mutation" | "action", Visibility extends FunctionVisibility, Args extends DefaultFunctionArgs, Output> = {
217
+ query: RegisteredQuery<Visibility, Args, Output>;
218
+ mutation: RegisteredMutation<Visibility, Args, Output>;
219
+ action: RegisteredAction<Visibility, Args, Output>;
220
+ }[FuncType];
221
+ /**
222
+ * A builder that customizes a Convex function using argument validation.
223
+ * e.g. `query({ args: {}, handler: async (ctx, args) => {} })`
224
+ */
225
+ type ValidatedBuilder<FuncType extends "query" | "mutation" | "action", ModArgsValidator extends PropertyValidators, ModCtx extends Record<string, any>, ModMadeArgs extends Record<string, any>, InputCtx, Visibility extends FunctionVisibility> = <ExistingArgsValidator extends PropertyValidators, Output>(fn: {
226
+ args: ExistingArgsValidator;
227
+ handler: (ctx: InputCtx & ModCtx, args: ObjectType<ExistingArgsValidator> & ModMadeArgs) => Output;
228
+ }) => Registration<FuncType, Visibility, ObjectType<ExistingArgsValidator & ModArgsValidator>, Output>;
229
+ /**
230
+ * A builder that customizes a Convex function which doesn't validate arguments.
231
+ * e.g. `query(async (ctx, args) => {})`
232
+ * or `query({ handler: async (ctx, args) => {} })`
233
+ */
234
+ type UnvalidatedBuilder<FuncType extends "query" | "mutation" | "action", ModCtx extends Record<string, any>, ModMadeArgs extends Record<string, any>, InputCtx, Visibility extends FunctionVisibility> = <Output, ExistingArgs extends DefaultFunctionArgs = DefaultFunctionArgs>(fn: UnvalidatedFunction<InputCtx & ModCtx, [
235
+ ExistingArgs & ModMadeArgs
236
+ ], Output>) => Registration<FuncType, Visibility, ExistingArgs, Output>;
237
+ /**
238
+ * A builder that customizes a Convex function, whether or not it validates
239
+ * arguments. If the customization requires arguments, however, the resulting
240
+ * builder will require argument validation too.
241
+ */
242
+ type CustomBuilder<FuncType extends "query" | "mutation" | "action", ModArgsValidator extends PropertyValidators, ModCtx extends Record<string, any>, ModMadeArgs extends Record<string, any>, InputCtx, Visibility extends FunctionVisibility> = ModArgsValidator extends EmptyObject ? ValidatedBuilder<FuncType, ModArgsValidator, ModCtx, ModMadeArgs, InputCtx, Visibility> & UnvalidatedBuilder<FuncType, ModCtx, ModMadeArgs, InputCtx, Visibility> : ValidatedBuilder<FuncType, ModArgsValidator, ModCtx, ModMadeArgs, InputCtx, Visibility>;
243
+ type EmptyObject = Record<string, never>;
244
+ type DefaultFunctionArgs = Record<string, unknown>;
245
+ export {};
@@ -0,0 +1,295 @@
1
+ /**
2
+ * A helper for defining a Mod when your mod doesn't need to add or remove
3
+ * anything from args.
4
+ * @param mod A function that defines how to modify the ctx.
5
+ * @returns A ctx delta to be applied to the original ctx.
6
+ */
7
+ export function customCtx(mod) {
8
+ return {
9
+ args: {},
10
+ input: async (ctx) => ({ ctx: await mod(ctx), args: {} }),
11
+ };
12
+ }
13
+ /**
14
+ * A Mod that doesn't add or remove any context or args.
15
+ */
16
+ export const NoOp = {
17
+ args: {},
18
+ input() {
19
+ return { args: {}, ctx: {} };
20
+ },
21
+ };
22
+ /**
23
+ * customQuery helps define custom behavior on top of `query` or `internalQuery`
24
+ * by passing a function that modifies the ctx and args.
25
+ *
26
+ * Example usage:
27
+ * ```js
28
+ * const myQueryBuilder = customQuery(query, {
29
+ * args: { sessionId: v.id("sessions") },
30
+ * input: async (ctx, args) => {
31
+ * const user = await getUserOrNull(ctx);
32
+ * const session = await db.get(sessionId);
33
+ * const db = wrapDatabaseReader({ user }, ctx.db, rlsRules);
34
+ * return { ctx: { db, user, session }, args: {} };
35
+ * },
36
+ * });
37
+ *
38
+ * // Using the custom builder
39
+ * export const getSomeData = myQueryBuilder({
40
+ * args: { someArg: v.string() },
41
+ * handler: async (ctx, args) => {
42
+ * const { db, user, session, scheduler } = ctx;
43
+ * const { someArg } = args;
44
+ * // ...
45
+ * }
46
+ * });
47
+ * ```
48
+ *
49
+ * Simple usage only modifying ctx:
50
+ * ```js
51
+ * const myInternalQuery = customQuery(
52
+ * internalQuery,
53
+ * customCtx(async (ctx) => {
54
+ * return {
55
+ * // Throws an exception if the user isn't logged in
56
+ * user: await getUserByTokenIdentifier(ctx),
57
+ * };
58
+ * })
59
+ * );
60
+ *
61
+ * // Using it
62
+ * export const getUser = myInternalQuery({
63
+ * args: {},
64
+ * handler: async (ctx, args) => {
65
+ * return ctx.user;
66
+ * },
67
+ * });
68
+ *
69
+ * @param query The query to be modified. Usually `query` or `internalQuery`
70
+ * from `_generated/server`.
71
+ * @param mod The modifier to be applied to the query, changing ctx and args.
72
+ * @returns A new query builder to define queries with modified ctx and args.
73
+ */
74
+ export function customQuery(query, mod) {
75
+ function customQueryBuilder(fn) {
76
+ // Looking forward to when input / args / ... are optional
77
+ const inputMod = mod.input ?? NoOp.input;
78
+ const inputArgs = mod.args ?? NoOp.args;
79
+ if ("args" in fn) {
80
+ return query({
81
+ args: {
82
+ ...fn.args,
83
+ ...inputArgs,
84
+ },
85
+ handler: async (ctx, allArgs) => {
86
+ const { split, rest } = splitArgs(inputArgs, allArgs);
87
+ const added = await inputMod(ctx, split);
88
+ return await fn.handler({ ...ctx, ...added.ctx }, { ...rest, ...added.args });
89
+ },
90
+ });
91
+ }
92
+ if (Object.keys(inputArgs).length > 0) {
93
+ throw new Error("If you're using a custom function with arguments for the input " +
94
+ "modifier, you must declare the arguments for the function too.");
95
+ }
96
+ const handler = fn.handler ?? fn;
97
+ return query({
98
+ handler: async (ctx, args) => {
99
+ const { ctx: modCtx } = await inputMod(ctx, args);
100
+ return await handler({ ...ctx, ...modCtx }, args);
101
+ },
102
+ });
103
+ }
104
+ return customQueryBuilder;
105
+ }
106
+ /**
107
+ * customMutation helps define custom behavior on top of `mutation`
108
+ * or `internalMutation` by passing a function that modifies the ctx and args.
109
+ *
110
+ * Example usage:
111
+ * ```js
112
+ * const myMutationBuilder = customMutation(mutation, {
113
+ * args: { sessionId: v.id("sessions") },
114
+ * input: async (ctx, args) => {
115
+ * const user = await getUserOrNull(ctx);
116
+ * const session = await db.get(sessionId);
117
+ * const db = wrapDatabaseReader({ user }, ctx.db, rlsRules);
118
+ * return { ctx: { db, user, session }, args: {} };
119
+ * },
120
+ * });
121
+ *
122
+ * // Using the custom builder
123
+ * export const setSomeData = myMutationBuilder({
124
+ * args: { someArg: v.string() },
125
+ * handler: async (ctx, args) => {
126
+ * const { db, user, session, scheduler } = ctx;
127
+ * const { someArg } = args;
128
+ * // ...
129
+ * }
130
+ * });
131
+ * ```
132
+ *
133
+ * Simple usage only modifying ctx:
134
+ * ```js
135
+ * const myUserMutation = customMutation(
136
+ * mutation,
137
+ * customCtx(async (ctx) => {
138
+ * return {
139
+ * // Throws an exception if the user isn't logged in
140
+ * user: await getUserByTokenIdentifier(ctx),
141
+ * };
142
+ * })
143
+ * );
144
+ *
145
+ * // Using it
146
+ * export const setMyName = myUserMutation({
147
+ * args: { name: v.string() },
148
+ * handler: async (ctx, args) => {
149
+ * await ctx.db.patch(ctx.user._id, { name: args.name });
150
+ * },
151
+ * });
152
+ *
153
+ * @param mutation The mutation to be modified. Usually `mutation` or `internalMutation`
154
+ * from `_generated/server`.
155
+ * @param mod The modifier to be applied to the mutation, changing ctx and args.
156
+ * @returns A new mutation builder to define queries with modified ctx and args.
157
+ */
158
+ export function customMutation(mutation, mod) {
159
+ function customMutationBuilder(fn) {
160
+ // Looking forward to when input / args / ... are optional
161
+ const inputMod = mod.input ?? NoOp.input;
162
+ const inputArgs = mod.args ?? NoOp.args;
163
+ if ("args" in fn) {
164
+ return mutation({
165
+ args: {
166
+ ...fn.args,
167
+ ...inputArgs,
168
+ },
169
+ handler: async (ctx, allArgs) => {
170
+ const { split, rest } = splitArgs(inputArgs, allArgs);
171
+ const added = await inputMod(ctx, split);
172
+ return await fn.handler({ ...ctx, ...added.ctx }, { ...rest, ...added.args });
173
+ },
174
+ });
175
+ }
176
+ if (Object.keys(inputArgs).length > 0) {
177
+ throw new Error("If you're using a custom function with arguments for the input " +
178
+ "modifier, you must declare the arguments for the function too.");
179
+ }
180
+ const handler = fn.handler ?? fn;
181
+ return mutation({
182
+ handler: async (ctx, args) => {
183
+ const { ctx: modCtx } = await inputMod(ctx, args);
184
+ return await handler({ ...ctx, ...modCtx }, args);
185
+ },
186
+ });
187
+ }
188
+ return customMutationBuilder;
189
+ }
190
+ /**
191
+ * customAction helps define custom behavior on top of `action`
192
+ * or `internalAction` by passing a function that modifies the ctx and args.
193
+ *
194
+ * Example usage:
195
+ * ```js
196
+ * const myActionBuilder = customAction(action, {
197
+ * args: { secretKey: v.string() },
198
+ * input: async (ctx, args) => {
199
+ * // Very basic authorization, e.g. from trusted backends.
200
+ * if (args.secretKey !== process.env.SECRET_KEY) {
201
+ * throw new Error("Invalid secret key");
202
+ * }
203
+ * const user = await ctx.runQuery(internal.users.getUser, {});
204
+ * return { ctx: { user }, args: {} };
205
+ * },
206
+ * });
207
+ *
208
+ * // Using the custom builder
209
+ * export const runSomeAction = myActionBuilder({
210
+ * args: { someArg: v.string() },
211
+ * handler: async (ctx, args) => {
212
+ * const { user, scheduler } = ctx;
213
+ * const { someArg } = args;
214
+ * // ...
215
+ * }
216
+ * });
217
+ * ```
218
+ *
219
+ * Simple usage only modifying ctx:
220
+ * ```js
221
+ * const myUserAction = customAction(
222
+ * internalAction,
223
+ * customCtx(async (ctx) => {
224
+ * return {
225
+ * // Throws an exception if the user isn't logged in
226
+ * user: await ctx.runQuery(internal.users.getUser, {});
227
+ * };
228
+ * })
229
+ * );
230
+ *
231
+ * // Using it
232
+ * export const sendUserEmail = myUserAction({
233
+ * args: { subject: v.string(), body: v.string() },
234
+ * handler: async (ctx, args) => {
235
+ * await sendEmail(ctx.user.email, args.subject, args.body);
236
+ * },
237
+ * });
238
+ *
239
+ * @param action The action to be modified. Usually `action` or `internalAction`
240
+ * from `_generated/server`.
241
+ * @param mod The modifier to be applied to the action, changing ctx and args.
242
+ * @returns A new action builder to define queries with modified ctx and args.
243
+ */
244
+ export function customAction(action, mod) {
245
+ function customActionBuilder(fn) {
246
+ // Looking forward to when input / args / ... are optional
247
+ const inputMod = mod.input ?? NoOp.input;
248
+ const inputArgs = mod.args ?? NoOp.args;
249
+ if ("args" in fn) {
250
+ return action({
251
+ args: {
252
+ ...fn.args,
253
+ ...inputArgs,
254
+ },
255
+ handler: async (ctx, allArgs) => {
256
+ const { split, rest } = splitArgs(inputArgs, allArgs);
257
+ const added = await inputMod(ctx, split);
258
+ return await fn.handler({ ...ctx, ...added.ctx }, { ...rest, ...added.args });
259
+ },
260
+ });
261
+ }
262
+ if (Object.keys(inputArgs).length > 0) {
263
+ throw new Error("If you're using a custom function with arguments for the input " +
264
+ "modifier, you must declare the arguments for the function too.");
265
+ }
266
+ const handler = fn.handler ?? fn;
267
+ return action({
268
+ handler: async (ctx, args) => {
269
+ const { ctx: modCtx } = await inputMod(ctx, args);
270
+ return await handler({ ...ctx, ...modCtx }, args);
271
+ },
272
+ });
273
+ }
274
+ return customActionBuilder;
275
+ }
276
+ /**
277
+ *
278
+ * @param splitArgsValidator The args that should be split out from the rest.
279
+ * As an object mapping arg names to validators (v.* from convex/values).
280
+ * @param args The arguments to a function, including values to be split out.
281
+ * @returns The args split into two objects: `split` and `rest` based on keys.
282
+ */
283
+ function splitArgs(splitArgsValidator, args) {
284
+ const rest = {};
285
+ const split = {};
286
+ for (const arg in args) {
287
+ if (arg in splitArgsValidator) {
288
+ split[arg] = args[arg];
289
+ }
290
+ else {
291
+ rest[arg] = args[arg];
292
+ }
293
+ }
294
+ return { split, rest };
295
+ }
File without changes
@@ -0,0 +1 @@
1
+ "use strict";