panorama/crates/panorama-daemon/src/node.rs

236 lines
5.1 KiB
Rust
Raw Normal View History

2024-06-03 05:02:45 +00:00
use std::collections::{BTreeMap, HashMap};
2024-05-25 15:11:34 +00:00
2024-05-25 10:04:05 +00:00
use axum::{
2024-05-27 05:43:09 +00:00
extract::{Path, Query, State},
2024-05-25 11:45:23 +00:00
http::StatusCode,
2024-06-03 05:02:45 +00:00
routing::{get, post, put},
2024-06-02 23:12:03 +00:00
Json, Router,
2024-05-25 10:04:05 +00:00
};
2024-06-10 19:03:40 +00:00
use chrono::{DateTime, Utc};
2024-06-10 19:52:13 +00:00
use cozo::{DataValue, MultiTransaction};
2024-06-03 05:02:45 +00:00
use panorama_core::state::node::ExtraData;
2024-05-25 10:04:05 +00:00
use serde_json::Value;
2024-06-02 21:37:36 +00:00
use utoipa::{OpenApi, ToSchema};
2024-05-25 10:04:05 +00:00
use crate::{error::AppResult, AppState};
2024-06-02 23:12:03 +00:00
/// Node API
2024-06-02 21:37:36 +00:00
#[derive(OpenApi)]
2024-06-03 05:02:45 +00:00
#[openapi(
paths(get_node, update_node, create_node),
components(schemas(GetNodeResult))
)]
2024-06-02 21:37:36 +00:00
pub(super) struct NodeApi;
2024-06-02 23:12:03 +00:00
pub(super) fn router() -> Router<AppState> {
2024-06-03 05:02:45 +00:00
Router::new()
2024-06-10 19:52:13 +00:00
.route("/", put(create_node))
2024-06-03 05:02:45 +00:00
.route("/:id", get(get_node))
.route("/:id", post(update_node))
2024-06-10 19:52:13 +00:00
.route("/search", get(search_nodes))
2024-06-02 23:12:03 +00:00
}
2024-06-02 21:37:36 +00:00
#[derive(Serialize, Deserialize, ToSchema, Clone)]
struct GetNodeResult {
2024-06-10 19:03:40 +00:00
node_id: String,
fields: HashMap<String, Value>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
2024-06-02 21:37:36 +00:00
}
2024-06-03 05:02:45 +00:00
/// Get node info
///
/// This endpoint retrieves all the fields for a particular node
2024-06-02 21:37:36 +00:00
#[utoipa::path(
get,
path = "/{id}",
responses(
2024-06-03 05:02:45 +00:00
(status = 200, body = [GetNodeResult]),
(status = 404, description = "the node ID provided was not found")
2024-06-02 23:12:03 +00:00
),
params(
("id" = String, Path, description = "Node ID"),
),
2024-06-02 21:37:36 +00:00
)]
2024-05-25 10:04:05 +00:00
pub async fn get_node(
State(state): State<AppState>,
Path(node_id): Path<String>,
2024-05-25 11:45:23 +00:00
) -> AppResult<(StatusCode, Json<Value>)> {
2024-06-10 19:03:40 +00:00
let node_info = state.get_node(&node_id).await?;
2024-05-25 10:04:05 +00:00
2024-05-25 11:45:23 +00:00
Ok((
StatusCode::OK,
Json(json!({
2024-06-10 19:03:40 +00:00
"node_id": node_id,
"fields": node_info.fields,
"created_at": node_info.created_at,
"updated_at": node_info.updated_at,
2024-05-25 11:45:23 +00:00
})),
))
}
2024-05-25 15:11:34 +00:00
#[derive(Deserialize, Debug)]
pub struct UpdateData {
title: Option<String>,
2024-05-27 06:27:17 +00:00
extra_data: Option<ExtraData>,
2024-05-25 15:11:34 +00:00
}
2024-05-25 11:45:23 +00:00
2024-06-03 05:02:45 +00:00
/// Update node info
2024-06-02 23:12:03 +00:00
#[utoipa::path(
post,
path = "/{id}",
responses(
(status = 200)
),
params(
("id" = String, Path, description = "Node ID"),
)
)]
2024-05-25 11:45:23 +00:00
pub async fn update_node(
State(state): State<AppState>,
Path(node_id): Path<String>,
2024-05-25 15:11:34 +00:00
Json(update_data): Json<UpdateData>,
2024-05-25 11:45:23 +00:00
) -> AppResult<Json<Value>> {
2024-05-27 04:07:02 +00:00
let node_id_data = DataValue::from(node_id.clone());
// TODO: Combine these into the same script
2024-05-25 15:11:34 +00:00
2024-05-27 02:56:12 +00:00
let tx = state.db.multi_transaction(true);
2024-05-27 04:35:18 +00:00
if let Some(title) = update_data.title {
let title = DataValue::from(title);
tx.run_script(
"
2024-06-10 19:52:13 +00:00
# Always update the time
?[ id, title ] <- [[ $node_id, $title ]]
:update node { id, title }
",
2024-05-27 04:35:18 +00:00
btmap! {
"node_id".to_owned() => node_id_data.clone(),
"title".to_owned() => title,
},
)?;
}
2024-05-27 02:56:12 +00:00
if let Some(extra_data) = update_data.extra_data {
2024-05-27 06:27:17 +00:00
let result = get_rows_for_extra_keys(&tx, &extra_data)?;
2024-05-27 04:07:02 +00:00
for (key, (relation, field_name, ty)) in result.iter() {
let new_value = extra_data.get(key).unwrap();
// TODO: Make this more generic
let new_value = DataValue::from(new_value.as_str().unwrap());
let query = format!(
"
?[ node_id, {field_name} ] <- [[$node_id, $input_data]]
:update {relation} {{ node_id, {field_name} }}
"
);
2024-05-27 06:27:17 +00:00
2024-05-27 04:07:02 +00:00
let result = tx.run_script(
&query,
btmap! {
"node_id".to_owned() => node_id_data.clone(),
"input_data".to_owned() => new_value,
},
)?;
}
2024-05-27 02:56:12 +00:00
}
2024-05-27 04:07:02 +00:00
tx.run_script(
"
# Always update the time
?[ id, updated_at ] <- [[ $node_id, now() ]]
:update node { id, updated_at }
",
btmap! {
"node_id".to_owned() => node_id_data,
},
2024-05-27 04:35:18 +00:00
)?;
2024-05-27 04:07:02 +00:00
2024-05-27 02:56:12 +00:00
tx.commit()?;
2024-05-25 11:45:23 +00:00
Ok(Json(json!({})))
2024-05-25 10:04:05 +00:00
}
2024-05-25 15:11:34 +00:00
2024-05-27 06:27:17 +00:00
#[derive(Debug, Deserialize)]
pub struct CreateNodeOpts {
// TODO: Allow submitting a string
// id: Option<String>,
#[serde(rename = "type")]
ty: String,
extra_data: Option<ExtraData>,
}
2024-06-03 05:02:45 +00:00
#[utoipa::path(
put,
path = "/",
2024-06-10 19:52:13 +00:00
responses((status = 200)),
2024-06-03 05:02:45 +00:00
)]
2024-05-27 06:27:17 +00:00
pub async fn create_node(
State(state): State<AppState>,
Json(opts): Json<CreateNodeOpts>,
) -> AppResult<Json<Value>> {
2024-06-10 19:52:13 +00:00
let node_info = state
.create_or_update_node(opts.ty, opts.extra_data)
.await?;
2024-05-27 06:27:17 +00:00
Ok(Json(json!({
2024-06-10 19:52:13 +00:00
"node_id": node_info.node_id.to_string(),
2024-05-27 06:27:17 +00:00
})))
2024-05-27 05:43:09 +00:00
}
#[derive(Deserialize)]
pub struct SearchQuery {
query: String,
}
2024-06-10 19:52:13 +00:00
#[utoipa::path(
get,
path = "/search",
responses((status = 200)),
)]
2024-05-27 05:43:09 +00:00
pub async fn search_nodes(
State(state): State<AppState>,
Query(query): Query<SearchQuery>,
) -> AppResult<Json<Value>> {
2024-06-10 19:52:13 +00:00
let search_result = state.search_nodes(query.query).await?;
2024-05-27 05:43:09 +00:00
Ok(Json(json!({
2024-06-10 19:52:13 +00:00
"results": search_result
2024-05-27 05:43:09 +00:00
})))
}
2024-05-27 06:27:17 +00:00
fn get_rows_for_extra_keys(
tx: &MultiTransaction,
extra_data: &ExtraData,
) -> AppResult<HashMap<String, (String, String, String)>> {
let result = tx.run_script(
"
?[key, relation, field_name, type] :=
*fqkey_to_dbkey{key, relation, field_name, type},
is_in(key, $keys)
",
btmap! {
"keys".to_owned() => DataValue::List(
extra_data
.keys()
.map(|s| DataValue::from(s.as_str()))
.collect::<Vec<_>>()
),
},
)?;
let s = |s: &DataValue| s.get_str().unwrap().to_owned();
Ok(
result
.rows
.into_iter()
.map(|row| (s(&row[0]), (s(&row[1]), s(&row[2]), s(&row[3]))))
.collect::<HashMap<_, _>>(),
)
}