diff --git a/crates/hyperlog-protos/proto/hyperlog.proto b/crates/hyperlog-protos/proto/hyperlog.proto index ffcd013..f620411 100644 --- a/crates/hyperlog-protos/proto/hyperlog.proto +++ b/crates/hyperlog-protos/proto/hyperlog.proto @@ -32,11 +32,29 @@ message GraphItem { } service Graph { + // Commands + rpc CreateSection(CreateSectionRequest) returns (CreateSectionResponse); + rpc CreateRoot(CreateRootRequest) returns (CreateRootResponse); + + // Queriers rpc GetAvailableRoots(GetAvailableRootsRequest) returns (GetAvailableRootsResponse); rpc Get(GetRequest) returns (GetReply); - rpc CreateSection(CreateSectionRequest) returns (CreateSectionResponse); + } +// Commands +message CreateSectionRequest { + string root = 1; + repeated string path = 2; +} +message CreateSectionResponse {} + +message CreateRootRequest { + string root = 1; +} +message CreateRootResponse {} + +// Queries message GetAvailableRootsRequest {} message GetAvailableRootsResponse { repeated string roots = 1; @@ -50,8 +68,3 @@ message GetRequest { message GetReply { GraphItem item = 1; } - - -message CreateSectionRequest {} -message CreateSectionResponse {} - diff --git a/crates/hyperlog-server/migrations/crdb/20240201211013_initial.sql b/crates/hyperlog-server/migrations/crdb/20240201211013_initial.sql index 8ddc1d3..a973354 100644 --- a/crates/hyperlog-server/migrations/crdb/20240201211013_initial.sql +++ b/crates/hyperlog-server/migrations/crdb/20240201211013_initial.sql @@ -1 +1,14 @@ -- Add migration script here + +CREATE TABLE roots ( + id UUID NOT NULL PRIMARY KEY, + root_name VARCHAR(255) UNIQUE NOT NULL +); + +CREATE TABLE nodes ( + id UUID NOT NULL PRIMARY KEY, + root_id UUID NOT NULL, + path VARCHAR NOT NULL, + item_type VARCHAR NOT NULL, + item_content JSONB +); diff --git a/crates/hyperlog-server/src/commands.rs b/crates/hyperlog-server/src/commands.rs index 24a552d..e29bfae 100644 --- a/crates/hyperlog-server/src/commands.rs +++ b/crates/hyperlog-server/src/commands.rs @@ -1,5 +1,13 @@ use hyperlog_core::log::{GraphItem, ItemState}; +use crate::{ + services::{ + create_root::{self, CreateRoot, CreateRootExt}, + create_section::{self, CreateSection, CreateSectionExt}, + }, + state::SharedState, +}; + #[allow(dead_code)] pub enum Command { CreateRoot { @@ -35,14 +43,35 @@ pub enum Command { } #[allow(dead_code)] -pub struct Commander {} +pub struct Commander { + create_root: CreateRoot, + create_section: CreateSection, +} -#[allow(dead_code, unused_variables)] impl Commander { - pub fn execute(&self, cmd: Command) -> anyhow::Result<()> { + pub fn new(create_root: CreateRoot, create_section: CreateSection) -> Self { + Self { + create_root, + create_section, + } + } + + pub async fn execute(&self, cmd: Command) -> anyhow::Result<()> { match cmd { - Command::CreateRoot { root } => todo!(), - Command::CreateSection { root, path } => todo!(), + Command::CreateRoot { root } => { + self.create_root + .execute(create_root::Request { root }) + .await?; + + Ok(()) + } + Command::CreateSection { root, path } => { + self.create_section + .execute(create_section::Request { root, path }) + .await?; + + Ok(()) + } Command::CreateItem { root, path, @@ -61,38 +90,14 @@ impl Commander { Command::Move { root, src, dest } => todo!(), } } +} - pub async fn create_root(&self, root: &str) -> anyhow::Result<()> { - todo!() - } +pub trait CommanderExt { + fn commander(&self) -> Commander; +} - pub async fn create(&self, root: &str, path: &[&str], item: GraphItem) -> anyhow::Result<()> { - todo!() - } - - pub async fn get(&self, root: &str, path: &[&str]) -> Option { - todo!() - } - - pub async fn section_move( - &self, - root: &str, - src_path: &[&str], - dest_path: &[&str], - ) -> anyhow::Result<()> { - todo!() - } - - pub async fn delete(&self, root: &str, path: &[&str]) -> anyhow::Result<()> { - todo!() - } - - pub async fn update_item( - &self, - root: &str, - path: &[&str], - item: &GraphItem, - ) -> anyhow::Result<()> { - todo!() +impl CommanderExt for SharedState { + fn commander(&self) -> Commander { + Commander::new(self.create_root_service(), self.create_section_service()) } } diff --git a/crates/hyperlog-server/src/external_grpc.rs b/crates/hyperlog-server/src/external_grpc.rs index 3c68f6c..40deecd 100644 --- a/crates/hyperlog-server/src/external_grpc.rs +++ b/crates/hyperlog-server/src/external_grpc.rs @@ -6,6 +6,7 @@ use std::{collections::HashMap, net::SocketAddr}; use tonic::{transport, Response}; use crate::{ + commands::{Command, Commander, CommanderExt}, querier::{Querier, QuerierExt}, state::SharedState, }; @@ -13,11 +14,12 @@ use crate::{ #[allow(dead_code)] pub struct Server { querier: Querier, + commander: Commander, } impl Server { - pub fn new(querier: Querier) -> Self { - Self { querier } + pub fn new(querier: Querier, commander: Commander) -> Self { + Self { querier, commander } } } @@ -72,8 +74,85 @@ impl Graph for Server { let req = request.into_inner(); tracing::trace!("create section: req({:?})", req); + if req.root.is_empty() { + return Err(tonic::Status::new( + tonic::Code::InvalidArgument, + "root cannot be empty".to_string(), + )); + } + + 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(), + )); + } + + self.commander + .execute(Command::CreateSection { + root: req.root, + path: req.path, + }) + .await + .map_err(to_tonic_err)?; + Ok(Response::new(CreateSectionResponse {})) } + + async fn create_root( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + let req = request.into_inner(); + tracing::trace!("create root: 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 {})) + } +} + +// TODO: create more defined protobuf categories for errors +fn to_tonic_err(err: anyhow::Error) -> tonic::Status { + tonic::Status::new(tonic::Code::Unknown, err.to_string()) } pub trait ServerExt { @@ -82,7 +161,7 @@ pub trait ServerExt { impl ServerExt for SharedState { fn grpc_server(&self) -> Server { - Server::new(self.querier()) + Server::new(self.querier(), self.commander()) } } diff --git a/crates/hyperlog-server/src/lib.rs b/crates/hyperlog-server/src/lib.rs index 00564a8..459113a 100644 --- a/crates/hyperlog-server/src/lib.rs +++ b/crates/hyperlog-server/src/lib.rs @@ -11,6 +11,8 @@ mod querier; mod state; +mod services; + #[derive(Clone)] pub struct ServeOptions { pub external_http: SocketAddr, diff --git a/crates/hyperlog-server/src/services.rs b/crates/hyperlog-server/src/services.rs new file mode 100644 index 0000000..727192f --- /dev/null +++ b/crates/hyperlog-server/src/services.rs @@ -0,0 +1,2 @@ +pub mod create_root; +pub mod create_section; diff --git a/crates/hyperlog-server/src/services/create_root.rs b/crates/hyperlog-server/src/services/create_root.rs new file mode 100644 index 0000000..4c5f74b --- /dev/null +++ b/crates/hyperlog-server/src/services/create_root.rs @@ -0,0 +1,38 @@ +use crate::state::SharedState; + +#[derive(Clone)] +pub struct CreateRoot { + db: sqlx::PgPool, +} + +pub struct Request { + pub root: String, +} +pub struct Response {} + +impl CreateRoot { + pub fn new(db: sqlx::PgPool) -> Self { + Self { db } + } + + pub async fn execute(&self, req: Request) -> anyhow::Result { + let root_id = uuid::Uuid::new_v4(); + sqlx::query(r#"INSERT INTO roots (id, root_name) VALUES ($1, $2)"#) + .bind(root_id) + .bind(req.root) + .execute(&self.db) + .await?; + + Ok(Response {}) + } +} + +pub trait CreateRootExt { + fn create_root_service(&self) -> CreateRoot; +} + +impl CreateRootExt for SharedState { + fn create_root_service(&self) -> CreateRoot { + CreateRoot::new(self.db.clone()) + } +} diff --git a/crates/hyperlog-server/src/services/create_section.rs b/crates/hyperlog-server/src/services/create_section.rs new file mode 100644 index 0000000..1657fe2 --- /dev/null +++ b/crates/hyperlog-server/src/services/create_section.rs @@ -0,0 +1,62 @@ +use hyperlog_core::log::GraphItem; + +use crate::state::SharedState; + +#[derive(Clone)] +pub struct CreateSection { + db: sqlx::PgPool, +} + +pub struct Request { + pub root: String, + pub path: Vec, +} +pub struct Response {} + +#[derive(sqlx::FromRow)] +struct Root { + id: uuid::Uuid, + root_name: String, +} + +impl CreateSection { + 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?; + + 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("SECTION".to_string()) + .bind(None::) + .execute(&self.db) + .await?; + + Ok(Response {}) + } +} + +pub trait CreateSectionExt { + fn create_section_service(&self) -> CreateSection; +} + +impl CreateSectionExt for SharedState { + fn create_section_service(&self) -> CreateSection { + CreateSection::new(self.db.clone()) + } +} diff --git a/crates/hyperlog-server/src/state.rs b/crates/hyperlog-server/src/state.rs index 68a7309..f008f4a 100644 --- a/crates/hyperlog-server/src/state.rs +++ b/crates/hyperlog-server/src/state.rs @@ -15,7 +15,7 @@ impl Deref for SharedState { } pub struct State { - pub _db: Pool, + pub db: Pool, } impl State { @@ -32,6 +32,6 @@ impl State { let _ = sqlx::query("SELECT 1;").fetch_one(&db).await?; - Ok(Self { _db: db }) + Ok(Self { db }) } } diff --git a/crates/hyperlog-tui/src/commander/remote.rs b/crates/hyperlog-tui/src/commander/remote.rs index c098ec7..76d25e6 100644 --- a/crates/hyperlog-tui/src/commander/remote.rs +++ b/crates/hyperlog-tui/src/commander/remote.rs @@ -20,7 +20,13 @@ impl Commander { match cmd.clone() { Command::CreateRoot { root } => { - todo!() + let channel = self.channel.clone(); + + let mut client = GraphClient::new(channel); + + let request = tonic::Request::new(CreateRootRequest { root }); + let response = client.create_root(request).await?; + let res = response.into_inner(); //self.engine.create_root(&root)?; } Command::CreateSection { root, path } => { @@ -28,7 +34,7 @@ impl Commander { let mut client = GraphClient::new(channel); - let request = tonic::Request::new(CreateSectionRequest {}); + let request = tonic::Request::new(CreateSectionRequest { root, path }); let response = client.create_section(request).await?; let res = response.into_inner(); diff --git a/cuddle.yaml b/cuddle.yaml index 2f57133..365632f 100644 --- a/cuddle.yaml +++ b/cuddle.yaml @@ -19,3 +19,5 @@ please: scripts: dev: type: shell + install: + type: shell diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..5e6b07d --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env zsh + +set -eo pipefail + +cargo install --path crates/hyperlog --force