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-window-state",
"tokio",
"tracing-subscriber",
]
[[package]]
@ -3674,6 +3675,7 @@ dependencies = [
"tokio",
"tower",
"tower-http",
"tracing-subscriber",
"utoipa",
"utoipa-scalar",
"utoipa-swagger-ui",
@ -6038,6 +6040,7 @@ dependencies = [
"pin-project-lite",
"tower-layer",
"tower-service",
"tracing",
]
[[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-window-state = "2.0.0-beta"
tokio = { version = "1.38.0", features = ["full"] }
tracing-subscriber = "0.3.18"
[features]
# 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]
async fn main() {
tracing_subscriber::fmt::init();
let opt = Opt::parse();
match opt.command {

View file

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

View file

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

View file

@ -107,7 +107,6 @@ fn migration_01(db: &DbInstance) -> Result<()> {
id: String
=>
type: String,
title: String? default null,
created_at: Float default now(),
updated_at: Float default now(),
extra_data: Json default {},
@ -131,6 +130,8 @@ fn migration_01(db: &DbInstance) -> Result<()> {
}
{
?[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/mail/config/imap_hostname', 'mail_config', 'imap_hostname', 'string', 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/message/body', 'message', 'body', '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 }
}
# 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 } }
# Mail

View file

@ -7,13 +7,15 @@ use uuid::Uuid;
use crate::{AppState, NodeId};
use super::node::CreateOrUpdate;
impl AppState {
pub async fn get_todays_journal_id(&self) -> Result<NodeId> {
let today = todays_date();
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! {
"day".to_owned() => today.clone().into(),
@ -24,33 +26,46 @@ impl AppState {
// 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();
// 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,
)?;
let node_info = self
.create_or_update_node(
CreateOrUpdate::Create {
r#type: "panorama/journal/page".to_owned(),
},
Some(btmap! {
"panorama/journal/page/day".to_owned() => today.clone().into(),
"panorama/journal/page/content".to_owned() => "".to_owned().into(),
"panorama/journal/page/title".to_owned() => today.clone().into(),
}),
)
.await?;
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();

View file

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

View file

@ -14,7 +14,7 @@ csv = "1.3.0"
dirs = "5.0.1"
futures = "0.3.30"
itertools = "0.13.0"
miette = "5.5.0"
miette = { version = "5.5.0", features = ["fancy", "backtrace"] }
panorama-core = { path = "../panorama-core" }
serde = { version = "1.0.202", features = ["derive"] }
serde_json = "1.0.117"
@ -22,7 +22,8 @@ sugars = "3.0.1"
tantivy = { version = "0.22.0", features = ["zstd"] }
tokio = { version = "1.37.0", features = ["full"] }
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"] }
[dependencies.utoipa]

View file

@ -20,7 +20,10 @@ use miette::{IntoDiagnostic, Result};
use panorama_core::AppState;
use tokio::net::TcpListener;
use tower::ServiceBuilder;
use tower_http::cors::{self, CorsLayer};
use tower_http::{
cors::{self, CorsLayer},
trace::TraceLayer,
};
use utoipa::OpenApi;
use utoipa_scalar::{Scalar, Servable};
@ -47,11 +50,13 @@ pub async fn run() -> Result<()> {
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_headers(cors::Any)
.allow_origin(cors::Any);
let trace_layer = TraceLayer::new_for_http();
// build our application with a single route
let app = Router::new()
.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()))
.route("/mail/config", get(get_mail_config))
.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());
let listener = TcpListener::bind("0.0.0.0:5195").await.into_diagnostic()?;

View file

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

View file

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