diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ea53068..93640d3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - `curl -H "Accept: application/json" http://example.com/page.sql`: returns a json array - `curl -H "Accept: application/x-ndjson" http://example.com/page.sql`: returns one json object per line. - Fixed a bug in `sqlpage.link`: a link with no path (link to the current page) and no url parameter now works as expected. It used to keep the existing url parameters instead of removing them. `sqlpage.link('', '{}')` now returns `'?'` instead of the empty string. + - `sqlpage.fetch(null)` and `sqlpage.fetch_with_meta(null)` now return `null` instead of throwing an error. - **New Function**: `sqlpage.set_variable(name, value)` - Returns a URL with the specified variable set to the given value, preserving other existing variables. - This is a shorthand for `sqlpage.link(sqlpage.path(), json_patch(sqlpage.variables('get'), json_object(name, value)))`. diff --git a/examples/official-site/sqlpage/migrations/40_fetch.sql b/examples/official-site/sqlpage/migrations/40_fetch.sql index 5a3d7b18..f93d6f50 100644 --- a/examples/official-site/sqlpage/migrations/40_fetch.sql +++ b/examples/official-site/sqlpage/migrations/40_fetch.sql @@ -36,7 +36,7 @@ In this example, we use the complex form of the function to make an authenticated POST request, with custom request headers and a custom request body. We use SQLite''s json functions to build the request body. -See [the list of SQL databases and their JSON functions](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide) for +See [the list of SQL databases and their JSON functions](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide) for more information on how to build JSON objects in your database. ```sql @@ -94,7 +94,22 @@ The fetch function accepts either a URL string, or a JSON object with the follow If the request fails, this function throws an error, that will be displayed to the user. The response headers are not available for inspection. -If you need to handle errors or inspect the response headers, use [`sqlpage.fetch_with_meta`](?function=fetch_with_meta). +## Conditional data fetching + +Since v0.40, `sqlpage.fetch(null)` returns null instead of throwing an error. +This makes it easier to conditionnally query an API: + +```sql +set current_field_value = (select field from my_table where id = 1); +set target_url = nullif(''http://example.com/api/field/1'', null); -- null if the field is currently null in the db +set api_value = sqlpage.fetch($target_url); -- no http request made if the field is not null in the db +update my_table set field = $api_value where id = 1 and $api_value is not null; -- update the field only if it was not present before +``` + +## Advanced usage + +If you need to handle errors or inspect the response headers or the status code, +use [`sqlpage.fetch_with_meta`](?function=fetch_with_meta). ' ); INSERT INTO sqlpage_function_parameters ( diff --git a/src/webserver/database/sqlpage_functions/function_traits.rs b/src/webserver/database/sqlpage_functions/function_traits.rs index 74bbbe22..b3125a0d 100644 --- a/src/webserver/database/sqlpage_functions/function_traits.rs +++ b/src/webserver/database/sqlpage_functions/function_traits.rs @@ -84,6 +84,24 @@ impl<'a, T: BorrowFromStr<'a> + Sized + 'a> FunctionParamType<'a> for SqlPageFun } } +impl<'a, T: BorrowFromStr<'a> + Sized + 'a> FunctionParamType<'a> + for Option> +{ + type TargetType = Option; + + fn from_args( + arg: &mut std::vec::IntoIter>>, + ) -> anyhow::Result { + let param = >>::from_args(arg)?; + let res = if let Some(param) = param { + Some(T::borrow_from_str(param)?) + } else { + None + }; + Ok(res) + } +} + pub(super) trait FunctionResultType<'a> { fn into_cow_result(self) -> anyhow::Result>>; } diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index 0bfebe9b..0ac7ed86 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -1,8 +1,9 @@ use super::{ExecutionContext, RequestInfo}; use crate::webserver::{ database::{ - blob_to_data_url::vec_to_data_uri_with_mime, execute_queries::DbConn, - sqlpage_functions::url_parameters::URLParameters, + blob_to_data_url::vec_to_data_uri_with_mime, + execute_queries::DbConn, + sqlpage_functions::{http_fetch_request::HttpFetchRequest, url_parameters::URLParameters}, }, http_client::make_http_client, request_variables::SetVariablesMap, @@ -26,8 +27,8 @@ super::function_definition_macro::sqlpage_functions! { environment_variable(name: Cow); exec((&RequestInfo), program_name: Cow, args: Vec>); - fetch((&RequestInfo), http_request: SqlPageFunctionParam>); - fetch_with_meta((&RequestInfo), http_request: SqlPageFunctionParam>); + fetch((&RequestInfo), http_request: Option>>); + fetch_with_meta((&RequestInfo), http_request: Option>>); hash_password(password: Option); header((&RequestInfo), name: Cow); @@ -185,8 +186,11 @@ fn prepare_request_body( async fn fetch( request: &RequestInfo, - http_request: super::http_fetch_request::HttpFetchRequest<'_>, -) -> anyhow::Result { + http_request: Option>, +) -> anyhow::Result> { + let Some(http_request) = http_request else { + return Ok(None); + }; let client = make_http_client(&request.app_state.config) .with_context(|| "Unable to create an HTTP client")?; let req = build_request(&client, &http_request)?; @@ -219,7 +223,7 @@ async fn fetch( .to_vec(); let response_str = decode_response(body, http_request.response_encoding.as_deref())?; log::debug!("Fetch response: {response_str}"); - Ok(response_str) + Ok(Some(response_str)) } fn decode_response(response: Vec, encoding: Option<&str>) -> anyhow::Result { @@ -259,10 +263,14 @@ fn decode_response(response: Vec, encoding: Option<&str>) -> anyhow::Result< async fn fetch_with_meta( request: &RequestInfo, - http_request: super::http_fetch_request::HttpFetchRequest<'_>, -) -> anyhow::Result { + http_request: Option>, +) -> anyhow::Result> { use serde::{ser::SerializeMap, Serializer}; + let Some(http_request) = http_request else { + return Ok(None); + }; + let client = make_http_client(&request.app_state.config) .with_context(|| "Unable to create an HTTP client")?; let req = build_request(&client, &http_request)?; @@ -337,7 +345,7 @@ async fn fetch_with_meta( obj.end()?; let return_value = String::from_utf8(resp_str)?; - Ok(return_value) + Ok(Some(return_value)) } pub(crate) async fn hash_password(password: Option) -> anyhow::Result> { diff --git a/tests/sql_test_files/data/fetch_null.sql b/tests/sql_test_files/data/fetch_null.sql new file mode 100644 index 00000000..9bff0482 --- /dev/null +++ b/tests/sql_test_files/data/fetch_null.sql @@ -0,0 +1 @@ +select null as expected, sqlpage.fetch(null) as actual; diff --git a/tests/sql_test_files/data/fetch_with_meta_null.sql b/tests/sql_test_files/data/fetch_with_meta_null.sql new file mode 100644 index 00000000..92b4fdaf --- /dev/null +++ b/tests/sql_test_files/data/fetch_with_meta_null.sql @@ -0,0 +1 @@ +select null as expected, sqlpage.fetch_with_meta(null) as actual;