From 87ca20f1700f556fd7630b73f418d9b23a61853f Mon Sep 17 00:00:00 2001 From: Mario Alejandro Montoya Cortes Date: Wed, 10 Dec 2025 13:18:33 -0500 Subject: [PATCH] Make /v1/database/:name/call/:func call procedures too, remove procedure route --- crates/cli/src/subcommands/call.rs | 170 ++++++++++----- crates/client-api/src/routes/database.rs | 197 ++++++++---------- crates/core/src/host/host_controller.rs | 6 + crates/core/src/host/mod.rs | 6 +- crates/schema/src/def.rs | 6 + .../00100-cli-reference.md | 8 +- docs/docs/01400-http-api/00300-database.md | 12 +- 7 files changed, 227 insertions(+), 178 deletions(-) diff --git a/crates/cli/src/subcommands/call.rs b/crates/cli/src/subcommands/call.rs index 23b351766c5..e2c22ad6eb7 100644 --- a/crates/cli/src/subcommands/call.rs +++ b/crates/cli/src/subcommands/call.rs @@ -9,23 +9,25 @@ use convert_case::{Case, Casing}; use itertools::Itertools; use spacetimedb_lib::sats::{self, AlgebraicType, Typespace}; use spacetimedb_lib::{Identity, ProductTypeElement}; -use spacetimedb_schema::def::{ModuleDef, ReducerDef}; +use spacetimedb_schema::def::{ModuleDef, ProcedureDef, ReducerDef}; use std::fmt::Write; use super::sql::parse_req; pub fn cli() -> clap::Command { clap::Command::new("call") - .about(format!("Invokes a reducer function in a database. {UNSTABLE_WARNING}")) + .about(format!( + "Invokes a reducer function OR procedure in a database. {UNSTABLE_WARNING}" + )) .arg( Arg::new("database") .required(true) .help("The database name or identity to use to invoke the call"), ) .arg( - Arg::new("reducer_name") + Arg::new("reducer_procedure_name") .required(true) - .help("The name of the reducer to call"), + .help("The name of the reducer OR procedure to call"), ) .arg(Arg::new("arguments").help("arguments formatted as JSON").num_args(1..)) .arg(common_args::server().help("The nickname, host name or URL of the server hosting the database")) @@ -34,9 +36,35 @@ pub fn cli() -> clap::Command { .after_help("Run `spacetime help call` for more detailed information.\n") } +enum CallResult<'a> { + Reducer(&'a ReducerDef), + Procedure(&'a ProcedureDef), +} + +impl<'a> CallResult<'a> { + fn params(&self) -> &'a sats::ProductType { + match self { + CallResult::Reducer(reducer_def) => &reducer_def.params, + CallResult::Procedure(procedure_def) => &procedure_def.params, + } + } + fn name(&self) -> &str { + match self { + CallResult::Reducer(reducer_def) => &reducer_def.name, + CallResult::Procedure(procedure_def) => &procedure_def.name, + } + } + fn kind(&self) -> &str { + match self { + CallResult::Reducer(_) => "reducer", + CallResult::Procedure(_) => "procedure", + } + } +} + pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), Error> { eprintln!("{UNSTABLE_WARNING}\n"); - let reducer_name = args.get_one::("reducer_name").unwrap(); + let reducer_procedure_name = args.get_one::("reducer_procedure_name").unwrap(); let arguments = args.get_many::("arguments"); let conn = parse_req(config, args).await?; @@ -47,14 +75,25 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), Error> { let module_def: ModuleDef = api.module_def().await?.try_into()?; - let reducer_def = module_def - .reducer(&**reducer_name) - .ok_or_else(|| anyhow::Error::msg(no_such_reducer(&database_identity, database, reducer_name, &module_def)))?; + let call_result = match module_def.reducer(&**reducer_procedure_name) { + Some(reducer_def) => CallResult::Reducer(reducer_def), + None => match module_def.procedure(&**reducer_procedure_name) { + Some(procedure_def) => CallResult::Procedure(procedure_def), + None => { + return Err(anyhow::Error::msg(no_such_reducer_or_procedure( + &database_identity, + database, + reducer_procedure_name, + &module_def, + ))); + } + }, + }; // String quote any arguments that should be quoted let arguments = arguments .unwrap_or_default() - .zip(&*reducer_def.params.elements) + .zip(&call_result.params().elements) .map(|(argument, element)| match &element.algebraic_type { AlgebraicType::String if !argument.starts_with('\"') || !argument.ends_with('\"') => { format!("\"{argument}\"") @@ -63,7 +102,7 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), Error> { }); let arg_json = format!("[{}]", arguments.format(", ")); - let res = api.call(reducer_name, arg_json).await?; + let res = api.call(reducer_procedure_name, arg_json).await?; if let Err(e) = res.error_for_status_ref() { let Ok(response_text) = res.text().await else { @@ -73,17 +112,23 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), Error> { let error = Err(e).context(format!("Response text: {response_text}")); - let error_msg = if response_text.starts_with("no such reducer") { - no_such_reducer(&database_identity, database, reducer_name, &module_def) - } else if response_text.starts_with("invalid arguments") { - invalid_arguments(&database_identity, database, &response_text, &module_def, reducer_def) - } else { - return error; - }; + let error_msg = + if response_text.starts_with("no such reducer") || response_text.starts_with("no such procedure") { + no_such_reducer_or_procedure(&database_identity, database, reducer_procedure_name, &module_def) + } else if response_text.starts_with("invalid arguments") { + invalid_arguments(&database_identity, database, &response_text, &module_def, call_result) + } else { + return error; + }; return error.context(error_msg); } + if let CallResult::Procedure(_) = call_result { + let body = res.text().await?; + println!("{body}"); + } + Ok(()) } @@ -93,11 +138,14 @@ fn invalid_arguments( db: &str, text: &str, module_def: &ModuleDef, - reducer_def: &ReducerDef, + call_result: CallResult, ) -> String { let mut error = format!( - "Invalid arguments provided for reducer `{}` for database `{}` resolving to identity `{}`.", - reducer_def.name, db, identity + "Invalid arguments provided for {} `{}` for database `{}` resolving to identity `{}`.", + call_result.kind(), + call_result.name(), + db, + identity ); if let Some((actual, expected)) = find_actual_expected(text).filter(|(a, e)| a != e) { @@ -110,8 +158,9 @@ fn invalid_arguments( write!( error, - "\n\nThe reducer has the following signature:\n\t{}", - ReducerSignature(module_def.typespace().with_type(reducer_def)) + "\n\nThe {} has the following signature:\n\t{}", + call_result.kind(), + ReducerSignature(module_def.typespace().with_type(&call_result)) ) .unwrap(); @@ -139,17 +188,17 @@ fn split_at_first_substring<'t>(text: &'t str, substring: &str) -> Option<(&'t s /// Provided the `schema_json` for the database, /// returns the signature for a reducer with `reducer_name`. -struct ReducerSignature<'a>(sats::WithTypespace<'a, ReducerDef>); +struct ReducerSignature<'a>(sats::WithTypespace<'a, CallResult<'a>>); impl std::fmt::Display for ReducerSignature<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let reducer_def = self.0.ty(); let typespace = self.0.typespace(); - write!(f, "{}(", reducer_def.name)?; + write!(f, "{}(", reducer_def.name())?; // Print the arguments to `args`. let mut comma = false; - for arg in &*reducer_def.params.elements { + for arg in &*reducer_def.params().elements { if comma { write!(f, ", ")?; } @@ -164,12 +213,18 @@ impl std::fmt::Display for ReducerSignature<'_> { } } -/// Returns an error message for when `reducer` does not exist in `db`. -fn no_such_reducer(database_identity: &Identity, db: &str, reducer: &str, module_def: &ModuleDef) -> String { - let mut error = - format!("No such reducer `{reducer}` for database `{db}` resolving to identity `{database_identity}`."); +/// Returns an error message for when `reducer` or `procedure` does not exist in `db`. +fn no_such_reducer_or_procedure( + database_identity: &Identity, + db: &str, + reducer: &str, + module_def: &ModuleDef, +) -> String { + let mut error = format!( + "No such reducer OR procedure `{reducer}` for database `{db}` resolving to identity `{database_identity}`." + ); - add_reducer_ctx_to_err(&mut error, module_def, reducer); + add_reducer_procedure_ctx_to_err(&mut error, module_def, reducer); error } @@ -177,38 +232,51 @@ fn no_such_reducer(database_identity: &Identity, db: &str, reducer: &str, module const REDUCER_PRINT_LIMIT: usize = 10; /// Provided the schema for the database, -/// decorate `error` with more helpful info about reducers. -fn add_reducer_ctx_to_err(error: &mut String, module_def: &ModuleDef, reducer_name: &str) { - let mut reducers = module_def +/// decorate `error` with more helpful info about reducers and procedures. +fn add_reducer_procedure_ctx_to_err(error: &mut String, module_def: &ModuleDef, reducer_name: &str) { + let reducers = module_def .reducers() .filter(|reducer| reducer.lifecycle.is_none()) .map(|reducer| &*reducer.name) .collect::>(); + let procedures = module_def + .procedures() + .map(|reducer| &*reducer.name) + .collect::>(); + if let Some(best) = find_best_match_for_name(&reducers, reducer_name, None) { write!(error, "\n\nA reducer with a similar name exists: `{best}`").unwrap(); + } else if let Some(best) = find_best_match_for_name(&procedures, reducer_name, None) { + write!(error, "\n\nA procedure with a similar name exists: `{best}`").unwrap(); } else if reducers.is_empty() { write!(error, "\n\nThe database has no reducers.").unwrap(); } else { - // Sort reducers by relevance. - reducers.sort_by_key(|candidate| edit_distance(reducer_name, candidate, usize::MAX)); - - // Don't spam the user with too many entries. - let too_many_to_show = reducers.len() > REDUCER_PRINT_LIMIT; - let diff = reducers.len().abs_diff(REDUCER_PRINT_LIMIT); - reducers.truncate(REDUCER_PRINT_LIMIT); - - // List them. - write!(error, "\n\nHere are some existing reducers:").unwrap(); - for candidate in reducers { - write!(error, "\n- {candidate}").unwrap(); - } + let mut list_similar = |mut list: Vec<&str>, name: &str, kind: &str| { + if list.is_empty() { + return; + } + list.sort_by_key(|candidate| edit_distance(name, candidate, usize::MAX)); - // When some where not listed, note that are more. - if too_many_to_show { - let plural = if diff == 1 { "" } else { "s" }; - write!(error, "\n... ({diff} reducer{plural} not shown)").unwrap(); - } + // Don't spam the user with too many entries. + let too_many_to_show = list.len() > REDUCER_PRINT_LIMIT; + let diff = list.len().abs_diff(REDUCER_PRINT_LIMIT); + list.truncate(REDUCER_PRINT_LIMIT); + + // List them. + write!(error, "\n\nHere are some existing {kind}s:").unwrap(); + for candidate in list { + write!(error, "\n- {candidate}").unwrap(); + } + + // When some where not listed, note that are more. + if too_many_to_show { + let plural = if diff == 1 { "" } else { "s" }; + write!(error, "\n... ({diff} {kind}{plural} not shown)").unwrap(); + } + }; + list_similar(reducers, reducer_name, "reducer"); + list_similar(procedures, reducer_name, "procedure"); } } diff --git a/crates/client-api/src/routes/database.rs b/crates/client-api/src/routes/database.rs index 1d7bf416d3c..f54f2b3f875 100644 --- a/crates/client-api/src/routes/database.rs +++ b/crates/client-api/src/routes/database.rs @@ -26,7 +26,7 @@ use log::info; use serde::Deserialize; use spacetimedb::database_logger::DatabaseLogger; use spacetimedb::host::module_host::ClientConnectedError; -use spacetimedb::host::UpdateDatabaseResult; +use spacetimedb::host::{CallResult, UpdateDatabaseResult}; use spacetimedb::host::{FunctionArgs, MigratePlanResult}; use spacetimedb::host::{ModuleHost, ReducerOutcome}; use spacetimedb::host::{ProcedureCallError, ReducerCallError}; @@ -85,6 +85,46 @@ pub struct CallParams { pub const NO_SUCH_DATABASE: (StatusCode, &str) = (StatusCode::NOT_FOUND, "No such database."); +fn map_reducer_error(e: ReducerCallError, reducer: &str) -> (StatusCode, String) { + let status_code = match e { + ReducerCallError::Args(_) => { + log::debug!("Attempt to call reducer {reducer} with invalid arguments"); + StatusCode::BAD_REQUEST + } + ReducerCallError::NoSuchModule(_) | ReducerCallError::ScheduleReducerNotFound => StatusCode::NOT_FOUND, + ReducerCallError::NoSuchReducer => { + log::debug!("Attempt to call non-existent reducer {reducer}"); + StatusCode::NOT_FOUND + } + ReducerCallError::LifecycleReducer(lifecycle) => { + log::debug!("Attempt to call {lifecycle:?} lifecycle reducer {reducer}"); + StatusCode::BAD_REQUEST + } + }; + + log::debug!("Error while invoking reducer {e:#}"); + (status_code, format!("{:#}", anyhow::anyhow!(e))) +} + +fn map_procedure_error(e: ProcedureCallError, procedure: &str) -> (StatusCode, String) { + let status_code = match e { + ProcedureCallError::Args(_) => { + log::debug!("Attempt to call procedure {procedure} with invalid arguments"); + StatusCode::BAD_REQUEST + } + ProcedureCallError::NoSuchModule(_) => StatusCode::NOT_FOUND, + ProcedureCallError::NoSuchProcedure => { + log::debug!("Attempt to call non-existent procedure OR reducer {procedure}"); + StatusCode::NOT_FOUND + } + ProcedureCallError::OutOfEnergy => StatusCode::PAYMENT_REQUIRED, + ProcedureCallError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + }; + log::error!("Error while invoking procedure {e:#}"); + (status_code, format!("{:#}", anyhow::anyhow!(e))) +} + +/// Call a reducer or procedure on the specified database module. pub async fn call( State(worker_ctx): State, Extension(auth): Extension, @@ -107,53 +147,68 @@ pub async fn call( let (module, Database { owner_identity, .. }) = find_module_and_database(&worker_ctx, name_or_identity).await?; + // Call the database's `client_connected` reducer, if any. + // If it fails or rejects the connection, bail. module .call_identity_connected(auth.into(), connection_id) .await .map_err(client_connected_error_to_response)?; let result = match module - .call_reducer(caller_identity, Some(connection_id), None, None, None, &reducer, args) + .call_reducer( + caller_identity, + Some(connection_id), + None, + None, + None, + &reducer, + args.clone(), + ) .await { - Ok(rcr) => Ok(rcr), - Err(e) => { - let status_code = match e { - ReducerCallError::Args(_) => { - log::debug!("Attempt to call reducer with invalid arguments"); - StatusCode::BAD_REQUEST - } - ReducerCallError::NoSuchModule(_) | ReducerCallError::ScheduleReducerNotFound => StatusCode::NOT_FOUND, - ReducerCallError::NoSuchReducer => { - log::debug!("Attempt to call non-existent reducer {reducer}"); - StatusCode::NOT_FOUND - } - ReducerCallError::LifecycleReducer(lifecycle) => { - log::debug!("Attempt to call {lifecycle:?} lifecycle reducer {reducer}"); - StatusCode::BAD_REQUEST - } - }; - - log::debug!("Error while invoking reducer {e:#}"); - Err((status_code, format!("{:#}", anyhow::anyhow!(e)))) + Ok(rcr) => Ok(CallResult::Reducer(rcr)), + Err(ReducerCallError::NoSuchReducer | ReducerCallError::ScheduleReducerNotFound) => { + // Not a reducer — try procedure instead + match module + .call_procedure(caller_identity, Some(connection_id), None, &reducer, args) + .await + .result + { + Ok(res) => Ok(CallResult::Procedure(res)), + Err(e) => Err(map_procedure_error(e, &reducer)), + } } + Err(e) => Err(map_reducer_error(e, &reducer)), }; module - // We don't clear views after reducer calls + // We don't clear views or procedures after reducer calls .call_identity_disconnected(caller_identity, connection_id, false) .await .map_err(client_disconnected_error_to_response)?; match result { - Ok(result) => { + Ok(CallResult::Reducer(result)) => { let (status, body) = reducer_outcome_response(&owner_identity, &reducer, result.outcome); Ok(( status, TypedHeader(SpacetimeEnergyUsed(result.energy_used)), TypedHeader(SpacetimeExecutionDurationMicros(result.execution_duration)), body, - )) + ) + .into_response()) + } + Ok(CallResult::Procedure(result)) => { + // Procedures don't assign a special meaning to error returns, unlike reducers, + // as there's no transaction for them to automatically abort. + // Instead, we just pass on their return value with the OK status so long as we successfully invoked the procedure. + let (status, body) = procedure_outcome_response(result.return_val); + Ok(( + status, + TypedHeader(SpacetimeExecutionDurationMicros(result.execution_duration)), + body, + ) + .into_response()) } Err(e) => Err((e.0, e.1).into()), } @@ -254,88 +309,6 @@ pub enum DBCallErr { InstanceNotScheduled, } -#[derive(Deserialize)] -pub struct ProcedureParams { - name_or_identity: NameOrIdentity, - procedure: String, -} - -async fn procedure( - State(worker_ctx): State, - Extension(auth): Extension, - Path(ProcedureParams { - name_or_identity, - procedure, - }): Path, - TypedHeader(content_type): TypedHeader, - ByteStringBody(body): ByteStringBody, -) -> axum::response::Result { - assert_content_type_json(content_type)?; - - let caller_identity = auth.claims.identity; - - let args = FunctionArgs::Json(body); - - let (module, _) = find_module_and_database(&worker_ctx, name_or_identity).await?; - - // HTTP callers always need a connection ID to provide to connect/disconnect, - // so generate one. - let connection_id = generate_random_connection_id(); - - // Call the database's `client_connected` reducer, if any. - // If it fails or rejects the connection, bail. - module - .call_identity_connected(auth.into(), connection_id) - .await - .map_err(client_connected_error_to_response)?; - - let result = match module - .call_procedure(caller_identity, Some(connection_id), None, &procedure, args) - .await - .result - { - Ok(res) => Ok(res), - Err(e) => { - let status_code = match e { - ProcedureCallError::Args(_) => { - log::debug!("Attempt to call reducer with invalid arguments"); - StatusCode::BAD_REQUEST - } - ProcedureCallError::NoSuchModule(_) => StatusCode::NOT_FOUND, - ProcedureCallError::NoSuchProcedure => { - log::debug!("Attempt to call non-existent procedure {procedure}"); - StatusCode::NOT_FOUND - } - ProcedureCallError::OutOfEnergy => StatusCode::PAYMENT_REQUIRED, - ProcedureCallError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, - }; - log::error!("Error while invoking procedure {e:#}"); - Err((status_code, format!("{:#}", anyhow::anyhow!(e)))) - } - }; - - module - // We don't clear views after procedure calls - .call_identity_disconnected(caller_identity, connection_id, false) - .await - .map_err(client_disconnected_error_to_response)?; - - match result { - Ok(result) => { - // Procedures don't assign a special meaning to error returns, unlike reducers, - // as there's no transaction for them to automatically abort. - // Instead, we just pass on their return value with the OK status so long as we successfully invoked the procedure. - let (status, body) = procedure_outcome_response(result.return_val); - Ok(( - status, - TypedHeader(SpacetimeExecutionDurationMicros(result.execution_duration)), - body, - )) - } - Err(e) => Err((e.0, e.1).into()), - } -} - fn procedure_outcome_response(return_val: AlgebraicValue) -> (StatusCode, axum::response::Response) { ( StatusCode::OK, @@ -1191,9 +1164,7 @@ pub struct DatabaseRoutes { /// GET: /database/:name_or_identity/subscribe pub subscribe_get: MethodRouter, /// POST: /database/:name_or_identity/call/:reducer - pub call_reducer_post: MethodRouter, - /// POST: /database/:name_or_identity/procedure/:reducer - pub call_procedure_post: MethodRouter, + pub call_reducer_procedure_post: MethodRouter, /// GET: /database/:name_or_identity/schema pub schema_get: MethodRouter, /// GET: /database/:name_or_identity/logs @@ -1224,8 +1195,7 @@ where names_put: put(set_names::), identity_get: get(get_identity::), subscribe_get: get(handle_websocket::), - call_reducer_post: post(call::), - call_procedure_post: post(procedure::), + call_reducer_procedure_post: post(call::), schema_get: get(schema::), logs_get: get(logs::), sql_post: post(sql::), @@ -1250,8 +1220,7 @@ where .route("/names", self.names_put) .route("/identity", self.identity_get) .route("/subscribe", self.subscribe_get) - .route("/call/:reducer", self.call_reducer_post) - .route("/unstable/procedure/:procedure", self.call_procedure_post) + .route("/call/:reducer", self.call_reducer_procedure_post) .route("/schema", self.schema_get) .route("/logs", self.logs_get) .route("/sql", self.sql_post) diff --git a/crates/core/src/host/host_controller.rs b/crates/core/src/host/host_controller.rs index 4bf60054c28..9b57fd6f79d 100644 --- a/crates/core/src/host/host_controller.rs +++ b/crates/core/src/host/host_controller.rs @@ -181,6 +181,12 @@ pub struct ProcedureCallResult { pub start_timestamp: Timestamp, } +#[derive(Debug)] +pub enum CallResult { + Reducer(ReducerCallResult), + Procedure(ProcedureCallResult), +} + #[derive(Debug)] pub struct CallProcedureReturn { pub result: Result, diff --git a/crates/core/src/host/mod.rs b/crates/core/src/host/mod.rs index c928c63261f..2c2f6b2a6fd 100644 --- a/crates/core/src/host/mod.rs +++ b/crates/core/src/host/mod.rs @@ -25,8 +25,8 @@ mod wasm_common; pub use disk_storage::DiskStorage; pub use host_controller::{ - extract_schema, CallProcedureReturn, ExternalDurability, ExternalStorage, HostController, MigratePlanResult, - ProcedureCallResult, ProgramStorage, ReducerCallResult, ReducerOutcome, + extract_schema, CallProcedureReturn, CallResult, ExternalDurability, ExternalStorage, HostController, + MigratePlanResult, ProcedureCallResult, ProgramStorage, ReducerCallResult, ReducerOutcome, }; pub use module_host::{ModuleHost, NoSuchModule, ProcedureCallError, ReducerCallError, UpdateDatabaseResult}; pub use scheduler::Scheduler; @@ -34,7 +34,7 @@ pub use scheduler::Scheduler; /// Encoded arguments to a database function. /// /// A database function is either a reducer or a procedure. -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum FunctionArgs { Json(ByteString), Bsatn(Bytes), diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index 2c0a97d9ef4..c4ac01d5289 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -284,6 +284,12 @@ impl ModuleDef { .map(|(_, def)| def) } + /// Convenience method to look up a procedure, possibly by a string. + pub fn procedure>(&self, name: &K) -> Option<&ProcedureDef> { + // If the string IS a valid identifier, we can just look it up. + self.procedures.get(name) + } + /// Convenience method to look up a procedure, possibly by a string, returning its id as well. pub fn procedure_full>( &self, diff --git a/docs/docs/00500-cli-reference/00100-cli-reference.md b/docs/docs/00500-cli-reference/00100-cli-reference.md index 689172a854e..d68c3e23b85 100644 --- a/docs/docs/00500-cli-reference/00100-cli-reference.md +++ b/docs/docs/00500-cli-reference/00100-cli-reference.md @@ -48,7 +48,7 @@ This document contains the help content for the `spacetime` command-line program - `publish` — Create and update a SpacetimeDB database - `delete` — Deletes a SpacetimeDB database - `logs` — Prints logs from a SpacetimeDB database -- `call` — Invokes a reducer function in a database. WARNING: This command is UNSTABLE and subject to breaking changes. +- `call` — Invokes a reducer function OR procedure in a database. WARNING: This command is UNSTABLE and subject to breaking changes. - `describe` — Describe the structure of a database or entities within it. WARNING: This command is UNSTABLE and subject to breaking changes. - `energy` — Invokes commands related to database budgets. WARNING: This command is UNSTABLE and subject to breaking changes. - `sql` — Runs a SQL query on the database. WARNING: This command is UNSTABLE and subject to breaking changes. @@ -145,16 +145,16 @@ Run `spacetime help logs` for more detailed information. ## spacetime call -Invokes a reducer function in a database. WARNING: This command is UNSTABLE and subject to breaking changes. +Invokes a reducer function OR procedure in a database. WARNING: This command is UNSTABLE and subject to breaking changes. -**Usage:** `spacetime call [OPTIONS] [arguments]...` +**Usage:** `spacetime call [OPTIONS] [arguments]...` Run `spacetime help call` for more detailed information. ###### Arguments: - `` — The database name or identity to use to invoke the call -- `` — The name of the reducer to call +- `` — The name of the reducer OR procedure to call - `` — arguments formatted as JSON ###### Options: diff --git a/docs/docs/01400-http-api/00300-database.md b/docs/docs/01400-http-api/00300-database.md index 7055304f0cb..cbc7aca3a08 100644 --- a/docs/docs/01400-http-api/00300-database.md +++ b/docs/docs/01400-http-api/00300-database.md @@ -9,7 +9,7 @@ The HTTP endpoints in `/v1/database` allow clients to interact with Spacetime da ## At a glance | Route | Description | -| -------------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| ---------------------------------------------------------------------------------------------------| ------------------------------------------------- | | [`POST /v1/database`](#post-v1database) | Publish a new database given its module code. | | [`POST /v1/database/:name_or_identity`](#post-v1databasename_or_identity) | Publish to a database given its module code. | | [`GET /v1/database/:name_or_identity`](#get-v1databasename_or_identity) | Get a JSON description of a database. | @@ -19,7 +19,7 @@ The HTTP endpoints in `/v1/database` allow clients to interact with Spacetime da | [`PUT /v1/database/:name_or_identity/names`](#put-v1databasename_or_identitynames) | Set the list of names for this database. | | [`GET /v1/database/:name_or_identity/identity`](#get-v1databasename_or_identityidentity) | Get the identity of a database. | | [`GET /v1/database/:name_or_identity/subscribe`](#get-v1databasename_or_identitysubscribe) | Begin a WebSocket connection. | -| [`POST /v1/database/:name_or_identity/call/:reducer`](#post-v1databasename_or_identitycallreducer) | Invoke a reducer in a database. | +| [`POST /v1/database/:name_or_identity/call/:reducer`](#post-v1databasename_or_identitycallreducer) | Invoke a reducer or procedure in a database. | | [`GET /v1/database/:name_or_identity/schema`](#get-v1databasename_or_identityschema) | Get the schema for a database. | | [`GET /v1/database/:name_or_identity/logs`](#get-v1databasename_or_identitylogs) | Retrieve logs from a database. | | [`POST /v1/database/:name_or_identity/sql`](#post-v1databasename_or_identitysql) | Run a SQL query against a database. | @@ -241,13 +241,13 @@ The SpacetimeDB text WebSocket protocol, `v1.json.spacetimedb`, encodes messages ## `POST /v1/database/:name_or_identity/call/:reducer` -Invoke a reducer in a database. +Invoke a reducer or procedure in a database. #### Path parameters -| Name | Value | -| ---------- | ------------------------ | -| `:reducer` | The name of the reducer. | +| Name | Value | +| ---------- | ------------------------------------- | +| `:reducer` | The name of the reducer or procedure. | #### Required Headers