Skip to content

Commit c7d3714

Browse files
ldanilekConvex, Inc.
authored andcommitted
wrap function handlers to support independently exported handler (#30078)
i've been doing this pattern so i can call my handler from other functions and from unit tests ```ts export async function listHandler(ctx: QueryCtx) { return await ctx.db.query("messages").collect(); }; export const list = query({ handler: listHandler, }); ``` unfortunately, this makes both `list` and `listHandler` get analyzed as callable queries -- in the dashboard function view, the function runner, `npx convex run`, in components codegen, etc. l To fix, we can avoid `query`, `mutation`, and friends modifying the handler in place. We re-wrap in a separate function, and attach the handler as `.handler` on this separate function so analyze can get source maps from it. Tested that the dashboard still finds the function just fine. <img width="1321" alt="Screenshot 2024-09-23 at 5 30 25 PM" src="https://github.com/user-attachments/assets/c6e37cc1-110b-4097-b817-661f1917e62d"> GitOrigin-RevId: ff0ecfdd44bf40f38ef9c377d86694b3747c15a4
1 parent aef1e1b commit c7d3714

File tree

4 files changed

+80
-13
lines changed

4 files changed

+80
-13
lines changed

crates/isolate/src/environment/analyze.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -623,19 +623,33 @@ fn udf_analyze<RT: Runtime>(
623623
},
624624
};
625625

626+
let handler_str = strings::_handler.create(scope)?;
627+
let handler = match function.get(scope, handler_str.into()) {
628+
Some(handler_value) if handler_value.is_function() => {
629+
let handler: v8::Local<v8::Function> = handler_value.try_into()?;
630+
handler
631+
},
632+
Some(handler_value) if handler_value.is_undefined() => function,
633+
Some(_) => {
634+
let message = format!("{module_path:?}:{property_name}.handler is not a function.");
635+
return Ok(Err(JsError::from_message(message)));
636+
},
637+
None => function,
638+
};
639+
626640
// These are originally zero-indexed, so we just add 1
627-
let lineno = function
641+
let lineno = handler
628642
.get_script_line_number()
629643
.ok_or_else(|| anyhow!("Failed to get function line number"))?
630644
+ 1;
631-
let linecol = function
645+
let linecol = handler
632646
.get_script_column_number()
633647
.ok_or_else(|| anyhow!("Failed to get function column number"))?
634648
+ 1;
635649

636650
// Get the appropriate source map to look in
637651
let (fn_source_map, fn_canon_path) = {
638-
let resource_name_val = function
652+
let resource_name_val = handler
639653
.get_script_origin()
640654
.resource_name()
641655
.ok_or(anyhow!("resource_name was None"))?;
@@ -853,19 +867,33 @@ fn http_analyze<RT: Runtime>(
853867
return routes_error(format!("arr[{}][2] not an HttpAction", i).as_str());
854868
};
855869

870+
let handler_str = strings::_handler.create(scope)?;
871+
let handler = match function.get(scope, handler_str.into()) {
872+
Some(handler_value) if handler_value.is_function() => {
873+
let handler: v8::Local<v8::Function> = handler_value.try_into()?;
874+
handler
875+
},
876+
Some(handler_value) if handler_value.is_undefined() => function,
877+
Some(_) => {
878+
let message = format!("arr[{}][2].handler is not a function", i);
879+
return Ok(Err(JsError::from_message(message)));
880+
},
881+
None => function,
882+
};
883+
856884
// These are originally zero-indexed, so we just add 1
857-
let lineno = function
885+
let lineno = handler
858886
.get_script_line_number()
859887
.ok_or_else(|| anyhow!("Failed to get function line number"))?
860888
+ 1;
861-
let linecol = function
889+
let linecol = handler
862890
.get_script_column_number()
863891
.ok_or_else(|| anyhow!("Failed to get function column number"))?
864892
+ 1;
865893

866894
// Get the appropriate source map to look in
867895
let (fn_source_map, fn_canon_path) = {
868-
let resource_name_val = function
896+
let resource_name_val = handler
869897
.get_script_origin()
870898
.resource_name()
871899
.ok_or(anyhow!("resource_name was None"))?;

crates/isolate/src/strings.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ macro_rules! declare_strings {
5555
// identifier as a string, or explicitly name the string with the `$name =>
5656
// $string` syntax.
5757
declare_strings!(
58+
_handler,
5859
_onInitCallbacks,
5960
Convex,
6061
asyncOp,

npm-packages/convex/src/server/impl/registration_impl.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,13 @@ function exportReturns(functionDefinition: FunctionDefinition) {
173173
export const mutationGeneric: MutationBuilder<any, "public"> = ((
174174
functionDefinition: FunctionDefinition,
175175
) => {
176-
const func = (
176+
const handler = (
177177
typeof functionDefinition === "function"
178178
? functionDefinition
179179
: functionDefinition.handler
180180
) as RegisteredMutation<"public", any, any>;
181+
const func = ((ctx: any, args: any) =>
182+
handler(ctx, args)) as RegisteredMutation<"public", any, any>;
181183

182184
// Helpful runtime check that functions are only be registered once
183185
if (func.isRegistered) {
@@ -190,6 +192,7 @@ export const mutationGeneric: MutationBuilder<any, "public"> = ((
190192
func.invokeMutation = (argsStr) => invokeMutation(func, argsStr);
191193
func.exportArgs = exportArgs(functionDefinition);
192194
func.exportReturns = exportReturns(functionDefinition);
195+
func._handler = handler;
193196
return func;
194197
}) as MutationBuilder<any, "public">;
195198

@@ -209,11 +212,13 @@ export const mutationGeneric: MutationBuilder<any, "public"> = ((
209212
export const internalMutationGeneric: MutationBuilder<any, "internal"> = ((
210213
functionDefinition: FunctionDefinition,
211214
) => {
212-
const func = (
215+
const handler = (
213216
typeof functionDefinition === "function"
214217
? functionDefinition
215218
: functionDefinition.handler
216219
) as RegisteredMutation<"internal", any, any>;
220+
const func = ((ctx: any, args: any) =>
221+
handler(ctx, args)) as RegisteredMutation<"internal", any, any>;
217222

218223
// Helpful runtime check that functions are only be registered once
219224
if (func.isRegistered) {
@@ -226,6 +231,7 @@ export const internalMutationGeneric: MutationBuilder<any, "internal"> = ((
226231
func.invokeMutation = (argsStr) => invokeMutation(func, argsStr);
227232
func.exportArgs = exportArgs(functionDefinition);
228233
func.exportReturns = exportReturns(functionDefinition);
234+
func._handler = handler;
229235
return func;
230236
}) as MutationBuilder<any, "internal">;
231237

@@ -263,11 +269,16 @@ async function invokeQuery<
263269
export const queryGeneric: QueryBuilder<any, "public"> = ((
264270
functionDefinition: FunctionDefinition,
265271
) => {
266-
const func = (
272+
const handler = (
267273
typeof functionDefinition === "function"
268274
? functionDefinition
269275
: functionDefinition.handler
270276
) as RegisteredQuery<"public", any, any>;
277+
const func = ((ctx: any, args: any) => handler(ctx, args)) as RegisteredQuery<
278+
"public",
279+
any,
280+
any
281+
>;
271282

272283
// Helpful runtime check that functions are only be registered once
273284
if (func.isRegistered) {
@@ -280,6 +291,7 @@ export const queryGeneric: QueryBuilder<any, "public"> = ((
280291
func.invokeQuery = (argsStr) => invokeQuery(func, argsStr);
281292
func.exportArgs = exportArgs(functionDefinition);
282293
func.exportReturns = exportReturns(functionDefinition);
294+
func._handler = handler;
283295
return func;
284296
}) as QueryBuilder<any, "public">;
285297

@@ -299,11 +311,16 @@ export const queryGeneric: QueryBuilder<any, "public"> = ((
299311
export const internalQueryGeneric: QueryBuilder<any, "internal"> = ((
300312
functionDefinition: FunctionDefinition,
301313
) => {
302-
const func = (
314+
const handler = (
303315
typeof functionDefinition === "function"
304316
? functionDefinition
305317
: functionDefinition.handler
306318
) as RegisteredQuery<"internal", any, any>;
319+
const func = ((ctx: any, args: any) => handler(ctx, args)) as RegisteredQuery<
320+
"internal",
321+
any,
322+
any
323+
>;
307324

308325
// Helpful runtime check that functions are only be registered once
309326
if (func.isRegistered) {
@@ -316,6 +333,7 @@ export const internalQueryGeneric: QueryBuilder<any, "internal"> = ((
316333
func.invokeQuery = (argsStr) => invokeQuery(func as any, argsStr);
317334
func.exportArgs = exportArgs(functionDefinition);
318335
func.exportReturns = exportReturns(functionDefinition);
336+
func._handler = handler;
319337
return func;
320338
}) as QueryBuilder<any, "internal">;
321339

@@ -349,11 +367,13 @@ async function invokeAction<
349367
export const actionGeneric: ActionBuilder<any, "public"> = ((
350368
functionDefinition: FunctionDefinition,
351369
) => {
352-
const func = (
370+
const handler = (
353371
typeof functionDefinition === "function"
354372
? functionDefinition
355373
: functionDefinition.handler
356374
) as RegisteredAction<"public", any, any>;
375+
const func = ((ctx: any, args: any) =>
376+
handler(ctx, args)) as RegisteredAction<"public", any, any>;
357377

358378
// Helpful runtime check that functions are only be registered once
359379
if (func.isRegistered) {
@@ -367,6 +387,7 @@ export const actionGeneric: ActionBuilder<any, "public"> = ((
367387
invokeAction(func, requestId, argsStr);
368388
func.exportArgs = exportArgs(functionDefinition);
369389
func.exportReturns = exportReturns(functionDefinition);
390+
func._handler = handler;
370391
return func;
371392
}) as ActionBuilder<any, "public">;
372393

@@ -384,11 +405,13 @@ export const actionGeneric: ActionBuilder<any, "public"> = ((
384405
export const internalActionGeneric: ActionBuilder<any, "internal"> = ((
385406
functionDefinition: FunctionDefinition,
386407
) => {
387-
const func = (
408+
const handler = (
388409
typeof functionDefinition === "function"
389410
? functionDefinition
390411
: functionDefinition.handler
391412
) as RegisteredAction<"internal", any, any>;
413+
const func = ((ctx: any, args: any) =>
414+
handler(ctx, args)) as RegisteredAction<"internal", any, any>;
392415

393416
// Helpful runtime check that functions are only be registered once
394417
if (func.isRegistered) {
@@ -402,6 +425,7 @@ export const internalActionGeneric: ActionBuilder<any, "internal"> = ((
402425
invokeAction(func, requestId, argsStr);
403426
func.exportArgs = exportArgs(functionDefinition);
404427
func.exportReturns = exportReturns(functionDefinition);
428+
func._handler = handler;
405429
return func;
406430
}) as ActionBuilder<any, "internal">;
407431

@@ -437,7 +461,9 @@ export const httpActionGeneric = (
437461
request: Request,
438462
) => Promise<Response>,
439463
): PublicHttpAction => {
440-
const q = func as unknown as PublicHttpAction;
464+
const handler = func as unknown as PublicHttpAction;
465+
const q = ((ctx: any, request: any) =>
466+
handler(ctx, request)) as PublicHttpAction;
441467
// Helpful runtime check that functions are only be registered once
442468
if (q.isRegistered) {
443469
throw new Error("Function registered twice " + func);
@@ -446,6 +472,7 @@ export const httpActionGeneric = (
446472
q.isRegistered = true;
447473
q.isHttp = true;
448474
q.invokeHttpAction = (request) => invokeHttpAction(func as any, request);
475+
q._handler = func;
449476
return q;
450477
};
451478

npm-packages/convex/src/server/registration.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,9 @@ export type RegisteredMutation<
363363

364364
/** @internal */
365365
exportReturns(): string;
366+
367+
/** @internal */
368+
_handler: (ctx: GenericMutationCtx<any>, args: Args) => Returns;
366369
} & VisibilityProperties<Visibility>;
367370

368371
/**
@@ -392,6 +395,9 @@ export type RegisteredQuery<
392395

393396
/** @internal */
394397
exportReturns(): string;
398+
399+
/** @internal */
400+
_handler: (ctx: GenericQueryCtx<any>, args: Args) => Returns;
395401
} & VisibilityProperties<Visibility>;
396402

397403
/**
@@ -421,6 +427,9 @@ export type RegisteredAction<
421427

422428
/** @internal */
423429
exportReturns(): string;
430+
431+
/** @internal */
432+
_handler: (ctx: GenericActionCtx<any>, args: Args) => Returns;
424433
} & VisibilityProperties<Visibility>;
425434

426435
/**
@@ -438,6 +447,8 @@ export type PublicHttpAction = {
438447

439448
/** @internal */
440449
invokeHttpAction(request: Request): Promise<Response>;
450+
/** @internal */
451+
_handler: (ctx: GenericActionCtx<any>, request: Request) => Promise<Response>;
441452
};
442453

443454
/**

0 commit comments

Comments
 (0)