Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
@@ -5,6 +5,7 @@ use crate::{
|
||||
create_item::{self, CreateItem, CreateItemExt},
|
||||
create_root::{self, CreateRoot, CreateRootExt},
|
||||
create_section::{self, CreateSection, CreateSectionExt},
|
||||
update_item::{UpdateItem, UpdateItemExt},
|
||||
},
|
||||
state::SharedState,
|
||||
};
|
||||
@@ -48,6 +49,7 @@ pub struct Commander {
|
||||
create_root: CreateRoot,
|
||||
create_section: CreateSection,
|
||||
create_item: CreateItem,
|
||||
update_item: UpdateItem,
|
||||
}
|
||||
|
||||
impl Commander {
|
||||
@@ -55,11 +57,13 @@ impl Commander {
|
||||
create_root: CreateRoot,
|
||||
create_section: CreateSection,
|
||||
create_item: CreateItem,
|
||||
update_item: UpdateItem,
|
||||
) -> Self {
|
||||
Self {
|
||||
create_root,
|
||||
create_section,
|
||||
create_item,
|
||||
update_item,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +108,19 @@ impl Commander {
|
||||
title,
|
||||
description,
|
||||
state,
|
||||
} => todo!(),
|
||||
} => {
|
||||
self.update_item
|
||||
.execute(crate::services::update_item::Request {
|
||||
root,
|
||||
path,
|
||||
title,
|
||||
description,
|
||||
state,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Command::ToggleItem { root, path } => todo!(),
|
||||
Command::Move { root, src, dest } => todo!(),
|
||||
}
|
||||
@@ -121,6 +137,7 @@ impl CommanderExt for SharedState {
|
||||
self.create_root_service(),
|
||||
self.create_section_service(),
|
||||
self.create_item_service(),
|
||||
self.update_item_service(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -196,26 +196,23 @@ impl Graph for Server {
|
||||
|
||||
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 {},
|
||||
)),
|
||||
})),
|
||||
},
|
||||
)]),
|
||||
})),
|
||||
}),
|
||||
}))
|
||||
let res = self
|
||||
.querier
|
||||
.get(&msg.root, msg.paths.clone())
|
||||
.await
|
||||
.map_err(to_tonic_err)?;
|
||||
|
||||
match res {
|
||||
Some(item) => Ok(Response::new(GetReply {
|
||||
item: Some(to_native(&item).map_err(to_tonic_err)?),
|
||||
})),
|
||||
None => {
|
||||
return Err(tonic::Status::new(
|
||||
tonic::Code::NotFound,
|
||||
"failed to find any valid roots",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_available_roots(
|
||||
@@ -242,6 +239,130 @@ impl Graph for Server {
|
||||
|
||||
Ok(Response::new(GetAvailableRootsResponse { roots }))
|
||||
}
|
||||
|
||||
async fn update_item(
|
||||
&self,
|
||||
request: tonic::Request<UpdateItemRequest>,
|
||||
) -> std::result::Result<tonic::Response<UpdateItemResponse>, tonic::Status> {
|
||||
let req = request.into_inner();
|
||||
tracing::trace!("update item: 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::<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::UpdateItem {
|
||||
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(UpdateItemResponse {}))
|
||||
}
|
||||
}
|
||||
|
||||
fn to_native(from: &hyperlog_core::log::GraphItem) -> anyhow::Result<GraphItem> {
|
||||
match from {
|
||||
hyperlog_core::log::GraphItem::User(section)
|
||||
| hyperlog_core::log::GraphItem::Section(section) => {
|
||||
let mut root = HashMap::new();
|
||||
for (key, value) in section.iter() {
|
||||
root.insert(key.to_string(), to_native(value)?);
|
||||
}
|
||||
match from {
|
||||
hyperlog_core::log::GraphItem::User(_) => Ok(GraphItem {
|
||||
contents: Some(graph_item::Contents::User(UserGraphItem { items: root })),
|
||||
}),
|
||||
hyperlog_core::log::GraphItem::Section(_) => Ok(GraphItem {
|
||||
contents: Some(graph_item::Contents::Section(SectionGraphItem {
|
||||
items: root,
|
||||
})),
|
||||
}),
|
||||
_ => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
hyperlog_core::log::GraphItem::Item {
|
||||
title,
|
||||
description,
|
||||
state,
|
||||
} => Ok(GraphItem {
|
||||
contents: Some(graph_item::Contents::Item(ItemGraphItem {
|
||||
title: title.to_owned(),
|
||||
description: description.to_owned(),
|
||||
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 {})
|
||||
}
|
||||
}),
|
||||
})),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: create more defined protobuf categories for errors
|
||||
|
@@ -1,3 +1,5 @@
|
||||
#![feature(map_try_insert)]
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use crate::state::{SharedState, State};
|
||||
|
@@ -1,18 +1,23 @@
|
||||
use hyperlog_core::log::GraphItem;
|
||||
|
||||
use crate::{
|
||||
services::get_available_roots::{self, GetAvailableRoots, GetAvailableRootsExt},
|
||||
services::{
|
||||
get_available_roots::{self, GetAvailableRoots, GetAvailableRootsExt},
|
||||
get_graph::{GetGraph, GetGraphExt},
|
||||
},
|
||||
state::SharedState,
|
||||
};
|
||||
|
||||
pub struct Querier {
|
||||
get_available_roots: GetAvailableRoots,
|
||||
get_graph: GetGraph,
|
||||
}
|
||||
|
||||
impl Querier {
|
||||
pub fn new(get_available_roots: GetAvailableRoots) -> Self {
|
||||
pub fn new(get_available_roots: GetAvailableRoots, get_graph: GetGraph) -> Self {
|
||||
Self {
|
||||
get_available_roots,
|
||||
get_graph,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +34,20 @@ impl Querier {
|
||||
Ok(Some(res.roots))
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
pub async fn get(
|
||||
&self,
|
||||
root: &str,
|
||||
path: impl IntoIterator<Item = impl Into<String>>,
|
||||
) -> Option<GraphItem> {
|
||||
todo!()
|
||||
) -> anyhow::Result<Option<GraphItem>> {
|
||||
let graph = self
|
||||
.get_graph
|
||||
.execute(crate::services::get_graph::Request {
|
||||
root: root.into(),
|
||||
path: path.into_iter().map(|s| s.into()).collect(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(Some(graph.item))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +57,6 @@ pub trait QuerierExt {
|
||||
|
||||
impl QuerierExt for SharedState {
|
||||
fn querier(&self) -> Querier {
|
||||
Querier::new(self.get_available_roots_service())
|
||||
Querier::new(self.get_available_roots_service(), self.get_graph_service())
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
pub mod create_item;
|
||||
pub mod create_root;
|
||||
pub mod create_section;
|
||||
pub mod update_item;
|
||||
|
||||
pub mod get_available_roots;
|
||||
pub mod get_graph;
|
||||
|
360
crates/hyperlog-server/src/services/get_graph.rs
Normal file
360
crates/hyperlog-server/src/services/get_graph.rs
Normal file
@@ -0,0 +1,360 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use hyperlog_core::log::{GraphItem, ItemState};
|
||||
use serde::Deserialize;
|
||||
use sqlx::types::Json;
|
||||
|
||||
use crate::state::SharedState;
|
||||
|
||||
use self::engine::Engine;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GetGraph {
|
||||
db: sqlx::PgPool,
|
||||
}
|
||||
|
||||
pub struct Request {
|
||||
pub root: String,
|
||||
pub path: Vec<String>,
|
||||
}
|
||||
pub struct Response {
|
||||
pub item: GraphItem,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct Root {
|
||||
id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Item {
|
||||
title: String,
|
||||
description: String,
|
||||
state: ItemState,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug)]
|
||||
struct Node {
|
||||
id: uuid::Uuid,
|
||||
path: String,
|
||||
item_type: String,
|
||||
item_content: Option<Json<serde_json::Value>>,
|
||||
}
|
||||
|
||||
impl GetGraph {
|
||||
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?;
|
||||
|
||||
let nodes: Vec<Node> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
nodes
|
||||
WHERE
|
||||
root_id = $1
|
||||
LIMIT
|
||||
1000
|
||||
"#,
|
||||
)
|
||||
.bind(root_id)
|
||||
.fetch_all(&self.db)
|
||||
.await?;
|
||||
|
||||
let item = self.build_graph(req.root, req.path, nodes)?;
|
||||
|
||||
Ok(Response { item })
|
||||
}
|
||||
|
||||
fn build_graph(
|
||||
&self,
|
||||
root: String,
|
||||
path: Vec<String>,
|
||||
mut nodes: Vec<Node>,
|
||||
) -> anyhow::Result<GraphItem> {
|
||||
nodes.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
let mut engine = Engine::default();
|
||||
engine.create_root(&root)?;
|
||||
|
||||
self.get_graph_items(&root, &mut engine, &nodes)?;
|
||||
|
||||
engine
|
||||
.get(&root, &path.iter().map(|s| s.as_str()).collect::<Vec<_>>())
|
||||
.ok_or(anyhow::anyhow!("failed to find a valid graph"))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn get_graph_items(
|
||||
&self,
|
||||
root: &str,
|
||||
engine: &mut Engine,
|
||||
nodes: &Vec<Node>,
|
||||
) -> anyhow::Result<()> {
|
||||
for node in nodes {
|
||||
if let Some(item) = self.get_graph_item(node) {
|
||||
let path = node.path.split('.').collect::<Vec<_>>();
|
||||
engine.create(root, &path, item)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_graph_item(&self, node: &Node) -> Option<GraphItem> {
|
||||
match node.item_type.as_str() {
|
||||
"SECTION" => Some(GraphItem::Section(BTreeMap::default())),
|
||||
"ITEM" => {
|
||||
if let Some(content) = &node.item_content {
|
||||
let item: Item = serde_json::from_value(content.0.clone()).ok()?;
|
||||
|
||||
Some(GraphItem::Item {
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
state: item.state,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait GetGraphExt {
|
||||
fn get_graph_service(&self) -> GetGraph;
|
||||
}
|
||||
|
||||
impl GetGraphExt for SharedState {
|
||||
fn get_graph_service(&self) -> GetGraph {
|
||||
GetGraph::new(self.db.clone())
|
||||
}
|
||||
}
|
||||
|
||||
mod engine {
|
||||
|
||||
use std::{collections::BTreeMap, fmt::Display};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use hyperlog_core::log::{Graph, GraphItem, ItemState};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Engine {
|
||||
graph: Graph,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
pub fn engine_from_str(input: &str) -> anyhow::Result<Self> {
|
||||
let graph: Graph = serde_json::from_str(input)?;
|
||||
|
||||
Ok(Self { graph })
|
||||
}
|
||||
|
||||
pub fn to_str(&self) -> anyhow::Result<String> {
|
||||
serde_json::to_string_pretty(&self.graph).context("failed to serialize graph")
|
||||
}
|
||||
|
||||
pub fn create_root(&mut self, root: &str) -> anyhow::Result<()> {
|
||||
self.graph
|
||||
.try_insert(root.to_string(), GraphItem::User(BTreeMap::default()))
|
||||
.map_err(|_| anyhow!("entry was already found, aborting"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create(&mut self, root: &str, path: &[&str], item: GraphItem) -> anyhow::Result<()> {
|
||||
let graph = &mut self.graph;
|
||||
|
||||
let (last, items) = path.split_last().ok_or(anyhow!(
|
||||
"path cannot be empty, must contain at least one item"
|
||||
))?;
|
||||
|
||||
let root = graph
|
||||
.get_mut(root)
|
||||
.ok_or(anyhow!("root was missing a user, aborting"))?;
|
||||
|
||||
let mut current_item = root;
|
||||
for section in items {
|
||||
match current_item {
|
||||
GraphItem::User(u) => match u.get_mut(section.to_owned()) {
|
||||
Some(graph_item) => {
|
||||
current_item = graph_item;
|
||||
}
|
||||
None => anyhow::bail!("path: {} section was not found", section),
|
||||
},
|
||||
GraphItem::Item { .. } => anyhow::bail!("path: {} was already found", section),
|
||||
GraphItem::Section(s) => match s.get_mut(section.to_owned()) {
|
||||
Some(graph_item) => {
|
||||
current_item = graph_item;
|
||||
}
|
||||
None => anyhow::bail!("path: {} section was not found", section),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
match current_item {
|
||||
GraphItem::User(u) => {
|
||||
u.insert(last.to_string(), item);
|
||||
}
|
||||
GraphItem::Section(s) => {
|
||||
s.insert(last.to_string(), item);
|
||||
}
|
||||
GraphItem::Item { .. } => anyhow::bail!("cannot insert an item into an item"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(&self, root: &str, path: &[&str]) -> Option<&GraphItem> {
|
||||
let root = self.graph.get(root)?;
|
||||
|
||||
root.get(path)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, root: &str, path: &[&str]) -> Option<&mut GraphItem> {
|
||||
let root = self.graph.get_mut(root)?;
|
||||
|
||||
root.get_mut(path)
|
||||
}
|
||||
|
||||
pub fn take(&mut self, root: &str, path: &[&str]) -> Option<GraphItem> {
|
||||
let root = self.graph.get_mut(root)?;
|
||||
|
||||
root.take(path)
|
||||
}
|
||||
|
||||
pub fn section_move(
|
||||
&mut self,
|
||||
root: &str,
|
||||
src_path: &[&str],
|
||||
dest_path: &[&str],
|
||||
) -> anyhow::Result<()> {
|
||||
let src = self
|
||||
.take(root, src_path)
|
||||
.ok_or(anyhow!("failed to find source path"))?;
|
||||
|
||||
let dest = self
|
||||
.get_mut(root, dest_path)
|
||||
.ok_or(anyhow!("failed to find destination"))?;
|
||||
|
||||
let src_item = src_path
|
||||
.last()
|
||||
.ok_or(anyhow!("src path must have at least one item"))?;
|
||||
|
||||
match dest {
|
||||
GraphItem::User(u) => {
|
||||
u.try_insert(src_item.to_string(), src)
|
||||
.map_err(|_e| anyhow!("key was already found, aborting: {}", src_item))?;
|
||||
}
|
||||
GraphItem::Section(s) => {
|
||||
s.try_insert(src_item.to_string(), src)
|
||||
.map_err(|_e| anyhow!("key was already found, aborting: {}", src_item))?;
|
||||
}
|
||||
GraphItem::Item { .. } => {
|
||||
anyhow::bail!(
|
||||
"failed to insert src at item, item doesn't support arbitrary items"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, root: &str, path: &[&str]) -> anyhow::Result<()> {
|
||||
self.take(root, path)
|
||||
.map(|_| ())
|
||||
.ok_or(anyhow!("item was not found"))
|
||||
}
|
||||
|
||||
pub fn toggle_item(&mut self, root: &str, path: &[&str]) -> anyhow::Result<()> {
|
||||
if let Some(item) = self.get_mut(root, path) {
|
||||
match item {
|
||||
GraphItem::Item { state, .. } => match state {
|
||||
ItemState::NotDone => *state = ItemState::Done,
|
||||
ItemState::Done => *state = ItemState::NotDone,
|
||||
},
|
||||
_ => {
|
||||
anyhow::bail!("{}.{:?} is not an item", root, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_item(
|
||||
&mut self,
|
||||
root: &str,
|
||||
path: &[&str],
|
||||
item: &GraphItem,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some((name, dest_last)) = path.split_last() {
|
||||
if let Some(parent) = self.get_mut(root, dest_last) {
|
||||
match parent {
|
||||
GraphItem::User(s) | GraphItem::Section(s) => {
|
||||
if let Some(mut existing) = s.remove(*name) {
|
||||
match (&mut existing, item) {
|
||||
(
|
||||
GraphItem::Item {
|
||||
title: ex_title,
|
||||
description: ex_desc,
|
||||
state: ex_state,
|
||||
},
|
||||
GraphItem::Item {
|
||||
title,
|
||||
description,
|
||||
state,
|
||||
},
|
||||
) => {
|
||||
ex_title.clone_from(title);
|
||||
ex_desc.clone_from(description);
|
||||
ex_state.clone_from(state);
|
||||
|
||||
let title = title.replace(".", "-");
|
||||
s.insert(title, existing.clone());
|
||||
}
|
||||
_ => {
|
||||
anyhow::bail!(
|
||||
"path: {}.{} found is not an item",
|
||||
root,
|
||||
path.join(".")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GraphItem::Item { .. } => {
|
||||
anyhow::bail!("cannot rename when item is placed in an item")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_roots(&self) -> Option<Vec<String>> {
|
||||
let items = self.graph.keys().cloned().collect::<Vec<_>>();
|
||||
if items.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Engine {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let output = serde_json::to_string_pretty(&self.graph).unwrap();
|
||||
f.write_str(&output)
|
||||
}
|
||||
}
|
||||
}
|
107
crates/hyperlog-server/src/services/update_item.rs
Normal file
107
crates/hyperlog-server/src/services/update_item.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use hyperlog_core::log::ItemState;
|
||||
use sqlx::types::Json;
|
||||
|
||||
use crate::state::SharedState;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UpdateItem {
|
||||
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,
|
||||
}
|
||||
|
||||
impl UpdateItem {
|
||||
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?;
|
||||
let Root { id: node_id } = sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
nodes
|
||||
WHERE
|
||||
root_id = $1
|
||||
AND path = $2
|
||||
AND item_type = $3
|
||||
"#,
|
||||
)
|
||||
.bind(root_id)
|
||||
.bind(req.path.join("."))
|
||||
.bind("ITEM")
|
||||
.fetch_one(&self.db)
|
||||
.await?;
|
||||
|
||||
let (_, rest) = req
|
||||
.path
|
||||
.split_last()
|
||||
.ok_or(anyhow::anyhow!("expected path to have at least one item"))?;
|
||||
|
||||
let mut rest = rest.to_vec();
|
||||
rest.push(req.title.replace(".", "-"));
|
||||
|
||||
let res = sqlx::query(
|
||||
r#"
|
||||
UPDATE
|
||||
nodes
|
||||
SET
|
||||
item_content = $1,
|
||||
path = $2
|
||||
WHERE
|
||||
id = $3
|
||||
"#,
|
||||
)
|
||||
.bind(Json(ItemContent {
|
||||
title: req.title,
|
||||
description: req.description,
|
||||
state: req.state,
|
||||
}))
|
||||
.bind(rest.join("."))
|
||||
.bind(node_id)
|
||||
.execute(&self.db)
|
||||
.await?;
|
||||
|
||||
if res.rows_affected() != 1 {
|
||||
anyhow::bail!("failed to update item");
|
||||
}
|
||||
|
||||
Ok(Response {})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UpdateItemExt {
|
||||
fn update_item_service(&self) -> UpdateItem;
|
||||
}
|
||||
|
||||
impl UpdateItemExt for SharedState {
|
||||
fn update_item_service(&self) -> UpdateItem {
|
||||
UpdateItem::new(self.db.clone())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user