add support for building JSON APIs in SQLPage

This commit is contained in:
lovasoa
2023-07-22 15:08:08 +02:00
parent 79832df4c2
commit b45d7bc525
10 changed files with 121 additions and 14 deletions
+3
View File
@@ -2,6 +2,9 @@
## unreleased
- Added a new `json` component, which allows building a JSON API entirely in SQL with SQLPage !
Now creating an api over your database is as simple as `SELECT 'json' AS component, JSON_OBJECT('hello', 'world') AS contents`.
## 0.8.0 (2023-07-17)
- Added a new [`sqlite_extensions` configuration parameter](./configuration.md) to load SQLite extensions. This allows many interesting use cases, such as
Generated
+3 -3
View File
@@ -721,9 +721,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "either"
version = "1.8.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "encoding_rs"
@@ -2153,7 +2153,7 @@ dependencies = [
[[package]]
name = "sqlpage"
version = "0.8.0"
version = "0.9.0"
dependencies = [
"actix-web",
"actix-web-httpauth",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sqlpage"
version = "0.8.0"
version = "0.9.0"
edition = "2021"
description = "A SQL-only web application framework. Takes .sql files and formats the query result using pre-made configurable professional-looking components."
keywords = ["web", "sql", "framework"]
+1
View File
@@ -141,6 +141,7 @@ An alternative for Mac OS users is to use [SQLPage's homebrew package](https://f
- [Master-Detail Form](./examples/master-detail-form/): shows how to implement a simple set of forms to insert data into database tables that have a one-to-many relationship.
- [SQLPage's own official website and documentation](./examples/official-site/): The SQL source code for the project's official site, https://sql.ophir.dev
- [User Management](./examples/user-authentication/): An authentication demo with user registration, log in, log out, and confidential pages. Uses PostgreSQL.
- [Making a JSON API and integrating React components in the frontend](./examples/using%20react%20and%20other%20custom%20scripts%20and%20styles/): Shows how to integrate a react component in a SQLPage website, and how to easily build a REST API with SQLPage.
## Configuration
@@ -0,0 +1,66 @@
INSERT INTO component (name, description, icon, introduced_in_version)
VALUES (
'json',
'For advanced users, allows you to easily build an API over your database.
The json component responds to the current HTTP request with a JSON object.
This component must appear at the top of your SQL file, before any other data has been sent to the browser.',
'code',
'0.9.0'
);
-- Insert the parameters for the http_header component into the parameter table
INSERT INTO parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES (
'json',
'contents',
'The JSON payload to send. You should use your database''s built-in json functions to build the value to enter here.',
'TEXT',
TRUE,
FALSE
);
-- Insert an example usage of the http_header component into the example table
INSERT INTO example (component, description)
VALUES (
'json',
'
Creates an API endpoint that will allow developers to easily query a list of users stored in your database.
You should use the json functions provided by your database to form the value you pass to the `contents` property.
To build a json array out of rows from the database, you can use:
- `json_group_array()` in SQLite,
- `json_agg()` in Postgres, or
- `JSON_ARRAYAGG()` in MySQL.
```sql
SELECT ''json'' AS component,
JSON_OBJECT(
''users'', (
SELECT JSON_GROUP_ARRAY(
JSON_OBJECT(
''username'', username,
''userid'', id
)
) FROM users
)
) AS contents;
```
This will return a JSON response that looks like this:
```json
{
"users" : [
{ "username":"James", "userid":1 }
]
}
```
'
);
@@ -0,0 +1,7 @@
-- Adds a new entry in the clicks table
INSERT INTO clicks(click_time) VALUES (datetime('now'));
SELECT 'json' AS component,
JSON_OBJECT(
'total_clicks', (SELECT count(*) FROM clicks)
) AS contents;
@@ -5,8 +5,17 @@ function MyComponent({ greeting_name }) {
const [count, setCount] = React.useState(0);
return React.createElement(
'button',
{ onClick: () => setCount(count + 1), className: 'btn btn-primary' },
`Hello ${greeting_name}, you clicked me ${count} times!`
{
onClick: async () => {
const r = await fetch('/api.sql');
const { total_clicks } = await r.json();
setCount(total_clicks)
},
className: 'btn btn-primary'
},
count == 0
? `Hello, ${greeting_name}. Click me !`
: `You clicked me ${count} times!`
);
}
@@ -0,0 +1,3 @@
CREATE TABLE clicks(
click_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
+24 -5
View File
@@ -20,8 +20,8 @@ pub enum PageContext<W: std::io::Write> {
renderer: RenderContext<W>,
},
/// The headers have been set, but no response body should be sent
Close(HeaderContext<W>),
/// The response is ready, and should be sent as is. No further statements should be executed
Close(HttpResponse),
}
/// Handles the first SQL statements, before the headers have been sent to
@@ -49,6 +49,7 @@ impl<W: std::io::Write> HeaderContext<W> {
Some("status_code") => self.status_code(&data).map(PageContext::Header),
Some("http_header") => self.add_http_header(&data).map(PageContext::Header),
Some("redirect") => self.redirect(&data).map(PageContext::Close),
Some("json") => self.json(&data).map(PageContext::Close),
Some("cookie") => self.add_cookie(&data).map(PageContext::Header),
Some("authentication") => self.authentication(&data),
_ => self.start_body(data).await,
@@ -131,13 +132,29 @@ impl<W: std::io::Write> HeaderContext<W> {
Ok(self)
}
fn redirect(mut self, data: &JsonValue) -> anyhow::Result<Self> {
fn redirect(mut self, data: &JsonValue) -> anyhow::Result<HttpResponse> {
self.response.status(StatusCode::FOUND);
self.has_status = true;
let link = get_object_str(data, "link")
.with_context(|| "The redirect component requires a 'link' property")?;
self.response.insert_header((header::LOCATION, link));
Ok(self)
let response = self.response.body(());
Ok(response)
}
/// Answers to the HTTP request with a single json object
fn json(mut self, data: &JsonValue) -> anyhow::Result<HttpResponse> {
let contents = data
.get("contents")
.with_context(|| "Missing 'contents' property for the json component")?;
let json_response = if let Some(s) = contents.as_str() {
s.as_bytes().to_owned()
} else {
serde_json::to_vec(contents)?
};
self.response
.insert_header((header::CONTENT_TYPE, "application/json"));
Ok(self.response.body(json_response))
}
fn authentication(mut self, data: &JsonValue) -> anyhow::Result<PageContext<W>> {
@@ -171,7 +188,9 @@ impl<W: std::io::Write> HeaderContext<W> {
.insert_header((header::WWW_AUTHENTICATE, "Basic realm=\"Auth required\""));
self.has_status = true;
}
Ok(PageContext::Close(self))
// Set an empty response body
let http_response = self.response.body(());
Ok(PageContext::Close(http_response))
}
async fn start_body(self, data: JsonValue) -> anyhow::Result<PageContext<W>> {
+2 -3
View File
@@ -170,9 +170,8 @@ async fn build_response_header_and_stream<S: Stream<Item = DbItem>>(
database_entries_stream: stream,
});
}
PageContext::Close(h) => {
head_context = h;
break;
PageContext::Close(http_response) => {
return Ok(ResponseWithWriter::FinishedResponse { http_response })
}
},
DbItem::FinishedQuery => log::debug!("finished query"),