This commit is contained in:
Michael Zhang 2024-06-13 18:23:17 -04:00
parent 791349a2c4
commit e0b7ebedac
12 changed files with 128 additions and 88 deletions

3
Cargo.lock generated
View file

@ -3630,6 +3630,7 @@ dependencies = [
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
"tauri-plugin-window-state", "tauri-plugin-window-state",
"tokio", "tokio",
"tracing-subscriber",
] ]
[[package]] [[package]]
@ -3674,6 +3675,7 @@ dependencies = [
"tokio", "tokio",
"tower", "tower",
"tower-http", "tower-http",
"tracing-subscriber",
"utoipa", "utoipa",
"utoipa-scalar", "utoipa-scalar",
"utoipa-swagger-ui", "utoipa-swagger-ui",
@ -6038,6 +6040,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]

View file

@ -29,6 +29,7 @@ tauri-plugin-shell = "2.0.0-beta.7"
tauri-plugin-single-instance = "2.0.0-beta.9" tauri-plugin-single-instance = "2.0.0-beta.9"
tauri-plugin-window-state = "2.0.0-beta" tauri-plugin-window-state = "2.0.0-beta"
tokio = { version = "1.38.0", features = ["full"] } tokio = { version = "1.38.0", features = ["full"] }
tracing-subscriber = "0.3.18"
[features] [features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!

View file

@ -18,6 +18,7 @@ enum Command {
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
tracing_subscriber::fmt::init();
let opt = Opt::parse(); let opt = Opt::parse();
match opt.command { match opt.command {

View file

@ -9,6 +9,7 @@ import { parse as parseDate, format as formatDate } from "date-fns";
import { useDebounce } from "use-debounce"; import { useDebounce } from "use-debounce";
const JOURNAL_PAGE_CONTENT_FIELD_NAME = "panorama/journal/page/content"; const JOURNAL_PAGE_CONTENT_FIELD_NAME = "panorama/journal/page/content";
const JOURNAL_PAGE_TITLE_FIELD_NAME = "panorama/journal/page/title";
export interface JournalPageProps { export interface JournalPageProps {
id: string; id: string;
@ -33,44 +34,41 @@ export default function JournalPage({ id, data }: JournalPageProps) {
const previous = usePrevious(valueToSave); const previous = usePrevious(valueToSave);
const changed = valueToSave !== previous; const changed = valueToSave !== previous;
const [mode, setMode] = useState<PreviewType>("preview"); const [mode, setMode] = useState<PreviewType>("preview");
const [title, setTitle] = useState(() => data.title); const [title, setTitle] = useState(
() => data?.fields?.[JOURNAL_PAGE_TITLE_FIELD_NAME],
);
const [isEditingTitle, setIsEditingTitle] = useState(false); const [isEditingTitle, setIsEditingTitle] = useState(false);
const saveData = useCallback(async () => {
const extra_data = {
[JOURNAL_PAGE_TITLE_FIELD_NAME]: title,
[JOURNAL_PAGE_CONTENT_FIELD_NAME]: valueToSave,
};
console.log("extra Data", extra_data);
const resp = await fetch(`http://localhost:5195/node/${id}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ extra_data }),
});
const data = await resp.text();
console.log("result", data);
}, [title, valueToSave, id]);
useEffect(() => { useEffect(() => {
if (changed) { if (changed) {
(async () => { (async () => {
console.log("Saving..."); await saveData();
const resp = await fetch(`http://localhost:5195/node/${id}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
extra_data: {
"panorama/journal/page/content": valueToSave,
},
}),
});
const data = await resp.text();
console.log("result", data);
queryClient.invalidateQueries({ queryKey: ["fetchNode", id] }); queryClient.invalidateQueries({ queryKey: ["fetchNode", id] });
})(); })();
} }
}, [id, changed, valueToSave, queryClient]); }, [changed, queryClient, saveData]);
const saveChangedTitle = useCallback(() => { const saveChangedTitle = useCallback(() => {
(async () => { (async () => {
const resp = await fetch(`http://localhost:5195/node/${id}`, { await saveData();
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title: title }),
});
setIsEditingTitle(false); setIsEditingTitle(false);
})(); })();
}, [title, id]); }, [saveData]);
return ( return (
<> <>

View file

@ -10,7 +10,7 @@ chrono = { version = "0.4.38", features = ["serde"] }
cozo = { version = "0.7.6", features = ["storage-rocksdb"] } cozo = { version = "0.7.6", features = ["storage-rocksdb"] }
futures = "0.3.30" futures = "0.3.30"
itertools = "0.13.0" itertools = "0.13.0"
miette = "5.5.0" miette = { version = "5.5.0", features = ["fancy", "backtrace"] }
serde = { version = "1.0.203", features = ["derive"] } serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.117" serde_json = "1.0.117"
sugars = "3.0.1" sugars = "3.0.1"

View file

@ -107,7 +107,6 @@ fn migration_01(db: &DbInstance) -> Result<()> {
id: String id: String
=> =>
type: String, type: String,
title: String? default null,
created_at: Float default now(), created_at: Float default now(),
updated_at: Float default now(), updated_at: Float default now(),
extra_data: Json default {}, extra_data: Json default {},
@ -131,6 +130,8 @@ fn migration_01(db: &DbInstance) -> Result<()> {
} }
{ {
?[key, relation, field_name, type, is_fts_enabled] <- [ ?[key, relation, field_name, type, is_fts_enabled] <- [
['panorama/journal/page/day', 'journal_day', 'day', 'string', false],
['panorama/journal/page/title', 'journal', 'title', 'string', true],
['panorama/journal/page/content', 'journal', 'content', 'string', true], ['panorama/journal/page/content', 'journal', 'content', 'string', true],
['panorama/mail/config/imap_hostname', 'mail_config', 'imap_hostname', 'string', false], ['panorama/mail/config/imap_hostname', 'mail_config', 'imap_hostname', 'string', false],
['panorama/mail/config/imap_port', 'mail_config', 'imap_port', 'int', false], ['panorama/mail/config/imap_port', 'mail_config', 'imap_port', 'int', false],
@ -138,13 +139,13 @@ fn migration_01(db: &DbInstance) -> Result<()> {
['panorama/mail/config/imap_password', 'mail_config', 'imap_password', 'string', false], ['panorama/mail/config/imap_password', 'mail_config', 'imap_password', 'string', false],
['panorama/mail/message/body', 'message', 'body', 'string', true], ['panorama/mail/message/body', 'message', 'body', 'string', true],
['panorama/mail/message/subject', 'message', 'subject', 'string', true], ['panorama/mail/message/subject', 'message', 'subject', 'string', true],
['panorama/mail/message/message_id', 'message', 'message_id', 'string', true], ['panorama/mail/message/message_id', 'message', 'message_id', 'string', false],
] ]
:put fqkey_to_dbkey { key, relation, field_name, type, is_fts_enabled } :put fqkey_to_dbkey { key, relation, field_name, type, is_fts_enabled }
} }
# Create journal type # Create journal type
{ :create journal { node_id: String => content: String } } { :create journal { node_id: String => title: String default '', content: String } }
{ :create journal_day { day: String => node_id: String } } { :create journal_day { day: String => node_id: String } }
# Mail # Mail

View file

@ -7,13 +7,15 @@ use uuid::Uuid;
use crate::{AppState, NodeId}; use crate::{AppState, NodeId};
use super::node::CreateOrUpdate;
impl AppState { impl AppState {
pub async fn get_todays_journal_id(&self) -> Result<NodeId> { pub async fn get_todays_journal_id(&self) -> Result<NodeId> {
let today = todays_date(); let today = todays_date();
let result = self.db.run_script( let result = self.db.run_script(
" "
?[node_id] := *journal_day[day, node_id], day = $day ?[node_id] := *journal_day{day, node_id}, day = $day
", ",
btmap! { btmap! {
"day".to_owned() => today.clone().into(), "day".to_owned() => today.clone().into(),
@ -24,33 +26,46 @@ impl AppState {
// TODO: Do this check on the server side // TODO: Do this check on the server side
if result.rows.len() == 0 { if result.rows.len() == 0 {
// Insert a new one // Insert a new one
let uuid = Uuid::now_v7(); // let uuid = Uuid::now_v7();
let node_id = uuid.to_string(); // let node_id = uuid.to_string();
self.db.run_script( let node_info = self
" .create_or_update_node(
{ CreateOrUpdate::Create {
?[id, title, type] <- [[$node_id, $title, 'panorama/journal/page']] r#type: "panorama/journal/page".to_owned(),
:put node { id, title, type } },
} Some(btmap! {
{ "panorama/journal/page/day".to_owned() => today.clone().into(),
?[node_id, content] <- [[$node_id, '']] "panorama/journal/page/content".to_owned() => "".to_owned().into(),
:put journal { node_id => content } "panorama/journal/page/title".to_owned() => today.clone().into(),
} }),
{ )
?[day, node_id] <- [[$day, $node_id]] .await?;
: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)); // self.db.run_script(
// "
// {
// ?[id, type] <- [[$node_id, 'panorama/journal/page']]
// :put node { id, type }
// }
// {
// ?[node_id, title, content] <- [[$node_id, $title, '']]
// :put journal { node_id => title, 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(node_info.node_id);
} }
let node_id = result.rows[0][0].get_str().unwrap(); let node_id = result.rows[0][0].get_str().unwrap();

View file

@ -12,7 +12,7 @@ use tantivy::{
collector::TopDocs, collector::TopDocs,
query::QueryParser, query::QueryParser,
schema::{OwnedValue, Value as _}, schema::{OwnedValue, Value as _},
Document, TantivyDocument, Document, TantivyDocument, Term,
}; };
use uuid::Uuid; use uuid::Uuid;
@ -30,6 +30,7 @@ pub struct NodeInfo {
pub fields: Option<HashMap<String, Value>>, pub fields: Option<HashMap<String, Value>>,
} }
#[derive(Debug)]
pub struct FieldInfo { pub struct FieldInfo {
pub relation_name: String, pub relation_name: String,
pub relation_field: String, pub relation_field: String,
@ -157,6 +158,7 @@ impl AppState {
} }
} }
#[derive(Debug)]
pub enum CreateOrUpdate { pub enum CreateOrUpdate {
Create { r#type: String }, Create { r#type: String },
Update { node_id: NodeId }, Update { node_id: NodeId },
@ -175,10 +177,17 @@ impl AppState {
}; };
let node_id = node_id.to_string(); let node_id = node_id.to_string();
let action = match opts {
CreateOrUpdate::Create { .. } => "put",
CreateOrUpdate::Update { .. } => "update",
};
println!("Request: {opts:?} {extra_data:?}");
let tx = self.db.multi_transaction(true); let tx = self.db.multi_transaction(true);
let (created_at, updated_at) = match opts { let (created_at, updated_at) = match opts {
CreateOrUpdate::Create { r#type } => { CreateOrUpdate::Create { ref r#type } => {
let node_result = tx.run_script( let node_result = tx.run_script(
" "
?[id, type] <- [[$node_id, $type]] ?[id, type] <- [[$node_id, $type]]
@ -187,16 +196,15 @@ impl AppState {
", ",
btmap! { btmap! {
"node_id".to_owned() => DataValue::from(node_id.clone()), "node_id".to_owned() => DataValue::from(node_id.clone()),
"type".to_owned() => DataValue::from(r#type), "type".to_owned() => DataValue::from(r#type.to_owned()),
}, },
)?; )?;
println!("ROWS(1): {:?}", node_result);
let created_at = DateTime::from_timestamp_millis( let created_at = DateTime::from_timestamp_millis(
(node_result.rows[0][4].get_float().unwrap() * 1000.0) as i64, (node_result.rows[0][3].get_float().unwrap() * 1000.0) as i64,
) )
.unwrap(); .unwrap();
let updated_at = DateTime::from_timestamp_millis( let updated_at = DateTime::from_timestamp_millis(
(node_result.rows[0][5].get_float().unwrap() * 1000.0) as i64, (node_result.rows[0][4].get_float().unwrap() * 1000.0) as i64,
) )
.unwrap(); .unwrap();
(created_at, updated_at) (created_at, updated_at)
@ -211,7 +219,6 @@ impl AppState {
"node_id".to_owned() => DataValue::from(node_id.clone()), "node_id".to_owned() => DataValue::from(node_id.clone()),
}, },
)?; )?;
println!("ROWS(2): {:?}", node_result);
let created_at = DateTime::from_timestamp_millis( let created_at = DateTime::from_timestamp_millis(
(node_result.rows[0][2].get_float().unwrap() * 1000.0) as i64, (node_result.rows[0][2].get_float().unwrap() * 1000.0) as i64,
) )
@ -230,6 +237,7 @@ impl AppState {
.get_by_left("node_id") .get_by_left("node_id")
.unwrap() .unwrap()
.clone(); .clone();
if !extra_data.is_empty() { if !extra_data.is_empty() {
let keys = extra_data.keys().map(|s| s.to_owned()).collect::<Vec<_>>(); let keys = extra_data.keys().map(|s| s.to_owned()).collect::<Vec<_>>();
let field_mapping = let field_mapping =
@ -279,6 +287,11 @@ impl AppState {
let mut writer = let mut writer =
self.tantivy_index.writer(15_000_000).into_diagnostic()?; self.tantivy_index.writer(15_000_000).into_diagnostic()?;
let delete_term =
Term::from_field_text(node_id_field.clone(), &node_id);
writer.delete_term(delete_term);
writer.add_document(doc).into_diagnostic()?; writer.add_document(doc).into_diagnostic()?;
writer.commit().into_diagnostic()?; writer.commit().into_diagnostic()?;
drop(writer); drop(writer);
@ -286,27 +299,27 @@ impl AppState {
let keys = fields_mapping.keys().collect::<Vec<_>>(); let keys = fields_mapping.keys().collect::<Vec<_>>();
let keys_joined = keys.iter().join(", "); let keys_joined = keys.iter().join(", ");
let query = format!( if !keys.is_empty() {
" let query = format!(
"
?[ node_id, {keys_joined} ] <- [$input_data] ?[ node_id, {keys_joined} ] <- [$input_data]
:put {relation} {{ node_id, {keys_joined} }} :{action} {relation} {{ node_id, {keys_joined} }}
" "
); );
let mut params = vec![]; let mut params = vec![];
params.push(DataValue::from(node_id.clone())); params.push(DataValue::from(node_id.clone()));
for key in keys { for key in keys {
params.push(fields_mapping[key].clone()); params.push(fields_mapping[key].clone());
}
let result = tx.run_script(
&query,
btmap! {
"input_data".to_owned() => DataValue::List(params),
},
);
} }
println!("Query: {:?} \n {:?}", query, params);
let result = tx.run_script(
&query,
btmap! {
"input_data".to_owned() => DataValue::List(params),
},
)?;
} }
let input = DataValue::List( let input = DataValue::List(
@ -320,11 +333,12 @@ impl AppState {
}) })
.collect_vec(), .collect_vec(),
); );
tx.run_script( tx.run_script(
" "
?[key, id] <- $input_data ?[key, id] <- $input_data
:put node_has_key { key, id } :put node_has_key { key, id }
", ",
btmap! { btmap! {
"input_data".to_owned() => input "input_data".to_owned() => input
}, },

View file

@ -14,7 +14,7 @@ csv = "1.3.0"
dirs = "5.0.1" dirs = "5.0.1"
futures = "0.3.30" futures = "0.3.30"
itertools = "0.13.0" itertools = "0.13.0"
miette = "5.5.0" miette = { version = "5.5.0", features = ["fancy", "backtrace"] }
panorama-core = { path = "../panorama-core" } panorama-core = { path = "../panorama-core" }
serde = { version = "1.0.202", features = ["derive"] } serde = { version = "1.0.202", features = ["derive"] }
serde_json = "1.0.117" serde_json = "1.0.117"
@ -22,7 +22,8 @@ sugars = "3.0.1"
tantivy = { version = "0.22.0", features = ["zstd"] } tantivy = { version = "0.22.0", features = ["zstd"] }
tokio = { version = "1.37.0", features = ["full"] } tokio = { version = "1.37.0", features = ["full"] }
tower = "0.4.13" tower = "0.4.13"
tower-http = { version = "0.5.2", features = ["cors"] } tower-http = { version = "0.5.2", features = ["cors", "trace"] }
tracing-subscriber = "0.3.18"
uuid = { version = "1.8.0", features = ["v7"] } uuid = { version = "1.8.0", features = ["v7"] }
[dependencies.utoipa] [dependencies.utoipa]

View file

@ -20,7 +20,10 @@ use miette::{IntoDiagnostic, Result};
use panorama_core::AppState; use panorama_core::AppState;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tower::ServiceBuilder; use tower::ServiceBuilder;
use tower_http::cors::{self, CorsLayer}; use tower_http::{
cors::{self, CorsLayer},
trace::TraceLayer,
};
use utoipa::OpenApi; use utoipa::OpenApi;
use utoipa_scalar::{Scalar, Servable}; use utoipa_scalar::{Scalar, Servable};
@ -47,11 +50,13 @@ pub async fn run() -> Result<()> {
let state = AppState::new(&panorama_dir).await?; let state = AppState::new(&panorama_dir).await?;
let cors = CorsLayer::new() let cors_layer = CorsLayer::new()
.allow_methods([Method::GET, Method::POST, Method::PUT]) .allow_methods([Method::GET, Method::POST, Method::PUT])
.allow_headers(cors::Any) .allow_headers(cors::Any)
.allow_origin(cors::Any); .allow_origin(cors::Any);
let trace_layer = TraceLayer::new_for_http();
// build our application with a single route // build our application with a single route
let app = Router::new() let app = Router::new()
.merge(Scalar::with_url("/api/docs", ApiDoc::openapi())) .merge(Scalar::with_url("/api/docs", ApiDoc::openapi()))
@ -61,7 +66,8 @@ pub async fn run() -> Result<()> {
.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))
.route("/mail", get(get_mail)) .route("/mail", get(get_mail))
.layer(ServiceBuilder::new().layer(cors)) .layer(ServiceBuilder::new().layer(cors_layer))
.layer(ServiceBuilder::new().layer(trace_layer))
.with_state(state.clone()); .with_state(state.clone());
let listener = TcpListener::bind("0.0.0.0:5195").await.into_diagnostic()?; let listener = TcpListener::bind("0.0.0.0:5195").await.into_diagnostic()?;

View file

@ -2,6 +2,7 @@ use miette::Result;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
panorama_daemon::run().await?; panorama_daemon::run().await?;
Ok(()) Ok(())
} }

View file

@ -80,7 +80,6 @@ pub async fn get_node(
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct UpdateData { pub struct UpdateData {
title: Option<String>,
extra_data: Option<ExtraData>, extra_data: Option<ExtraData>,
} }