Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)))`.
Expand Down
19 changes: 17 additions & 2 deletions examples/official-site/sqlpage/migrations/40_fetch.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (
Expand Down
18 changes: 18 additions & 0 deletions src/webserver/database/sqlpage_functions/function_traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SqlPageFunctionParam<T>>
{
type TargetType = Option<T>;

fn from_args(
arg: &mut std::vec::IntoIter<Option<Cow<'a, str>>>,
) -> anyhow::Result<Self::TargetType> {
let param = <Option<Cow<'a, str>>>::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<Option<Cow<'a, str>>>;
}
Expand Down
28 changes: 18 additions & 10 deletions src/webserver/database/sqlpage_functions/functions.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -26,8 +27,8 @@ super::function_definition_macro::sqlpage_functions! {
environment_variable(name: Cow<str>);
exec((&RequestInfo), program_name: Cow<str>, args: Vec<Cow<str>>);

fetch((&RequestInfo), http_request: SqlPageFunctionParam<super::http_fetch_request::HttpFetchRequest<'_>>);
fetch_with_meta((&RequestInfo), http_request: SqlPageFunctionParam<super::http_fetch_request::HttpFetchRequest<'_>>);
fetch((&RequestInfo), http_request: Option<SqlPageFunctionParam<HttpFetchRequest<'_>>>);
fetch_with_meta((&RequestInfo), http_request: Option<SqlPageFunctionParam<HttpFetchRequest<'_>>>);

hash_password(password: Option<String>);
header((&RequestInfo), name: Cow<str>);
Expand Down Expand Up @@ -185,8 +186,11 @@ fn prepare_request_body(

async fn fetch(
request: &RequestInfo,
http_request: super::http_fetch_request::HttpFetchRequest<'_>,
) -> anyhow::Result<String> {
http_request: Option<HttpFetchRequest<'_>>,
) -> anyhow::Result<Option<String>> {
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)?;
Expand Down Expand Up @@ -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<u8>, encoding: Option<&str>) -> anyhow::Result<String> {
Expand Down Expand Up @@ -259,10 +263,14 @@ fn decode_response(response: Vec<u8>, encoding: Option<&str>) -> anyhow::Result<

async fn fetch_with_meta(
request: &RequestInfo,
http_request: super::http_fetch_request::HttpFetchRequest<'_>,
) -> anyhow::Result<String> {
http_request: Option<HttpFetchRequest<'_>>,
) -> anyhow::Result<Option<String>> {
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)?;
Expand Down Expand Up @@ -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<String>) -> anyhow::Result<Option<String>> {
Expand Down
1 change: 1 addition & 0 deletions tests/sql_test_files/data/fetch_null.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
select null as expected, sqlpage.fetch(null) as actual;
1 change: 1 addition & 0 deletions tests/sql_test_files/data/fetch_with_meta_null.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
select null as expected, sqlpage.fetch_with_meta(null) as actual;