Skip to content

Commit fa1ed98

Browse files
committed
Merge branch 'customFunctionExtraParams'
2 parents e4e4830 + 3f9efd6 commit fa1ed98

File tree

5 files changed

+160
-39
lines changed

5 files changed

+160
-39
lines changed

convex/testingFunctions.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ export const testingQuery = customQuery(query, {
2121

2222
export const testingMutation = customMutation(mutation, {
2323
args: {},
24-
input: async (_ctx, _args) => {
24+
input: async (_ctx, _args, { devOnly }: { devOnly: boolean }) => {
2525
if (process.env.IS_TEST === undefined) {
2626
throw new Error(
2727
"Calling a test only function in an unexpected environment",
2828
);
2929
}
30+
if (devOnly && process.env.IS_PROD) {
31+
throw new Error("This function is only available in development");
32+
}
3033
return { ctx: {}, args: {} };
3134
},
3235
});
@@ -43,13 +46,16 @@ export const testingAction = customAction(action, {
4346
},
4447
});
4548

46-
export const clearAll = testingMutation(async ({ db, scheduler, storage }) => {
47-
for (const table of Object.keys(schema.tables)) {
48-
const docs = await db.query(table as any).collect();
49-
await Promise.all(docs.map((doc) => db.delete(doc._id)));
50-
}
51-
const scheduled = await db.system.query("_scheduled_functions").collect();
52-
await Promise.all(scheduled.map((s) => scheduler.cancel(s._id)));
53-
const storedFiles = await db.system.query("_storage").collect();
54-
await Promise.all(storedFiles.map((s) => storage.delete(s._id)));
49+
export const clearAll = testingMutation({
50+
devOnly: true,
51+
handler: async ({ db, scheduler, storage }) => {
52+
for (const table of Object.keys(schema.tables)) {
53+
const docs = await db.query(table as any).collect();
54+
await Promise.all(docs.map((doc) => db.delete(doc._id)));
55+
}
56+
const scheduled = await db.system.query("_scheduled_functions").collect();
57+
await Promise.all(scheduled.map((s) => scheduler.cancel(s._id)));
58+
const storedFiles = await db.system.query("_storage").collect();
59+
await Promise.all(storedFiles.map((s) => storage.delete(s._id)));
60+
},
5561
});

packages/convex-helpers/README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ See the associated [Stack Post](https://stack.convex.dev/custom-functions)
5252

5353
For example:
5454

55-
```js
55+
```ts
5656
import { customQuery } from "convex-helpers/server/customFunctions.js";
5757

5858
const myQueryBuilder = customQuery(query, {
@@ -75,6 +75,34 @@ export const getSomeData = myQueryBuilder({
7575
});
7676
```
7777

78+
### Taking in extra arguments
79+
80+
You can take in extra arguments to a custom function by specifying the type of a third `input` arg.
81+
82+
```ts
83+
const myQueryBuilder = customQuery(query, {
84+
args: {},
85+
input: async (ctx, args, { role }: { role: "admin" | "user" }) => {
86+
const user = await getUser(ctx);
87+
if (role === "admin" && user.role !== "admin") {
88+
throw new Error("You are not an admin");
89+
}
90+
if (role === "user" && !user) {
91+
throw new Error("You must be logged in to access this query");
92+
}
93+
return { ctx: { user }, args: {} };
94+
},
95+
});
96+
97+
const myAdminQuery = myQueryBuilder({
98+
role: "admin",
99+
args: {},
100+
handler: async (ctx, args) => {
101+
// ...
102+
},
103+
});
104+
```
105+
78106
## Relationship helpers
79107

80108
Traverse database relationships without all the query boilerplate.

packages/convex-helpers/server/customFunctions.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,30 @@ customAction(
147147
customCtx((_ctx) => ({})),
148148
) satisfies typeof action;
149149

150+
customQuery({} as any, {
151+
args: {},
152+
input: async () => ({
153+
ctx: {},
154+
args: {},
155+
}),
156+
}) satisfies typeof query;
157+
158+
customMutation(mutation, {
159+
args: {},
160+
input: async () => ({
161+
ctx: {},
162+
args: {},
163+
}),
164+
}) satisfies typeof mutation;
165+
166+
customAction(action, {
167+
args: {},
168+
input: async () => ({
169+
ctx: {},
170+
args: {},
171+
}),
172+
}) satisfies typeof action;
173+
150174
/**
151175
* Testing custom function modifications.
152176
*/
@@ -360,6 +384,25 @@ export const outerRemoved = outerRemover({
360384
},
361385
});
362386

387+
/**
388+
* Adding extra args to `input`
389+
*/
390+
const extraArgQueryBuilder = customQuery(query, {
391+
args: { a: v.string() },
392+
input: async (_ctx, args, { extraArg }: { extraArg: string }) => ({
393+
ctx: { extraArg },
394+
args,
395+
}),
396+
});
397+
export const extraArgQuery = extraArgQueryBuilder({
398+
args: {},
399+
extraArg: "foo",
400+
handler: async (ctx, args) => {
401+
return { ctxA: ctx.extraArg };
402+
},
403+
});
404+
queryMatches(extraArgQuery, {}, { ctxA: "foo" });
405+
363406
/**
364407
* Test helpers
365408
*/
@@ -389,6 +432,7 @@ const testApi: ApiFromModules<{
389432
create: typeof create;
390433
outerAdds: typeof outerAdds;
391434
outerRemoved: typeof outerRemoved;
435+
extraArgQuery: typeof extraArgQuery;
392436
};
393437
}>["fns"] = anyApi["customFunctions.test"] as any;
394438

@@ -568,3 +612,12 @@ describe("nested custom functions", () => {
568612
).rejects.toThrow("Validator error: Expected `string`");
569613
});
570614
});
615+
616+
describe("extra args", () => {
617+
test("add extra args", async () => {
618+
const t = convexTest(schema, modules);
619+
expect(await t.query(testApi.extraArgQuery, { a: "foo" })).toMatchObject({
620+
ctxA: "foo",
621+
});
622+
});
623+
});

packages/convex-helpers/server/customFunctions.ts

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@ export type Mod<
5151
ModArgsValidator extends PropertyValidators,
5252
ModCtx extends Record<string, any>,
5353
ModMadeArgs extends Record<string, any>,
54+
ExtraArgs extends Record<string, any> = Record<string, any>,
5455
> = {
5556
args: ModArgsValidator;
5657
input: (
5758
ctx: Ctx,
5859
args: ObjectType<ModArgsValidator>,
60+
extra: ExtraArgs,
5961
) =>
6062
| Promise<{ ctx: ModCtx; args: ModMadeArgs }>
6163
| { ctx: ModCtx; args: ModMadeArgs };
@@ -70,12 +72,13 @@ export type Mod<
7072
export function customCtx<
7173
InCtx extends Record<string, any>,
7274
OutCtx extends Record<string, any>,
75+
ExtraArgs extends Record<string, any> = Record<string, any>,
7376
>(
74-
mod: (original: InCtx) => Promise<OutCtx> | OutCtx,
75-
): Mod<InCtx, Record<string, never>, OutCtx, Record<string, never>> {
77+
mod: (original: InCtx, extra: ExtraArgs) => Promise<OutCtx> | OutCtx,
78+
): Mod<InCtx, Record<string, never>, OutCtx, Record<string, never>, ExtraArgs> {
7679
return {
7780
args: {},
78-
input: async (ctx) => ({ ctx: await mod(ctx), args: {} }),
81+
input: async (ctx, _, extra) => ({ ctx: await mod(ctx, extra), args: {} }),
7982
};
8083
}
8184

@@ -147,17 +150,25 @@ export function customQuery<
147150
ModMadeArgs extends Record<string, any>,
148151
Visibility extends FunctionVisibility,
149152
DataModel extends GenericDataModel,
153+
ExtraArgs extends Record<string, any> = Record<string, any>,
150154
>(
151155
query: QueryBuilder<DataModel, Visibility>,
152-
mod: Mod<GenericQueryCtx<DataModel>, ModArgsValidator, ModCtx, ModMadeArgs>,
156+
mod: Mod<
157+
GenericQueryCtx<DataModel>,
158+
ModArgsValidator,
159+
ModCtx,
160+
ModMadeArgs,
161+
ExtraArgs
162+
>,
153163
) {
154164
return customFnBuilder(query, mod) as CustomBuilder<
155165
"query",
156166
ModArgsValidator,
157167
ModCtx,
158168
ModMadeArgs,
159169
GenericQueryCtx<DataModel>,
160-
Visibility
170+
Visibility,
171+
ExtraArgs
161172
>;
162173
}
163174

@@ -219,13 +230,15 @@ export function customMutation<
219230
ModMadeArgs extends Record<string, any>,
220231
Visibility extends FunctionVisibility,
221232
DataModel extends GenericDataModel,
233+
ExtraArgs extends Record<string, any> = Record<string, any>,
222234
>(
223235
mutation: MutationBuilder<DataModel, Visibility>,
224236
mod: Mod<
225237
GenericMutationCtx<DataModel>,
226238
ModArgsValidator,
227239
ModCtx,
228-
ModMadeArgs
240+
ModMadeArgs,
241+
ExtraArgs
229242
>,
230243
) {
231244
return customFnBuilder(mutation, mod) as CustomBuilder<
@@ -234,7 +247,8 @@ export function customMutation<
234247
ModCtx,
235248
ModMadeArgs,
236249
GenericMutationCtx<DataModel>,
237-
Visibility
250+
Visibility,
251+
ExtraArgs
238252
>;
239253
}
240254

@@ -298,44 +312,55 @@ export function customAction<
298312
ModMadeArgs extends Record<string, any>,
299313
Visibility extends FunctionVisibility,
300314
DataModel extends GenericDataModel,
315+
ExtraArgs extends Record<string, any> = Record<string, any>,
301316
>(
302317
action: ActionBuilder<DataModel, Visibility>,
303-
mod: Mod<GenericActionCtx<DataModel>, ModArgsValidator, ModCtx, ModMadeArgs>,
318+
mod: Mod<
319+
GenericActionCtx<DataModel>,
320+
ModArgsValidator,
321+
ModCtx,
322+
ModMadeArgs,
323+
ExtraArgs
324+
>,
304325
): CustomBuilder<
305326
"action",
306327
ModArgsValidator,
307328
ModCtx,
308329
ModMadeArgs,
309330
GenericActionCtx<DataModel>,
310-
Visibility
331+
Visibility,
332+
ExtraArgs
311333
> {
312334
return customFnBuilder(action, mod) as CustomBuilder<
313335
"action",
314336
ModArgsValidator,
315337
ModCtx,
316338
ModMadeArgs,
317339
GenericActionCtx<DataModel>,
318-
Visibility
340+
Visibility,
341+
ExtraArgs
319342
>;
320343
}
321344

322345
function customFnBuilder(
323346
builder: (args: any) => any,
324-
mod: Mod<any, any, any, any>,
347+
mod: Mod<any, any, any, any, any>,
325348
) {
326349
// Looking forward to when input / args / ... are optional
327350
const inputMod = mod.input ?? NoOp.input;
328351
const inputArgs = mod.args ?? NoOp.args;
329352
return function customBuilder(fn: any): any {
330-
const handler = fn.handler ?? fn;
331-
if ("args" in fn) {
353+
// N.B.: This is fine if it's a function
354+
const { args, handler = fn, returns, ...extra } = fn;
355+
if (args) {
332356
return builder({
333-
args: addArgs(fn.args, inputArgs),
334-
returns: fn.returns,
357+
args: addArgs(args, inputArgs),
358+
returns,
335359
handler: async (ctx: any, allArgs: any) => {
336360
const added = await inputMod(
337361
ctx,
338362
pick(allArgs, Object.keys(inputArgs)) as any,
363+
extra,
339364
);
340365
const args = omit(allArgs, Object.keys(inputArgs));
341366
return handler({ ...ctx, ...added.ctx }, { ...args, ...added.args });
@@ -351,7 +376,7 @@ function customFnBuilder(
351376
return builder({
352377
returns: fn.returns,
353378
handler: async (ctx: any, args: any) => {
354-
const added = await inputMod(ctx, args);
379+
const added = await inputMod(ctx, args, extra);
355380
return handler({ ...ctx, ...added.ctx }, { ...args, ...added.args });
356381
},
357382
});
@@ -430,6 +455,7 @@ export type CustomBuilder<
430455
ModMadeArgs extends Record<string, any>,
431456
InputCtx,
432457
Visibility extends FunctionVisibility,
458+
ExtraArgs extends Record<string, any>,
433459
> = {
434460
<
435461
ArgsValidator extends PropertyValidators | void | Validator<any, any, any>,
@@ -439,14 +465,18 @@ export type CustomBuilder<
439465
ArgsArrayForOptionalValidator<ArgsValidator> = DefaultArgsForOptionalValidator<ArgsValidator>,
440466
>(
441467
func:
442-
| {
468+
| ({
443469
args?: ArgsValidator;
444470
returns?: ReturnsValidator;
445471
handler: (
446472
ctx: Overwrite<InputCtx, ModCtx>,
447473
...args: ArgsForHandlerType<OneOrZeroArgs, ModMadeArgs>
448474
) => ReturnValue;
449-
}
475+
} & {
476+
[key in keyof ExtraArgs as key extends "args" | "returns" | "handler"
477+
? never
478+
: key]: ExtraArgs[key];
479+
})
450480
| {
451481
(
452482
ctx: Overwrite<InputCtx, ModCtx>,
@@ -474,9 +504,10 @@ export type CustomCtx<Builder> =
474504
infer ModCtx,
475505
any,
476506
infer InputCtx,
507+
any,
477508
any
478509
>
479510
? Overwrite<InputCtx, ModCtx>
480511
: never;
481512

482-
type Overwrite<T, U> = Omit<T, keyof U> & U;
513+
type Overwrite<T, U> = keyof U extends never ? T : Omit<T, keyof U> & U;

0 commit comments

Comments
 (0)