Skip to content

Commit

Permalink
First pass at read-only mode
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesaoverton committed Nov 28, 2024
1 parent 7367e36 commit 987db39
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 41 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ test-code: src/javascript/build/main.js
cargo test

.PHONY: test-docs
test-docs:
test-docs: target/debug/nanobot
PATH="$${PATH}:$$(pwd)/target/debug"; tesh --debug false ./doc

.PHONY: test
Expand Down
54 changes: 45 additions & 9 deletions doc/cgi.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ The CGI environment variables are:
- `PATH_INFO` with the path part of the URL, defaults to `/table`
- `QUERY_STRING` with the query string part of the URL, which is optional

Nanobot also checks these environment variables:

- `NANOBOT_READONLY`: when `TRUE` no editing is supporting; defaults to `FALSE`

Nanobot will return an
[HTTP response](https://en.wikipedia.org/wiki/HTTP#HTTP/1.1_response_messages),
with a status line,
Expand All @@ -28,7 +32,15 @@ and blank line,
and an optional message body
which will usually contain the HTML or JSON response content.

You can test this from the command-line like so:
Nanobot's CGI mode works by
starting the same HTTP server used for `nanobot serve` on a random port,
executing the request,
and printing the response to `STDOUT`.
This is much less efficient than a long-running server,
but it's very simple
and works well enough for low volumes of traffic.

You can test Nanobot CGI from the command-line like so:

```console tesh-session="cgi"
$ nanobot init
Expand All @@ -45,14 +57,39 @@ column src/schema/column.tsv column Columns for all of the tables.
datatype src/schema/datatype.tsv datatype Datatypes for all of the columns
```

Nanobot's CGI mode works by
starting the same HTTP server used for `nanobot serve` on a random port,
executing the request,
and printing the response to `STDOUT`.
This is much less efficient than a long-running server,
but it's very simple
and works well enough for low volumes of traffic.
We can POST a new row using CGI and form contents as `STDIN`:

```console tesh-session="cgi"
$ export GATEWAY_INTERFACE=CGI/1.1
$ REQUEST_METHOD=POST PATH_INFO=/table nanobot <<< 'action=submit&table=foo'
...
$ PATH_INFO=/table.txt nanobot
status: 200 OK
content-type: text/plain
content-length: 337
date: ...

table path type description
table src/schema/table.tsv table All of the tables in this project.
column src/schema/column.tsv column Columns for all of the tables.
datatype src/schema/datatype.tsv datatype Datatypes for all of the columns
foo
```

When `NANOBOT_READONLY` is `TRUE`,
POSTing will not work,
and the WebUI will not include buttons for editing actions.

```console tesh-session="cgi"
$ export NANOBOT_READONLY=TRUE
$ REQUEST_METHOD=POST PATH_INFO=/table nanobot <<< 'action=submit&table=bar'
status: 403 Forbidden
content-type: text/html; charset=utf-8
content-length: 13
date: ...

403 Forbidden
```

## Python

Expand Down Expand Up @@ -122,4 +159,3 @@ def run_cgi(path):
# The `response` is set, so just return the body.
return '\n'.join(body)
```

7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct Config {
pub pool: Option<AnyPool>,
pub valve: Option<Valve>,
pub valve_path: String,
pub editable: bool,
pub create_only: bool,
pub asset_path: Option<String>,
pub template_path: Option<String>,
Expand Down Expand Up @@ -319,6 +320,7 @@ impl Config {
.unwrap_or_default()
.path
.unwrap_or("src/schema/table.tsv".into()),
editable: true,
create_only: false,
asset_path: {
match user.assets.unwrap_or_default().path {
Expand Down Expand Up @@ -376,6 +378,11 @@ impl Config {
self
}

pub fn editable(&mut self, value: bool) -> &mut Config {
self.editable = value;
self
}

pub fn create_only(&mut self, value: bool) -> &mut Config {
self.create_only = value;
self
Expand Down
22 changes: 9 additions & 13 deletions src/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,19 +411,14 @@ async fn get_page(
let end = select.offset.unwrap_or(0) + cell_rows.len();

// Start with the VALVE table config, minus 'column' and 'column_order'.
let this_table_config = valve.config.table.get(&unquoted_table).unwrap();
let this_table_config = json!(this_table_config);
let mut this_table_config = this_table_config.as_object().unwrap().clone();
this_table_config.remove("column");
this_table_config.remove("column_order");
// Try to get Nanobot table config: will fail for "message" and "history" tables.
let mut this_table = config
.table
.iter()
.filter(|x| x.get("table").unwrap() == &json!(unquoted_table))
.next()
.unwrap_or(&this_table_config)
.clone();
let this_table = json!(table_config);
let mut this_table = this_table.as_object().unwrap().clone();
this_table.remove("column");
this_table.remove("column_order");
this_table.insert(
"editable".to_string(),
json!(table_config.options.contains("edit")),
);
this_table.insert("table".to_string(), json!(unquoted_table.clone()));
this_table.insert("href".to_string(), json!(unquoted_table.clone()));
this_table.insert("start".to_string(), json!(select.offset.unwrap_or(0) + 1));
Expand Down Expand Up @@ -607,6 +602,7 @@ async fn get_page(
let result: Value = json!({
"page": {
"project_name": "Nanobot",
"editable": config.editable,
"tables": tables,
"title": unquoted_table,
"url": select2.to_url().unwrap_or_default(),
Expand Down
54 changes: 40 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{config::Config, error::NanobotError, serve::build_app, sql::get_table_from_pool};
use axum_test_helper::{TestClient, TestResponse};
use clap::{arg, command, value_parser, Command};
use clap::{arg, command, value_parser, Arg, Command};
use ontodev_sqlrest::Select;
use ontodev_valve::valve::Valve;
use std::path::Path;
Expand Down Expand Up @@ -88,13 +88,22 @@ async fn main() -> Result<(), NanobotError> {
),
)
.subcommand(
Command::new("serve").about("Run HTTP server").arg(
arg!(
-c --connection <URL> "Specifies a database connection URL or file"
Command::new("serve")
.about("Run HTTP server")
.arg(
arg!(
-c --connection <URL> "Specifies a database connection URL or file"
)
.required(false)
.value_parser(value_parser!(String)),
)
.required(false)
.value_parser(value_parser!(String)),
),
.arg(
Arg::new("read-only")
.help("Run the server in read-only mode")
.long("read-only")
.required(false)
.num_args(0),
),
)
.get_matches();

Expand Down Expand Up @@ -141,6 +150,9 @@ async fn main() -> Result<(), NanobotError> {
if let Some(c) = sub_matches.get_one::<String>("connection") {
config.connection(c);
}
if let Some(r) = sub_matches.get_one::<bool>("read-only") {
config.editable = !*r;
}
if config.connection == ":memory:" {
(config.valve, config.pool) = {
let mut valve = Valve::build(&config.valve_path, &config.connection).await?;
Expand Down Expand Up @@ -209,12 +221,6 @@ async fn build_valve(config: &mut Config) -> Result<(), NanobotError> {
async fn handle_cgi(vars: &HashMap<String, String>, config: &mut Config) -> Result<String, String> {
tracing::debug!("Processing CGI request with vars: {:?}", vars);

let shared_state = Arc::new(serve::AppState {
config: config.clone(),
});
let app = build_app(shared_state);
let client = TestClient::new(app);

let request_method = vars
.get("REQUEST_METHOD")
.ok_or("No 'REQUEST_METHOD' in CGI vars".to_string())?;
Expand All @@ -224,6 +230,20 @@ async fn handle_cgi(vars: &HashMap<String, String>, config: &mut Config) -> Resu
let query_string = vars
.get("QUERY_STRING")
.ok_or("No 'QUERY_STRING' in CGI vars".to_string())?;
let read_only = vars
.get("NANOBOT_READONLY")
.ok_or("No 'NANOBOT_READONLY' in CGI vars".to_string())?;

if read_only.to_lowercase() == "true" {
config.editable = false;
}

let shared_state = Arc::new(serve::AppState {
config: config.clone(),
});
let app = build_app(shared_state);
let client = TestClient::new(app);

let mut url = path_info.clone();
if !url.starts_with("/") {
url = format!("/{}", path_info);
Expand Down Expand Up @@ -283,13 +303,19 @@ fn cgi_vars() -> Option<HashMap<String, String>> {
_ => return None,
};

for var in vec!["REQUEST_METHOD", "PATH_INFO", "QUERY_STRING"] {
for var in vec![
"REQUEST_METHOD",
"PATH_INFO",
"QUERY_STRING",
"NANOBOT_READONLY",
] {
match env::var_os(var).and_then(|p| Some(p.into_string())) {
Some(Ok(s)) => vars.insert(var.to_string(), s),
_ => match var {
"REQUEST_METHOD" => vars.insert(var.to_string(), "GET".to_string()),
"PATH_INFO" => vars.insert(var.to_string(), "/table".to_string()),
"QUERY_STRING" => vars.insert(var.to_string(), String::new()),
"NANOBOT_READONLY" => vars.insert(var.to_string(), String::from("FALSE")),
_ => {
// This should never happen since all possible cases should be handled above:
unreachable!(
Expand Down
8 changes: 7 additions & 1 deletion src/resources/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ <h3>Return to <a href="{{ page.root }}{{ table_name }}{% if row_number %}?offset
table_name }}</a> table.</h3>

<div class="row" style="padding-bottom:5px; padding-top:20px;">
{% if page.editable and table.editable %}
<form method="post">
{% for column, form_row in form_map|items %}
{{ form_row|safe }}
Expand All @@ -72,6 +73,11 @@ <h3>Return to <a href="{{ page.root }}{{ table_name }}{% if row_number %}?offset
<a class="btn btn-secondary" href="{{ page.root }}{{ table_name }}?offset={{ offset }}">Cancel</a>
</div>
</form>
{% else %}
{% for column, form_row in form_map|items %}
{{ form_row|safe }}
{% endfor %}
{% endif %}
</div>

{% endblock %}
{% endblock %}
2 changes: 2 additions & 0 deletions src/resources/page.html
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
</ul>
</li>
</ul>
{% if page.editable %}
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
Expand All @@ -164,6 +165,7 @@
</form>
</li>
</ul>
{% endif %}
</div>
</div>
</nav>
Expand Down
4 changes: 3 additions & 1 deletion src/resources/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ <h1 class="modal-title fs-5" id="{{ name|id }}ModalLabel">{{ value.label or name
</ul>
</span>

{% if table.table != "message" %}
{% if page.editable and table.editable %}
<a class="btn btn-outline-success" href="{{ table.table }}?view={{ table.edit_view or 'form' }}">Add row</a>
{% endif %}

Expand Down Expand Up @@ -291,8 +291,10 @@ <h1 class="modal-title fs-5" id="{{ name|id }}ModalLabel">{{ value.label or name
{% for col, cell in r|items -%}
{% if col == "row_number" %}
<td>
{% if page.editable and table.editable %}
<a class="btn btn-sm" href="{{ table.href }}/row/{{ cell.value }}?view={{ table.edit_view or 'form' }}"><i
class="bi-pencil" style="color: #adb5bd;"></i></a>
{% endif %}
</td>
{% elif col == "message_id" %}
{% else %}
Expand Down
Loading

0 comments on commit 987db39

Please sign in to comment.