diff --git a/crates/hyperlog-protos/proto/hyperlog.proto b/crates/hyperlog-protos/proto/hyperlog.proto index f620411..2ff9711 100644 --- a/crates/hyperlog-protos/proto/hyperlog.proto +++ b/crates/hyperlog-protos/proto/hyperlog.proto @@ -35,6 +35,7 @@ service Graph { // Commands rpc CreateSection(CreateSectionRequest) returns (CreateSectionResponse); rpc CreateRoot(CreateRootRequest) returns (CreateRootResponse); + rpc CreateItem(CreateItemRequest) returns (CreateItemResponse); // Queriers rpc GetAvailableRoots(GetAvailableRootsRequest) returns (GetAvailableRootsResponse); @@ -54,6 +55,13 @@ message CreateRootRequest { } message CreateRootResponse {} +message CreateItemRequest { + string root = 1; + repeated string path = 2; + ItemGraphItem item = 3; +} +message CreateItemResponse {} + // Queries message GetAvailableRootsRequest {} message GetAvailableRootsResponse { diff --git a/crates/hyperlog-server/migrations/crdb/20240201211013_initial.sql b/crates/hyperlog-server/migrations/crdb/20240201211013_initial.sql index a973354..166cdaa 100644 --- a/crates/hyperlog-server/migrations/crdb/20240201211013_initial.sql +++ b/crates/hyperlog-server/migrations/crdb/20240201211013_initial.sql @@ -12,3 +12,6 @@ CREATE TABLE nodes ( item_type VARCHAR NOT NULL, item_content JSONB ); + +CREATE UNIQUE INDEX idx_unique_root_path ON nodes(root_id, path); + diff --git a/crates/hyperlog-server/src/commands.rs b/crates/hyperlog-server/src/commands.rs index e29bfae..a7ae2c0 100644 --- a/crates/hyperlog-server/src/commands.rs +++ b/crates/hyperlog-server/src/commands.rs @@ -1,7 +1,8 @@ -use hyperlog_core::log::{GraphItem, ItemState}; +use hyperlog_core::log::ItemState; use crate::{ services::{ + create_item::{self, CreateItem, CreateItemExt}, create_root::{self, CreateRoot, CreateRootExt}, create_section::{self, CreateSection, CreateSectionExt}, }, @@ -46,13 +47,19 @@ pub enum Command { pub struct Commander { create_root: CreateRoot, create_section: CreateSection, + create_item: CreateItem, } impl Commander { - pub fn new(create_root: CreateRoot, create_section: CreateSection) -> Self { + pub fn new( + create_root: CreateRoot, + create_section: CreateSection, + create_item: CreateItem, + ) -> Self { Self { create_root, create_section, + create_item, } } @@ -78,7 +85,19 @@ impl Commander { title, description, state, - } => todo!(), + } => { + self.create_item + .execute(create_item::Request { + root, + path, + title, + description, + state, + }) + .await?; + + Ok(()) + } Command::UpdateItem { root, path, @@ -98,6 +117,10 @@ pub trait CommanderExt { impl CommanderExt for SharedState { fn commander(&self) -> Commander { - Commander::new(self.create_root_service(), self.create_section_service()) + Commander::new( + self.create_root_service(), + self.create_section_service(), + self.create_item_service(), + ) } } diff --git a/crates/hyperlog-server/src/external_grpc.rs b/crates/hyperlog-server/src/external_grpc.rs index 40deecd..d04c595 100644 --- a/crates/hyperlog-server/src/external_grpc.rs +++ b/crates/hyperlog-server/src/external_grpc.rs @@ -25,46 +25,107 @@ impl Server { #[tonic::async_trait] impl Graph for Server { - async fn get( + async fn create_item( &self, - request: tonic::Request, - ) -> std::result::Result, tonic::Status> { - let msg = request.get_ref(); + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + let req = request.into_inner(); + tracing::trace!("create item: req({:?})", req); - tracing::trace!("get: req({:?})", msg); + if req.root.is_empty() { + return Err(tonic::Status::new( + tonic::Code::InvalidArgument, + "root cannot be empty".to_string(), + )); + } - Ok(Response::new(GetReply { - item: Some(GraphItem { - path: "kjuulh".into(), - contents: Some(graph_item::Contents::User(UserGraphItem { - items: HashMap::from([( - "some".to_string(), - GraphItem { - path: "some".into(), - contents: Some(graph_item::Contents::Item(ItemGraphItem { - title: "some-title".into(), - description: "some-description".into(), - item_state: Some(item_graph_item::ItemState::NotDone( - ItemStateNotDone {}, - )), - })), - }, - )]), - })), - }), - })) + if req.path.is_empty() { + return Err(tonic::Status::new( + tonic::Code::InvalidArgument, + "path cannot be empty".to_string(), + )); + } + + if req + .path + .iter() + .filter(|item| item.is_empty()) + .collect::>() + .first() + .is_some() + { + return Err(tonic::Status::new( + tonic::Code::InvalidArgument, + "path cannot contain empty paths".to_string(), + )); + } + + if req + .path + .iter() + .filter(|item| item.contains(".")) + .collect::>() + .first() + .is_some() + { + return Err(tonic::Status::new( + tonic::Code::InvalidArgument, + "path cannot contain `.`".to_string(), + )); + } + + let item = match req.item { + Some(i) => i, + None => { + return Err(tonic::Status::new( + tonic::Code::InvalidArgument, + "item cannot contain empty or null".to_string(), + )); + } + }; + + self.commander + .execute(Command::CreateItem { + root: req.root, + path: req.path, + title: item.title, + description: item.description, + state: match item.item_state { + Some(item_graph_item::ItemState::Done(_)) => { + hyperlog_core::log::ItemState::Done + } + Some(item_graph_item::ItemState::NotDone(_)) => { + hyperlog_core::log::ItemState::NotDone + } + None => hyperlog_core::log::ItemState::default(), + }, + }) + .await + .map_err(to_tonic_err)?; + + Ok(Response::new(CreateItemResponse {})) } - async fn get_available_roots( + async fn create_root( &self, - request: tonic::Request, - ) -> std::result::Result, tonic::Status> { + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { let req = request.into_inner(); - tracing::trace!("get available roots: req({:?})", req); + tracing::trace!("create root: req({:?})", req); - Ok(Response::new(GetAvailableRootsResponse { - roots: vec!["kjuulh".into()], - })) + if req.root.is_empty() { + return Err(tonic::Status::new( + tonic::Code::InvalidArgument, + "root cannot be empty".to_string(), + )); + } + + self.commander + .execute(Command::CreateRoot { root: req.root }) + .await + .map_err(to_tonic_err)?; + + Ok(Response::new(CreateRootResponse {})) } async fn create_section( @@ -127,26 +188,46 @@ impl Graph for Server { Ok(Response::new(CreateSectionResponse {})) } - async fn create_root( + async fn get( &self, - request: tonic::Request, - ) -> std::result::Result, tonic::Status> { + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + let msg = request.get_ref(); + + tracing::trace!("get: req({:?})", msg); + + Ok(Response::new(GetReply { + item: Some(GraphItem { + path: "kjuulh".into(), + contents: Some(graph_item::Contents::User(UserGraphItem { + items: HashMap::from([( + "some".to_string(), + GraphItem { + path: "some".into(), + contents: Some(graph_item::Contents::Item(ItemGraphItem { + title: "some-title".into(), + description: "some-description".into(), + item_state: Some(item_graph_item::ItemState::NotDone( + ItemStateNotDone {}, + )), + })), + }, + )]), + })), + }), + })) + } + + async fn get_available_roots( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { let req = request.into_inner(); - tracing::trace!("create root: req({:?})", req); + tracing::trace!("get available roots: req({:?})", req); - if req.root.is_empty() { - return Err(tonic::Status::new( - tonic::Code::InvalidArgument, - "root cannot be empty".to_string(), - )); - } - - self.commander - .execute(Command::CreateRoot { root: req.root }) - .await - .map_err(to_tonic_err)?; - - Ok(Response::new(CreateRootResponse {})) + Ok(Response::new(GetAvailableRootsResponse { + roots: vec!["kjuulh".into()], + })) } } diff --git a/crates/hyperlog-server/src/services.rs b/crates/hyperlog-server/src/services.rs index 727192f..2e24ae2 100644 --- a/crates/hyperlog-server/src/services.rs +++ b/crates/hyperlog-server/src/services.rs @@ -1,2 +1,3 @@ +pub mod create_item; pub mod create_root; pub mod create_section; diff --git a/crates/hyperlog-server/src/services/create_item.rs b/crates/hyperlog-server/src/services/create_item.rs new file mode 100644 index 0000000..9ce7dfc --- /dev/null +++ b/crates/hyperlog-server/src/services/create_item.rs @@ -0,0 +1,107 @@ +use hyperlog_core::log::{GraphItem, ItemState}; +use sqlx::types::Json; + +use crate::state::SharedState; + +#[derive(Clone)] +pub struct CreateItem { + db: sqlx::PgPool, +} + +pub struct Request { + pub root: String, + pub path: Vec, + + pub title: String, + pub description: String, + pub state: ItemState, +} +pub struct Response {} + +#[derive(serde::Serialize)] +struct ItemContent { + pub title: String, + pub description: String, + pub state: ItemState, +} + +#[derive(sqlx::FromRow)] +struct Root { + id: uuid::Uuid, + root_name: String, +} + +#[derive(sqlx::FromRow)] +struct Section { + id: uuid::Uuid, +} + +impl CreateItem { + pub fn new(db: sqlx::PgPool) -> Self { + Self { db } + } + + pub async fn execute(&self, req: Request) -> anyhow::Result { + let Root { id: root_id, .. } = + sqlx::query_as(r#"SELECT * FROM roots WHERE root_name = $1"#) + .bind(req.root) + .fetch_one(&self.db) + .await?; + + match req.path.split_last() { + Some((_, section_path)) => { + if !section_path.is_empty() { + let Section { .. } = sqlx::query_as( + r#" + SELECT + * + FROM + nodes + WHERE + root_id = $1 AND + path = $2 AND + item_type = 'SECTION' + "#, + ) + .bind(root_id) + .bind(section_path.join(".")) + .fetch_one(&self.db) + .await?; + } + + let node_id = uuid::Uuid::new_v4(); + sqlx::query( + r#" + INSERT INTO nodes + (id, root_id, path, item_type, item_content) + VALUES + ($1, $2, $3, $4, $5)"#, + ) + .bind(node_id) + .bind(root_id) + .bind(req.path.join(".")) + .bind("ITEM".to_string()) + .bind(Json(ItemContent { + title: req.title, + description: req.description, + state: req.state, + })) + .execute(&self.db) + .await?; + } + None => anyhow::bail!("path most contain at least one item"), + } + + Ok(Response {}) + } +} + +pub trait CreateItemExt { + fn create_item_service(&self) -> CreateItem; +} + +impl CreateItemExt for SharedState { + fn create_item_service(&self) -> CreateItem { + CreateItem::new(self.db.clone()) + } +} diff --git a/crates/hyperlog-server/src/services/create_section.rs b/crates/hyperlog-server/src/services/create_section.rs index 1657fe2..b2ed365 100644 --- a/crates/hyperlog-server/src/services/create_section.rs +++ b/crates/hyperlog-server/src/services/create_section.rs @@ -31,6 +31,8 @@ impl CreateSection { .fetch_one(&self.db) .await?; + // FIXME: implement consistency check on path + let node_id = uuid::Uuid::new_v4(); sqlx::query( r#" diff --git a/crates/hyperlog-tui/src/commander/remote.rs b/crates/hyperlog-tui/src/commander/remote.rs index 76d25e6..820cf13 100644 --- a/crates/hyperlog-tui/src/commander/remote.rs +++ b/crates/hyperlog-tui/src/commander/remote.rs @@ -51,7 +51,28 @@ impl Commander { description, state, } => { - todo!() + let channel = self.channel.clone(); + + let mut client = GraphClient::new(channel); + + let request = tonic::Request::new(CreateItemRequest { + root, + path, + item: Some(ItemGraphItem { + title, + description, + item_state: Some(match state { + hyperlog_core::log::ItemState::NotDone => { + item_graph_item::ItemState::NotDone(ItemStateNotDone {}) + } + hyperlog_core::log::ItemState::Done => { + item_graph_item::ItemState::Done(ItemStateDone {}) + } + }), + }), + }); + let response = client.create_item(request).await?; + let res = response.into_inner(); // self.engine.create( // &root, // &path.iter().map(|p| p.as_str()).collect::>(),