Skip to content

Commit daff7a8

Browse files
emmaling27Convex, Inc.
authored and
Convex, Inc.
committed
Allow component functions to be called from node actions (#29821)
This PR adds support for calling component queries, mutations, actions, and scheduling component functions from node actions. GitOrigin-RevId: 81f1f22055b6ae0911144fbcb7f2a5b6e82e8cc7
1 parent 07baa23 commit daff7a8

File tree

5 files changed

+104
-41
lines changed

5 files changed

+104
-41
lines changed

crates/application/src/lib.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ use common::{
4343
ComponentId,
4444
ComponentPath,
4545
PublicFunctionPath,
46+
Reference,
47+
Resource,
4648
},
4749
document::{
4850
DocumentUpdate,
@@ -167,6 +169,7 @@ use model::{
167169
config::ComponentConfigModel,
168170
handles::FunctionHandlesModel,
169171
types::ProjectConfig,
172+
ComponentsModel,
170173
},
171174
config::{
172175
module_loader::ModuleLoader,
@@ -2755,6 +2758,41 @@ impl<RT: Runtime> Application<RT> {
27552758
FunctionHandlesModel::new(&mut tx).lookup(handle).await
27562759
}
27572760

2761+
pub async fn canonicalized_function_path(
2762+
&self,
2763+
identity: Identity,
2764+
component_id: ComponentId,
2765+
path: Option<String>,
2766+
reference: Option<String>,
2767+
) -> anyhow::Result<CanonicalizedComponentFunctionPath> {
2768+
let reference = match (path, reference) {
2769+
(None, None) => anyhow::bail!(ErrorMetadata::bad_request(
2770+
"MissingUdfPathOrFunctionReference",
2771+
"Missing UDF path or function reference. One must be provided."
2772+
)),
2773+
(Some(path), None) => Reference::Function(path.parse()?),
2774+
(None, Some(reference)) => reference.parse()?,
2775+
(Some(_), Some(_)) => anyhow::bail!(ErrorMetadata::bad_request(
2776+
"InvalidUdfPathOrFunctionReference",
2777+
"Both UDF path and function reference provided. Only one must be provided."
2778+
)),
2779+
};
2780+
// Reading from a separate transaction here is safe because the component id to
2781+
// component path mapping is stable.
2782+
let mut tx = self.begin(identity).await?;
2783+
let mut components_model = ComponentsModel::new(&mut tx);
2784+
let resource = components_model
2785+
.resolve(component_id, None, &reference)
2786+
.await?;
2787+
let path = match resource {
2788+
Resource::Function(path) => path,
2789+
Resource::Value(_) | Resource::ResolvedSystemUdf(_) => {
2790+
anyhow::bail!("Resource type not supported for internal query")
2791+
},
2792+
};
2793+
Ok(path)
2794+
}
2795+
27582796
pub fn files_storage(&self) -> Arc<dyn Storage> {
27592797
self.files_storage.clone()
27602798
}

crates/local_backend/src/node_action_callbacks.rs

Lines changed: 41 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@ use axum::{
1616
};
1717
use common::{
1818
components::{
19-
CanonicalizedComponentFunctionPath,
2019
ComponentFunctionPath,
2120
ComponentId,
22-
ComponentPath,
2321
PublicFunctionPath,
2422
},
2523
execution_context::{
@@ -77,15 +75,23 @@ use vector::{
7775

7876
use crate::{
7977
authentication::ExtractAuthenticationToken,
80-
parse::parse_udf_path,
8178
public_api::{
8279
export_value,
83-
UdfPostRequest,
8480
UdfResponse,
8581
},
8682
LocalAppState,
8783
};
8884

85+
#[derive(Deserialize, Debug)]
86+
#[serde(rename_all = "camelCase")]
87+
pub struct NodeCallbackUdfPostRequest {
88+
pub path: Option<String>,
89+
pub reference: Option<String>,
90+
pub args: UdfArgsJson,
91+
92+
pub format: Option<String>,
93+
}
94+
8995
/// This is like `public_query_post`, except it allows calling internal
9096
/// functions as well. This should not be used for any publicly accessible
9197
/// endpoints, and should only be used to support Convex functions calling into
@@ -96,21 +102,21 @@ pub async fn internal_query_post(
96102
State(st): State<LocalAppState>,
97103
ExtractActionIdentity {
98104
identity,
99-
component_id: _,
105+
component_id,
100106
}: ExtractActionIdentity,
101107
ExtractClientVersion(client_version): ExtractClientVersion,
102108
ExtractExecutionContext(context): ExtractExecutionContext,
103-
Json(req): Json<UdfPostRequest>,
109+
Json(req): Json<NodeCallbackUdfPostRequest>,
104110
) -> Result<impl IntoResponse, HttpResponseError> {
105-
let udf_path = parse_udf_path(&req.path)?;
111+
let path = st
112+
.application
113+
.canonicalized_function_path(identity.clone(), component_id, req.path, req.reference)
114+
.await?;
106115
let udf_return = st
107116
.application
108117
.read_only_udf(
109118
context.request_id,
110-
PublicFunctionPath::Component(CanonicalizedComponentFunctionPath {
111-
component: ComponentPath::TODO(),
112-
udf_path,
113-
}),
119+
PublicFunctionPath::Component(path),
114120
req.args.into_arg_vec(),
115121
identity,
116122
FunctionCaller::Action {
@@ -144,21 +150,21 @@ pub async fn internal_mutation_post(
144150
State(st): State<LocalAppState>,
145151
ExtractActionIdentity {
146152
identity,
147-
component_id: _,
153+
component_id,
148154
}: ExtractActionIdentity,
149155
ExtractClientVersion(client_version): ExtractClientVersion,
150156
ExtractExecutionContext(context): ExtractExecutionContext,
151-
Json(req): Json<UdfPostRequest>,
157+
Json(req): Json<NodeCallbackUdfPostRequest>,
152158
) -> Result<impl IntoResponse, HttpResponseError> {
153-
let udf_path = parse_udf_path(&req.path)?;
159+
let path = st
160+
.application
161+
.canonicalized_function_path(identity.clone(), component_id, req.path, req.reference)
162+
.await?;
154163
let udf_result = st
155164
.application
156165
.mutation_udf(
157166
context.request_id,
158-
PublicFunctionPath::Component(CanonicalizedComponentFunctionPath {
159-
component: ComponentPath::TODO(),
160-
udf_path,
161-
}),
167+
PublicFunctionPath::Component(path),
162168
req.args.into_arg_vec(),
163169
identity,
164170
None,
@@ -197,21 +203,21 @@ pub async fn internal_action_post(
197203
State(st): State<LocalAppState>,
198204
ExtractActionIdentity {
199205
identity,
200-
component_id: _,
206+
component_id,
201207
}: ExtractActionIdentity,
202208
ExtractClientVersion(client_version): ExtractClientVersion,
203209
ExtractExecutionContext(context): ExtractExecutionContext,
204-
Json(req): Json<UdfPostRequest>,
210+
Json(req): Json<NodeCallbackUdfPostRequest>,
205211
) -> Result<impl IntoResponse, HttpResponseError> {
206-
let udf_path = parse_udf_path(&req.path)?;
212+
let path = st
213+
.application
214+
.canonicalized_function_path(identity.clone(), component_id, req.path, req.reference)
215+
.await?;
207216
let udf_result = st
208217
.application
209218
.action_udf(
210219
context.request_id,
211-
PublicFunctionPath::Component(CanonicalizedComponentFunctionPath {
212-
component: ComponentPath::TODO(),
213-
udf_path,
214-
}),
220+
PublicFunctionPath::Component(path),
215221
req.args.into_arg_vec(),
216222
identity,
217223
FunctionCaller::Action {
@@ -241,7 +247,8 @@ pub async fn internal_action_post(
241247
#[derive(Deserialize)]
242248
#[serde(rename_all = "camelCase")]
243249
pub struct ScheduleJobRequest {
244-
udf_path: String,
250+
reference: Option<String>,
251+
udf_path: Option<String>,
245252
udf_args: UdfArgsJson,
246253
scheduled_ts: f64,
247254
}
@@ -264,20 +271,21 @@ pub async fn schedule_job(
264271
) -> Result<impl IntoResponse, HttpResponseError> {
265272
let scheduled_ts = UnixTimestamp::from_secs_f64(req.scheduled_ts);
266273
// User might have entered an invalid path, so this is a developer error.
267-
let udf_path = req.udf_path.parse::<CanonicalizedUdfPath>().map_err(|e| {
268-
anyhow::anyhow!(ErrorMetadata::bad_request("InvalidUdfPath", e.to_string()))
269-
})?;
274+
let path = st
275+
.application
276+
.canonicalized_function_path(identity.clone(), component_id, req.udf_path, req.reference)
277+
.await
278+
.map_err(|e| {
279+
anyhow::anyhow!(ErrorMetadata::bad_request("InvalidUdfPath", e.to_string()))
280+
})?;
270281
let udf_args = req.udf_args.into_arg_vec();
271282
let job_id = st
272283
.application
273284
.runner()
274285
.schedule_job(
275286
identity,
276287
component_id,
277-
CanonicalizedComponentFunctionPath {
278-
component: ComponentPath::TODO(),
279-
udf_path,
280-
},
288+
path,
281289
udf_args,
282290
scheduled_ts,
283291
context,

crates/local_backend/src/public_api.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ use crate::{
5151
RouterState,
5252
};
5353

54-
#[derive(Deserialize)]
54+
#[derive(Deserialize, Debug)]
5555
#[serde(rename_all = "camelCase")]
5656
pub struct UdfPostRequest {
5757
pub path: String,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function syscallArgs(
2222
export function getFunctionAddress(functionReference: any) {
2323
// The `run*` syscalls expect either a UDF path at "name" or a serialized
2424
// reference at "reference". Dispatch on `functionReference` to coerce
25-
// it to one ore the other.
25+
// it to one or the other.
2626
let functionAddress;
2727

2828
// Legacy path for passing in UDF paths directly as function references.

npm-packages/node-executor/src/syscalls.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ const STATUS_CODE_BAD_REQUEST = 400;
1414
const STATUS_CODE_UDF_FAILED = 560;
1515

1616
const runFunctionArgs = z.object({
17-
name: z.string(),
17+
name: z.optional(z.string()),
18+
reference: z.optional(z.string()),
1819
args: z.any(),
1920
version: z.string(),
2021
});
@@ -32,7 +33,8 @@ const runFunctionReturn = z.union([
3233
]);
3334

3435
const scheduleSchema = z.object({
35-
name: z.string(),
36+
name: z.optional(z.string()),
37+
reference: z.optional(z.string()),
3638
ts: z.number(),
3739
args: z.any(),
3840
version: z.string(),
@@ -181,7 +183,9 @@ export class SyscallsImpl {
181183
const parsedArgs = argValidator.parse(args);
182184
return parsedArgs;
183185
} catch (e) {
184-
throw new Error(`Invalid ${operationName} request`);
186+
throw new Error(
187+
`Invalid ${operationName} request with args ${JSON.stringify(args)}`,
188+
);
185189
}
186190
}
187191

@@ -354,7 +358,11 @@ export class SyscallsImpl {
354358
};
355359
const queryResult = await this.actionCallback({
356360
version: queryArgs.version,
357-
body: { path: queryArgs.name, args: queryArgs.args },
361+
body: {
362+
path: queryArgs.name,
363+
reference: queryArgs.reference,
364+
args: queryArgs.args,
365+
},
358366
path: "/api/actions/query",
359367
operationName,
360368
responseValidator: runFunctionReturn,
@@ -391,7 +399,11 @@ export class SyscallsImpl {
391399
};
392400
const mutationResult = await this.actionCallback({
393401
version: mutationArgs.version,
394-
body: { path: mutationArgs.name, args: mutationArgs.args },
402+
body: {
403+
path: mutationArgs.name,
404+
reference: mutationArgs.reference,
405+
args: mutationArgs.args,
406+
},
395407
path: "/api/actions/mutation",
396408
operationName,
397409
responseValidator: runFunctionReturn,
@@ -428,7 +440,11 @@ export class SyscallsImpl {
428440
};
429441
const actionResult = await this.actionCallback({
430442
version: actionArgs.version,
431-
body: { path: actionArgs.name, args: actionArgs.args },
443+
body: {
444+
path: actionArgs.name,
445+
reference: actionArgs.reference,
446+
args: actionArgs.args,
447+
},
432448
path: "/api/actions/action",
433449
operationName,
434450
responseValidator: runFunctionReturn,
@@ -486,6 +502,7 @@ export class SyscallsImpl {
486502
const { jobId } = await this.actionCallback({
487503
version: scheduleArgs.version,
488504
body: {
505+
reference: scheduleArgs.reference,
489506
udfPath: scheduleArgs.name,
490507
udfArgs: scheduleArgs.args,
491508
scheduledTs: scheduleArgs.ts,

0 commit comments

Comments
 (0)