add support for building JSON APIs in SQLPage
This commit is contained in:
@@ -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
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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!`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
CREATE TABLE clicks(
|
||||
click_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
+24
-5
@@ -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>> {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user