feat: can add items
All checks were successful
continuous-integration/drone/push Build is passing

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
Kasper Juul Hermansen 2024-05-13 23:33:37 +02:00
parent 699bac7159
commit 7bdf8393b1
Signed by: kjuulh
GPG Key ID: 57B6E1465221F912
8 changed files with 300 additions and 54 deletions

View File

@ -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 {

View File

@ -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);

View File

@ -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(),
)
}
}

View File

@ -25,46 +25,107 @@ impl Server {
#[tonic::async_trait]
impl Graph for Server {
async fn get(
async fn create_item(
&self,
request: tonic::Request<GetRequest>,
) -> std::result::Result<tonic::Response<GetReply>, tonic::Status> {
let msg = request.get_ref();
request: tonic::Request<CreateItemRequest>,
) -> std::result::Result<tonic::Response<CreateItemResponse>, tonic::Status> {
let req = request.into_inner();
tracing::trace!("create item: req({:?})", req);
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 {},
)),
})),
},
)]),
})),
}),
}))
if req.root.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"root cannot be empty".to_string(),
));
}
async fn get_available_roots(
&self,
request: tonic::Request<GetAvailableRootsRequest>,
) -> std::result::Result<tonic::Response<GetAvailableRootsResponse>, tonic::Status> {
let req = request.into_inner();
tracing::trace!("get available roots: req({:?})", req);
if req.path.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"path cannot be empty".to_string(),
));
}
Ok(Response::new(GetAvailableRootsResponse {
roots: vec!["kjuulh".into()],
}))
if req
.path
.iter()
.filter(|item| item.is_empty())
.collect::<Vec<_>>()
.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::<Vec<_>>()
.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 create_root(
&self,
request: tonic::Request<CreateRootRequest>,
) -> std::result::Result<tonic::Response<CreateRootResponse>, 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 {}))
}
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<CreateRootRequest>,
) -> std::result::Result<tonic::Response<CreateRootResponse>, tonic::Status> {
let req = request.into_inner();
tracing::trace!("create root: req({:?})", req);
request: tonic::Request<GetRequest>,
) -> std::result::Result<tonic::Response<GetReply>, tonic::Status> {
let msg = request.get_ref();
if req.root.is_empty() {
return Err(tonic::Status::new(
tonic::Code::InvalidArgument,
"root cannot be empty".to_string(),
));
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 {},
)),
})),
},
)]),
})),
}),
}))
}
self.commander
.execute(Command::CreateRoot { root: req.root })
.await
.map_err(to_tonic_err)?;
async fn get_available_roots(
&self,
request: tonic::Request<GetAvailableRootsRequest>,
) -> std::result::Result<tonic::Response<GetAvailableRootsResponse>, tonic::Status> {
let req = request.into_inner();
tracing::trace!("get available roots: req({:?})", req);
Ok(Response::new(CreateRootResponse {}))
Ok(Response::new(GetAvailableRootsResponse {
roots: vec!["kjuulh".into()],
}))
}
}

View File

@ -1,2 +1,3 @@
pub mod create_item;
pub mod create_root;
pub mod create_section;

View File

@ -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<String>,
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<Response> {
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())
}
}

View File

@ -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#"

View File

@ -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::<Vec<_>>(),