This commit is contained in:
Michael Zhang 2024-06-10 15:52:13 -04:00
parent 9ea6ee5248
commit 7fc3917134
8 changed files with 111 additions and 194 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@ node_modules
dist dist
target target
.DS_Store .DS_Store
/export

View file

@ -1,9 +1,65 @@
use miette::Result; use std::str::FromStr;
use chrono::Local;
use cozo::ScriptMutability;
use miette::{IntoDiagnostic, Result};
use uuid::Uuid;
use crate::{AppState, NodeId}; use crate::{AppState, NodeId};
impl AppState { impl AppState {
pub fn get_todays_journal_id(&self) -> Result<NodeId> { pub async fn get_todays_journal_id(&self) -> Result<NodeId> {
todo!() let today = todays_date();
let result = self.db.run_script(
"
?[node_id] := *journal_day[day, node_id], day = $day
",
btmap! {
"day".to_owned() => today.clone().into(),
},
ScriptMutability::Immutable,
)?;
// TODO: Do this check on the server side
if result.rows.len() == 0 {
// Insert a new one
let uuid = Uuid::now_v7();
let node_id = uuid.to_string();
self.db.run_script(
"
{
?[id, title, type] <- [[$node_id, $title, 'panorama/journal/page']]
:put node { id, title, type }
}
{
?[node_id, content] <- [[$node_id, '']]
:put journal { node_id => content }
}
{
?[day, node_id] <- [[$day, $node_id]]
:put journal_day { day => node_id }
}
",
btmap! {
"node_id".to_owned() => node_id.clone().into(),
"day".to_owned() => today.clone().into(),
"title".to_owned() => today.clone().into(),
},
ScriptMutability::Mutable,
)?;
return Ok(NodeId(uuid));
}
let node_id = result.rows[0][0].get_str().unwrap();
Ok(NodeId(Uuid::from_str(node_id).into_diagnostic()?))
} }
} }
fn todays_date() -> String {
let now = Local::now();
let date = now.date_naive();
date.format("%Y-%m-%d").to_string()
}

View file

@ -7,7 +7,6 @@ use std::{collections::HashMap, fs, path::Path};
use cozo::DbInstance; use cozo::DbInstance;
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use node::FieldMapping;
use tantivy::{ use tantivy::{
directory::MmapDirectory, directory::MmapDirectory,
schema::{Field, Schema, STORED, STRING, TEXT}, schema::{Field, Schema, STORED, STRING, TEXT},

View file

@ -1,17 +1,15 @@
use std::{ use std::{
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
fmt::write,
str::FromStr, str::FromStr,
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use cozo::{DataValue, MultiTransaction, NamedRows, ScriptMutability}; use cozo::{DataValue, MultiTransaction, NamedRows};
use itertools::Itertools; use itertools::Itertools;
use miette::{IntoDiagnostic, Result}; use miette::{bail, IntoDiagnostic, Result};
use serde_json::Value; use serde_json::Value;
use tantivy::{ use tantivy::{
collector::TopDocs, collector::TopDocs,
doc,
query::QueryParser, query::QueryParser,
schema::{OwnedValue, Value as _}, schema::{OwnedValue, Value as _},
Document, TantivyDocument, Document, TantivyDocument,
@ -122,6 +120,10 @@ impl AppState {
btmap! { "node_id".to_owned() => node_id.to_string().into(), }, btmap! { "node_id".to_owned() => node_id.to_string().into(), },
)?; )?;
if result.rows.is_empty() {
bail!("Not found")
}
let created_at = DateTime::from_timestamp_millis( let created_at = DateTime::from_timestamp_millis(
(result.rows[0][2].get_float().unwrap() * 1000.0) as i64, (result.rows[0][2].get_float().unwrap() * 1000.0) as i64,
) )
@ -248,6 +250,8 @@ impl AppState {
params.push(fields_mapping[key].clone()); params.push(fields_mapping[key].clone());
} }
println!("Query: {:?} \n {:?}", query, params);
let result = tx.run_script( let result = tx.run_script(
&query, &query,
btmap! { btmap! {
@ -334,11 +338,22 @@ impl AppState {
.unwrap() .unwrap()
.as_str() .as_str()
.unwrap(); .unwrap();
let all_fields = retrieved_doc.get_sorted_field_values();
let node_id = NodeId(Uuid::from_str(node_id).unwrap()); let node_id = NodeId(Uuid::from_str(node_id).unwrap());
let fields = all_fields
.into_iter()
.map(|(field, value)| {
(
serde_json::to_string(&field).unwrap(),
serde_json::to_string(&value).unwrap(),
)
})
.collect::<HashMap<_, _>>();
( (
node_id, node_id,
json!({ json!({
"score": score, "score": score,
"fields": fields,
}), }),
) )
}) })

View file

@ -27,61 +27,9 @@ pub(super) fn router() -> Router<AppState> {
pub async fn get_todays_journal_id( pub async fn get_todays_journal_id(
State(state): State<AppState>, State(state): State<AppState>,
) -> AppResult<Json<Value>> { ) -> AppResult<Json<Value>> {
let today = todays_date(); let node_id = state.get_todays_journal_id().await?;
let result = state.db.run_script(
"
?[node_id] := *journal_day[day, node_id], day = $day
",
btmap! {
"day".to_owned() => today.clone().into(),
},
ScriptMutability::Immutable,
)?;
// TODO: Do this check on the server side
if result.rows.len() == 0 {
// Insert a new one
let uuid = Uuid::now_v7();
let node_id = uuid.to_string();
state.db.run_script(
"
{
?[id, title, type] <- [[$node_id, $title, 'panorama/journal/page']]
:put node { id, title, type }
}
{
?[node_id, content] <- [[$node_id, '']]
:put journal { node_id => content }
}
{
?[day, node_id] <- [[$day, $node_id]]
:put journal_day { day => node_id }
}
",
btmap! {
"node_id".to_owned() => node_id.clone().into(),
"day".to_owned() => today.clone().into(),
"title".to_owned() => today.clone().into(),
},
ScriptMutability::Mutable,
)?;
return Ok(Json(json!({
"node_id": node_id
})));
}
let node_id = result.rows[0][0].get_str().unwrap();
Ok(Json(json!({ Ok(Json(json!({
"node_id": node_id, "node_id": node_id.to_string(),
"day": today,
}))) })))
} }
fn todays_date() -> String {
let now = Local::now();
let date = now.date_naive();
date.format("%Y-%m-%d").to_string()
}

View file

@ -15,11 +15,7 @@ mod node;
use std::fs; use std::fs;
use axum::{ use axum::{http::Method, routing::get, Router};
http::Method,
routing::{get, put},
Router,
};
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use panorama_core::AppState; use panorama_core::AppState;
use tokio::net::TcpListener; use tokio::net::TcpListener;
@ -62,7 +58,6 @@ async fn main() -> Result<()> {
.merge(Scalar::with_url("/api/docs", ApiDoc::openapi())) .merge(Scalar::with_url("/api/docs", ApiDoc::openapi()))
.route("/", get(|| async { "Hello, World!" })) .route("/", get(|| async { "Hello, World!" }))
.route("/export", get(export)) .route("/export", get(export))
.route("/node/search", get(search_nodes))
.nest("/node", node::router().with_state(state.clone())) .nest("/node", node::router().with_state(state.clone()))
.nest("/journal", journal::router().with_state(state.clone())) .nest("/journal", journal::router().with_state(state.clone()))
.route("/mail/config", get(get_mail_config)) .route("/mail/config", get(get_mail_config))

View file

@ -7,12 +7,10 @@ use axum::{
Json, Router, Json, Router,
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use cozo::{DataValue, MultiTransaction, ScriptMutability}; use cozo::{DataValue, MultiTransaction};
use itertools::Itertools;
use panorama_core::state::node::ExtraData; use panorama_core::state::node::ExtraData;
use serde_json::Value; use serde_json::Value;
use utoipa::{OpenApi, ToSchema}; use utoipa::{OpenApi, ToSchema};
use uuid::Uuid;
use crate::{error::AppResult, AppState}; use crate::{error::AppResult, AppState};
@ -26,9 +24,10 @@ pub(super) struct NodeApi;
pub(super) fn router() -> Router<AppState> { pub(super) fn router() -> Router<AppState> {
Router::new() Router::new()
.route("/", put(create_node))
.route("/:id", get(get_node)) .route("/:id", get(get_node))
.route("/:id", post(update_node)) .route("/:id", post(update_node))
.route("/", put(create_node)) .route("/search", get(search_nodes))
} }
#[derive(Serialize, Deserialize, ToSchema, Clone)] #[derive(Serialize, Deserialize, ToSchema, Clone)]
@ -168,83 +167,18 @@ pub struct CreateNodeOpts {
#[utoipa::path( #[utoipa::path(
put, put,
path = "/", path = "/",
responses( responses((status = 200)),
(status = 200)
),
params(
("id" = String, Path, description = "Node ID"),
)
)] )]
pub async fn create_node( pub async fn create_node(
State(state): State<AppState>, State(state): State<AppState>,
Json(opts): Json<CreateNodeOpts>, Json(opts): Json<CreateNodeOpts>,
) -> AppResult<Json<Value>> { ) -> AppResult<Json<Value>> {
let node_id = Uuid::now_v7(); let node_info = state
let node_id = node_id.to_string(); .create_or_update_node(opts.ty, opts.extra_data)
.await?;
let tx = state.db.multi_transaction(true);
let result = tx.run_script(
"
?[id, type] <- [[$node_id, $type]]
:put node { id, type }
",
btmap! {
"node_id".to_owned() => DataValue::from(node_id.clone()),
"type".to_owned() => DataValue::from(opts.ty),
},
);
if let Some(extra_data) = opts.extra_data {
let result = get_rows_for_extra_keys(&tx, &extra_data)?;
let result_by_relation = result
.iter()
.into_group_map_by(|(key, (relation, field_name, ty))| relation);
for (relation, fields) in result_by_relation.iter() {
let fields_mapping = fields
.into_iter()
.map(|(key, (_, field_name, ty))| {
let new_value = extra_data.get(*key).unwrap();
// TODO: Make this more generic
let new_value = match ty.as_str() {
"int" => DataValue::from(new_value.as_i64().unwrap()),
_ => DataValue::from(new_value.as_str().unwrap()),
};
(field_name.to_owned(), new_value)
})
.collect::<BTreeMap<_, _>>();
let keys = fields_mapping.keys().collect::<Vec<_>>();
let keys_joined = keys.iter().join(", ");
let query = format!(
"
?[ node_id, {keys_joined} ] <- [$input_data]
:insert {relation} {{ node_id, {keys_joined} }}
"
);
let mut params = vec![];
params.push(DataValue::from(node_id.clone()));
for key in keys {
params.push(fields_mapping[key].clone());
}
let result = tx.run_script(
&query,
btmap! {
"input_data".to_owned() => DataValue::List(params),
},
)?;
}
}
tx.commit()?;
Ok(Json(json!({ Ok(Json(json!({
"node_id": node_id, "node_id": node_info.node_id.to_string(),
}))) })))
} }
@ -253,50 +187,19 @@ pub struct SearchQuery {
query: String, query: String,
} }
#[utoipa::path(
get,
path = "/search",
responses((status = 200)),
)]
pub async fn search_nodes( pub async fn search_nodes(
State(state): State<AppState>, State(state): State<AppState>,
Query(query): Query<SearchQuery>, Query(query): Query<SearchQuery>,
) -> AppResult<Json<Value>> { ) -> AppResult<Json<Value>> {
// TODO: This is temporary, there may be more ways to search so tacking on * let search_result = state.search_nodes(query.query).await?;
// at the end may destroy some queries
let query = format!("{}*", query.query);
let results = state.db.run_script(
"
results[node_id, content, score] := ~journal:text_index {node_id, content, |
query: $q,
k: 10,
score_kind: 'tf_idf',
bind_score: score
}
?[node_id, content, title, score] :=
results[node_id, content, score],
*node{ id: node_id, title }
:order -score
",
btmap! {
"q".to_owned() => DataValue::from(query),
},
ScriptMutability::Immutable,
)?;
let results = results
.rows
.into_iter()
.map(|row| {
json!({
"node_id": row[0].get_str().unwrap(),
"content": row[1].get_str().unwrap(),
"title": row[2].get_str().unwrap(),
"score": row[3].get_float().unwrap(),
})
})
.collect::<Vec<_>>();
Ok(Json(json!({ Ok(Json(json!({
"results": results "results": search_result
}))) })))
} }