use std::collections::BTreeMap;
use std::str;
use anyhow::Context as _;
use axum::extract::{DefaultBodyLimit, Path};
use axum::response::IntoResponse;
use axum::routing::options;
use axum::{Extension, Router};
use axum_extra::TypedHeader;
use axum_extra::extract::Query;
use bytes::Bytes;
use serde::Deserialize;
use surrealdb_core::dbs::Session;
use surrealdb_core::dbs::capabilities::RouteTarget;
use surrealdb_core::iam::check::check_ns_db;
use surrealdb_core::kvs::Datastore;
use surrealdb_core::{map, syn};
use surrealdb_types::{Array, SurrealValue, Value, Variables, vars};
use tower_http::limit::RequestBodyLimitLayer;
use super::AppState;
use super::error::ResponseError;
use super::headers::Accept;
use super::output::Output;
use crate::cnf::HTTP_MAX_KEY_BODY_SIZE;
use crate::ntw::error::Error as NetError;
use crate::ntw::input::bytes_to_utf8;
use crate::ntw::params::Params;
#[derive(Default, Deserialize, Debug, Clone)]
struct QueryOptions {
pub limit: Option<i64>,
pub start: Option<i64>,
pub fields: Option<Vec<String>>,
}
pub fn router<S>() -> Router<S>
where
S: Clone + Send + Sync + 'static,
{
Router::new()
.route(
"/key/{table}",
options(|| async {})
.get(select_all)
.post(create_all)
.put(update_all)
.patch(modify_all)
.delete(delete_all),
)
.route_layer(DefaultBodyLimit::disable())
.layer(RequestBodyLimitLayer::new(*HTTP_MAX_KEY_BODY_SIZE))
.merge(
Router::new()
.route(
"/key/{table}/{key}",
options(|| async {})
.get(select_one)
.post(create_one)
.put(update_one)
.patch(modify_one)
.delete(delete_one),
)
.route_layer(DefaultBodyLimit::disable())
.layer(RequestBodyLimitLayer::new(*HTTP_MAX_KEY_BODY_SIZE)),
)
}
async fn execute_and_return(
db: &Datastore,
sql: &str,
session: &Session,
mut vars: Variables,
accept: Option<&Accept>,
expr: Option<String>,
) -> Result<Output, anyhow::Error> {
let vars = if let Some(expr) = expr {
let ast = syn::parse(&expr)
.context("Invalid /key request body: expected a single SurrealQL value")?;
if !ast.is_value_expression() {
return Err(anyhow::anyhow!(
"Invalid /key request body: expected a single SurrealQL value (literal, object, array, or $param), not a statement or function call"
));
}
let mut value = db.execute(&expr, session, Some(vars.clone())).await?;
if let Some(resp) = value.pop() {
vars.insert("data".to_owned(), resp.result?);
}
vars
} else {
vars
};
match db.execute(sql, session, Some(vars)).await {
Ok(res) => match accept {
None | Some(Accept::ApplicationJson) => {
let v = Value::Array(Array::from(
res.into_iter().map(|x| x.into_value()).collect::<Vec<Value>>(),
));
Ok(Output::json_value(&v))
}
Some(Accept::ApplicationCbor) => {
let v = Value::Array(Array::from(
res.into_iter().map(|x| x.into_value()).collect::<Vec<Value>>(),
));
Ok(Output::cbor(v))
}
Some(Accept::ApplicationFlatbuffers) => {
let v = Value::Array(Array::from(
res.into_iter().map(|x| x.into_value()).collect::<Vec<Value>>(),
));
Ok(Output::flatbuffers(&v))
}
Some(_) => Err(NetError::InvalidType.into()),
},
Err(err) => Err(err.into()),
}
}
fn assert_capabilities(db: &Datastore, session: &Session) -> Result<(), anyhow::Error> {
if !db.allows_http_route(&RouteTarget::Key) {
warn!("Capabilities denied HTTP route request attempt, target: '{}'", &RouteTarget::Key);
return Err(NetError::ForbiddenRoute(RouteTarget::Key.to_string()).into());
}
if !db.allows_query_by_subject(session.au.as_ref()) {
return Err(NetError::ForbiddenRoute(RouteTarget::Key.to_string()).into());
}
Ok(())
}
async fn select_all(
Extension(state): Extension<AppState>,
Extension(session): Extension<Session>,
accept: Option<TypedHeader<Accept>>,
Path(table): Path<String>,
Query(query): Query<QueryOptions>,
) -> Result<impl IntoResponse, ResponseError> {
let ds = &state.datastore;
assert_capabilities(ds, &session).map_err(ResponseError)?;
let _ = check_ns_db(&session).map_err(ResponseError)?;
let sql = match query.fields {
None => "SELECT * FROM type::table($table) LIMIT $limit START $start",
_ => "SELECT type::fields($fields) FROM type::table($table) LIMIT $limit START $start",
};
let vars = vars! {
"table": Value::Table(table.into()),
"start": query.start.unwrap_or(0),
"limit": query.limit.unwrap_or(100),
"fields": Value::Array(Array::from(query.fields.unwrap_or_default().into_iter().map(SurrealValue::into_value).collect::<Vec<Value>>())),
};
execute_and_return(ds, sql, &session, vars, accept.as_deref(), None)
.await
.map_err(ResponseError)
}
async fn create_all(
Extension(state): Extension<AppState>,
Extension(session): Extension<Session>,
accept: Option<TypedHeader<Accept>>,
Path(table): Path<String>,
Query(params): Query<Params>,
body: Bytes,
) -> Result<impl IntoResponse, ResponseError> {
let db = &state.datastore;
assert_capabilities(db, &session).map_err(ResponseError)?;
let _ = check_ns_db(&session).map_err(ResponseError)?;
let data = bytes_to_utf8(&body).context("Non UTF-8 request body").map_err(ResponseError)?;
let sql = "CREATE type::table($table) CONTENT $data";
let vars = Variables::from(BTreeMap::from_iter(map! {
String::from("table") => Value::String(table),
=> params.parse()
}));
execute_and_return(db, sql, &session, vars, accept.as_deref(), Some(data.to_string()))
.await
.map_err(ResponseError)
}
async fn update_all(
Extension(state): Extension<AppState>,
Extension(session): Extension<Session>,
accept: Option<TypedHeader<Accept>>,
Path(table): Path<String>,
Query(params): Query<Params>,
body: Bytes,
) -> Result<impl IntoResponse, impl IntoResponse> {
let db = &state.datastore;
assert_capabilities(db, &session).map_err(ResponseError)?;
let _ = check_ns_db(&session).map_err(ResponseError)?;
let data = bytes_to_utf8(&body).context("Non UTF-8 request body").map_err(ResponseError)?;
let sql = "UPDATE type::table($table) CONTENT $data";
let vars = Variables::from(BTreeMap::from_iter(map! {
String::from("table") => Value::String(table),
=> params.parse()
}));
execute_and_return(db, sql, &session, vars, accept.as_deref(), Some(data.to_string()))
.await
.map_err(ResponseError)
}
async fn modify_all(
Extension(state): Extension<AppState>,
Extension(session): Extension<Session>,
accept: Option<TypedHeader<Accept>>,
Path(table): Path<String>,
Query(params): Query<Params>,
body: Bytes,
) -> Result<impl IntoResponse, impl IntoResponse> {
let db = &state.datastore;
assert_capabilities(db, &session).map_err(ResponseError)?;
let _ = check_ns_db(&session).map_err(ResponseError)?;
let data = bytes_to_utf8(&body).context("Non UTF-8 request body").map_err(ResponseError)?;
let sql = "UPDATE type::table($table) MERGE $data";
let vars = Variables::from(BTreeMap::from_iter(map! {
String::from("table") => Value::String(table),
=> params.parse()
}));
execute_and_return(db, sql, &session, vars, accept.as_deref(), Some(data.to_string()))
.await
.map_err(ResponseError)
}
async fn delete_all(
Extension(state): Extension<AppState>,
Extension(session): Extension<Session>,
accept: Option<TypedHeader<Accept>>,
Path(table): Path<String>,
Query(params): Query<Params>,
) -> Result<impl IntoResponse, impl IntoResponse> {
let db = &state.datastore;
assert_capabilities(db, &session).map_err(ResponseError)?;
let _ = check_ns_db(&session).map_err(ResponseError)?;
let sql = "DELETE type::table($table) RETURN BEFORE";
let vars = Variables::from(BTreeMap::from_iter(map! {
String::from("table") => Value::String(table),
=> params.parse()
}));
execute_and_return(db, sql, &session, vars, accept.as_deref(), None)
.await
.map_err(ResponseError)
}
async fn select_one(
Extension(state): Extension<AppState>,
Extension(session): Extension<Session>,
accept: Option<TypedHeader<Accept>>,
Path((table, id)): Path<(String, String)>,
Query(query): Query<QueryOptions>,
) -> Result<impl IntoResponse, impl IntoResponse> {
let db = &state.datastore;
assert_capabilities(db, &session).map_err(ResponseError)?;
let _ = check_ns_db(&session).map_err(ResponseError)?;
let sql = match query.fields {
None => "SELECT * FROM type::record($table, $id)",
_ => "SELECT type::fields($fields) FROM type::record($table, $id)",
};
let rid = match syn::json(&id) {
Ok(id) => id,
Err(_) => Value::String(id),
};
let vars = Variables::from(BTreeMap::from_iter(map! {
String::from("table") => Value::String(table),
String::from("id") => rid,
String::from("fields") => Value::Array(Array::from(query.fields.unwrap_or_default().into_iter().map(SurrealValue::into_value).collect::<Vec<Value>>())),
}));
execute_and_return(db, sql, &session, vars, accept.as_deref(), None)
.await
.map_err(ResponseError)
}
async fn create_one(
Extension(state): Extension<AppState>,
Extension(session): Extension<Session>,
accept: Option<TypedHeader<Accept>>,
Query(params): Query<Params>,
Path((table, id)): Path<(String, String)>,
body: Bytes,
) -> Result<impl IntoResponse, impl IntoResponse> {
let db = &state.datastore;
assert_capabilities(db, &session).map_err(ResponseError)?;
let _ = check_ns_db(&session).map_err(ResponseError)?;
let data = bytes_to_utf8(&body).context("Non UTF-8 request body").map_err(ResponseError)?;
let rid = match syn::json(&id) {
Ok(id) => id,
Err(_) => Value::String(id),
};
let sql = "CREATE type::record($table, $id) CONTENT $data";
let vars = Variables::from(BTreeMap::from_iter(map! {
String::from("table") => Value::String(table),
String::from("id") => rid,
=> params.parse()
}));
execute_and_return(db, sql, &session, vars, accept.as_deref(), Some(data.to_string()))
.await
.map_err(ResponseError)
}
async fn update_one(
Extension(state): Extension<AppState>,
Extension(session): Extension<Session>,
accept: Option<TypedHeader<Accept>>,
Query(params): Query<Params>,
Path((table, id)): Path<(String, String)>,
body: Bytes,
) -> Result<impl IntoResponse, impl IntoResponse> {
let db = &state.datastore;
assert_capabilities(db, &session).map_err(ResponseError)?;
let _ = check_ns_db(&session).map_err(ResponseError)?;
let data = bytes_to_utf8(&body).context("Non UTF-8 request body").map_err(ResponseError)?;
let rid = match syn::json(&id) {
Ok(id) => id,
Err(_) => Value::String(id),
};
let sql = "UPSERT type::record($table, $id) CONTENT $data";
let vars = Variables::from(BTreeMap::from_iter(map! {
String::from("table") => Value::String(table),
String::from("id") => rid,
=> params.parse()
}));
execute_and_return(db, sql, &session, vars, accept.as_deref(), Some(data.to_string()))
.await
.map_err(ResponseError)
}
async fn modify_one(
Extension(state): Extension<AppState>,
Extension(session): Extension<Session>,
accept: Option<TypedHeader<Accept>>,
Query(params): Query<Params>,
Path((table, id)): Path<(String, String)>,
body: Bytes,
) -> Result<impl IntoResponse, impl IntoResponse> {
let db = &state.datastore;
assert_capabilities(db, &session).map_err(ResponseError)?;
let _ = check_ns_db(&session).map_err(ResponseError)?;
let data = bytes_to_utf8(&body).context("Non UTF-8 request body").map_err(ResponseError)?;
let rid = match syn::json(&id) {
Ok(id) => id,
Err(_) => Value::String(id),
};
let sql = "UPSERT type::record($table, $id) MERGE $data";
let vars = Variables::from(BTreeMap::from_iter(map! {
String::from("table") => Value::String(table),
String::from("id") => rid,
=> params.parse()
}));
execute_and_return(db, sql, &session, vars, accept.as_deref(), Some(data.to_string()))
.await
.map_err(ResponseError)
}
async fn delete_one(
Extension(state): Extension<AppState>,
Extension(session): Extension<Session>,
accept: Option<TypedHeader<Accept>>,
Path((table, id)): Path<(String, String)>,
) -> Result<impl IntoResponse, impl IntoResponse> {
let db = &state.datastore;
assert_capabilities(db, &session).map_err(ResponseError)?;
let _ = check_ns_db(&session).map_err(ResponseError)?;
let sql = "DELETE type::record($table, $id) RETURN BEFORE";
let rid = match syn::json(&id) {
Ok(id) => id,
Err(_) => Value::String(id),
};
let vars = Variables::from(BTreeMap::from_iter(map! {
String::from("table") => Value::String(table),
String::from("id") => rid,
}));
execute_and_return(db, sql, &session, vars, accept.as_deref(), None)
.await
.map_err(ResponseError)
}