diff --git a/blog-server-api/src/endpoints/create_post/handler.rs b/blog-server-api/src/endpoints/create_post/handler.rs new file mode 100644 index 0000000..fd29182 --- /dev/null +++ b/blog-server-api/src/endpoints/create_post/handler.rs @@ -0,0 +1,39 @@ +use super::request_content::CreatePostRequestContent; +use super::response_content_failure::CreatePostContentFailure; +use super::response_content_failure::CreatePostContentFailure::*; +use super::response_content_success::CreatePostContentSuccess; + +pub async fn http_handler( + (CreatePostRequestContent { + new_post_data, + post_service, + auth_author_future, + },): (CreatePostRequestContent,), +) -> Result { + let author = auth_author_future.await.map_err(|e| Unauthorized { + reason: e.to_string(), + })?; + + let base_post = new_post_data + .map_err(|e| ValidationError { + reason: e.to_string(), + })? + .into(author.id); + + let inserted_id = post_service + .create_post(&base_post) + .await + .map_err(|e| DatabaseError { + reason: e.to_string(), + })?; + + let created_post = post_service + .post_by_id(&inserted_id) + .await + .map_err(|e| DatabaseError { + reason: e.to_string(), + })? + .ok_or(InsertFailed)?; + + Ok(created_post.into()) +} diff --git a/blog-server-api/src/endpoints/create_post/mod.rs b/blog-server-api/src/endpoints/create_post/mod.rs new file mode 100644 index 0000000..3426965 --- /dev/null +++ b/blog-server-api/src/endpoints/create_post/mod.rs @@ -0,0 +1,6 @@ +mod handler; +mod request_content; +mod response_content_failure; +mod response_content_success; + +pub use handler::http_handler; diff --git a/blog-server-api/src/endpoints/create_post/request_content.rs b/blog-server-api/src/endpoints/create_post/request_content.rs new file mode 100644 index 0000000..1f5e5f8 --- /dev/null +++ b/blog-server-api/src/endpoints/create_post/request_content.rs @@ -0,0 +1,32 @@ +use crate::{entities::CreatePost, extensions::Resolve, utils::auth}; +use blog_server_services::traits::{ + author_service::{Author, AuthorService}, + post_service::PostService, +}; +use screw_api::request::{ApiRequestContent, ApiRequestOriginContent}; +use screw_components::{dyn_fn::DFuture, dyn_result::DResult}; +use std::sync::Arc; + +pub struct CreatePostRequestContent { + pub(super) new_post_data: DResult, + pub(super) post_service: Arc>, + pub(super) auth_author_future: DFuture>, +} + +impl ApiRequestContent for CreatePostRequestContent +where + Extensions: Resolve>> + Resolve>>, +{ + type Data = CreatePost; + + fn create(origin_content: ApiRequestOriginContent) -> Self { + Self { + new_post_data: origin_content.data_result, + post_service: origin_content.extensions.resolve(), + auth_author_future: Box::pin(auth::author( + origin_content.http_parts, + origin_content.extensions.resolve(), + )), + } + } +} diff --git a/blog-server-api/src/endpoints/create_post/response_content_failure.rs b/blog-server-api/src/endpoints/create_post/response_content_failure.rs new file mode 100644 index 0000000..284ca3b --- /dev/null +++ b/blog-server-api/src/endpoints/create_post/response_content_failure.rs @@ -0,0 +1,64 @@ +use hyper::StatusCode; +use screw_api::response::{ApiResponseContentBase, ApiResponseContentFailure}; + +pub enum CreatePostContentFailure { + DatabaseError { reason: String }, + ValidationError { reason: String }, + AlreadyExists, + InsertFailed, + Unauthorized { reason: String }, +} + +impl ApiResponseContentBase for CreatePostContentFailure { + fn status_code(&self) -> &'static StatusCode { + match self { + CreatePostContentFailure::DatabaseError { reason: _ } => { + &StatusCode::INTERNAL_SERVER_ERROR + } + CreatePostContentFailure::AlreadyExists => &StatusCode::BAD_REQUEST, + CreatePostContentFailure::ValidationError { reason: _ } => &StatusCode::BAD_REQUEST, + CreatePostContentFailure::Unauthorized { reason: _ } => &StatusCode::UNAUTHORIZED, + CreatePostContentFailure::InsertFailed => &StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl ApiResponseContentFailure for CreatePostContentFailure { + fn identifier(&self) -> &'static str { + match self { + CreatePostContentFailure::DatabaseError { reason: _ } => "CREATE_POST_DATABASE_ERROR", + CreatePostContentFailure::ValidationError { reason: _ } => { + "CREATE_POST_VALIDATION_ERROR" + } + CreatePostContentFailure::AlreadyExists => "CREATE_POST_ALREASY_EXISTS", + CreatePostContentFailure::InsertFailed => "CREATE_POST_COULD_NOT_FIND_CREATED_POST", + CreatePostContentFailure::Unauthorized { reason: _ } => "CREATE_POST_UNAUTHORIZED", + } + } + + fn reason(&self) -> Option { + Some(match self { + CreatePostContentFailure::DatabaseError { reason } => { + if cfg!(debug_assertions) { + format!("database error: {}", reason) + } else { + "internal database error".to_string() + } + } + CreatePostContentFailure::Unauthorized { reason } => { + if cfg!(debug_assertions) { + format!("unauthorized error: {}", reason) + } else { + "unauthorized error".to_string() + } + } + CreatePostContentFailure::ValidationError { reason } => { + format!("validation error: {}", reason) + } + CreatePostContentFailure::AlreadyExists => { + String::from("post with specified ID already exists") + } + CreatePostContentFailure::InsertFailed => String::from("error while creating new post"), + }) + } +} diff --git a/blog-server-api/src/endpoints/create_post/response_content_success.rs b/blog-server-api/src/endpoints/create_post/response_content_success.rs new file mode 100644 index 0000000..b9098f1 --- /dev/null +++ b/blog-server-api/src/endpoints/create_post/response_content_success.rs @@ -0,0 +1,41 @@ +use crate::entities::Post; +use blog_server_services::traits::post_service::Post as ServicePost; +use hyper::StatusCode; +use screw_api::response::{ApiResponseContentBase, ApiResponseContentSuccess}; +use serde::Serialize; + +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CreatePostContentSuccess { + created_post: Post, +} + +impl Into for ServicePost { + fn into(self) -> CreatePostContentSuccess { + CreatePostContentSuccess { + created_post: (self.into()), + } + } +} + +impl ApiResponseContentBase for CreatePostContentSuccess { + fn status_code(&self) -> &'static StatusCode { + &StatusCode::OK + } +} + +impl ApiResponseContentSuccess for CreatePostContentSuccess { + type Data = Self; + + fn identifier(&self) -> &'static str { + "POST_CREATED" + } + + fn description(&self) -> Option { + Some(String::from("post record created")) + } + + fn data(&self) -> &Self::Data { + self + } +} diff --git a/blog-server-api/src/endpoints/mod.rs b/blog-server-api/src/endpoints/mod.rs index cc98090..b9131a4 100644 --- a/blog-server-api/src/endpoints/mod.rs +++ b/blog-server-api/src/endpoints/mod.rs @@ -2,6 +2,7 @@ pub mod author; pub mod author_me; pub mod authors; pub mod comments; +pub mod create_post; pub mod login; pub mod post; pub mod posts; diff --git a/blog-server-api/src/entities/author.rs b/blog-server-api/src/entities/author.rs index ae35ab4..6db996c 100644 --- a/blog-server-api/src/entities/author.rs +++ b/blog-server-api/src/entities/author.rs @@ -1,7 +1,7 @@ use blog_server_services::traits::author_service::Author as ServiceAuthor; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ShortAuthor { pub slug: String, diff --git a/blog-server-api/src/entities/create_post.rs b/blog-server-api/src/entities/create_post.rs new file mode 100644 index 0000000..1f9bb2e --- /dev/null +++ b/blog-server-api/src/entities/create_post.rs @@ -0,0 +1,35 @@ +use blog_server_services::traits::post_service::BasePost; +use serde::{Deserialize, Serialize}; + +use crate::utils::time_utils; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreatePost { + pub title: String, + pub slug: String, + pub summary: String, + pub published: u8, + pub content: Option, + pub tags: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ShortTag { + pub title: String, +} + +impl CreatePost { + pub fn into(self, author_id: u64) -> BasePost { + BasePost { + author_id, + created_at: time_utils::now_as_secs(), + title: self.title, + slug: self.slug, + summary: self.summary, + published: self.published, + content: self.content, + } + } +} diff --git a/blog-server-api/src/entities/mod.rs b/blog-server-api/src/entities/mod.rs index 43ca4f9..0a91547 100644 --- a/blog-server-api/src/entities/mod.rs +++ b/blog-server-api/src/entities/mod.rs @@ -1,9 +1,11 @@ mod author; mod comment; +mod create_post; mod post; mod tag; pub use author::*; pub use comment::*; +pub use create_post::*; pub use post::*; pub use tag::*; diff --git a/blog-server-api/src/entities/post.rs b/blog-server-api/src/entities/post.rs index 2a3ab2d..9746fc3 100644 --- a/blog-server-api/src/entities/post.rs +++ b/blog-server-api/src/entities/post.rs @@ -1,14 +1,15 @@ use super::*; use blog_server_services::traits::post_service::Post as ServicePost; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Post { + pub id: u64, pub title: String, pub slug: String, pub summary: String, - pub created_at: i64, + pub created_at: u64, pub content: Option, pub short_author: ShortAuthor, pub tags: Vec, @@ -17,6 +18,7 @@ pub struct Post { impl Into for ServicePost { fn into(self) -> Post { Post { + id: self.id, title: self.base.title, slug: self.base.slug, summary: self.base.summary, @@ -31,9 +33,9 @@ impl Into for ServicePost { .tags .into_iter() .map(|v| Tag { + id: v.id, title: v.title, - //TODO: Change to ID - slug: v.id.to_string(), + slug: v.slug, }) .collect(), } diff --git a/blog-server-api/src/entities/tag.rs b/blog-server-api/src/entities/tag.rs index 196876f..8e36d65 100644 --- a/blog-server-api/src/entities/tag.rs +++ b/blog-server-api/src/entities/tag.rs @@ -1,8 +1,9 @@ -use serde::Serialize; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Tag { + pub id: u64, pub title: String, pub slug: String, } diff --git a/blog-server-api/src/router.rs b/blog-server-api/src/router.rs index 5a13b77..ff192ce 100644 --- a/blog-server-api/src/router.rs +++ b/blog-server-api/src/router.rs @@ -77,6 +77,11 @@ pub fn make_router( .and_path("/posts") .and_handler(posts::http_handler), ) + .route( + route::first::Route::with_method(&hyper::Method::POST) + .and_path("/post") + .and_handler(create_post::http_handler), + ) .scoped("/search", |r| { r.route( route::first::Route::with_method(&hyper::Method::GET) diff --git a/blog-server-api/src/utils/auth.rs b/blog-server-api/src/utils/auth.rs index 018bfc0..3e381a2 100644 --- a/blog-server-api/src/utils/auth.rs +++ b/blog-server-api/src/utils/auth.rs @@ -10,7 +10,7 @@ use std::sync::Arc; #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct Data { - author_id: i64, + author_id: u64, exp: u64, } diff --git a/blog-server-api/src/utils/mod.rs b/blog-server-api/src/utils/mod.rs index 6ec6460..d9584bc 100644 --- a/blog-server-api/src/utils/mod.rs +++ b/blog-server-api/src/utils/mod.rs @@ -1,3 +1,4 @@ pub mod auth; pub mod jwt; pub mod password; +pub mod time_utils; diff --git a/blog-server-api/src/utils/time_utils.rs b/blog-server-api/src/utils/time_utils.rs new file mode 100644 index 0000000..9bc33b6 --- /dev/null +++ b/blog-server-api/src/utils/time_utils.rs @@ -0,0 +1,8 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +pub fn now_as_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} diff --git a/blog-server-services/src/impls/rbatis_author_service.rs b/blog-server-services/src/impls/rbatis_author_service.rs index ac537c1..31a5b80 100644 --- a/blog-server-services/src/impls/rbatis_author_service.rs +++ b/blog-server-services/src/impls/rbatis_author_service.rs @@ -7,7 +7,7 @@ pub fn create_rbatis_author_service(rb: RBatis) -> Box { } impl_insert!(BaseAuthor {}, "author"); -impl_select!(Author {select_by_id(id: &i64) -> Option => +impl_select!(Author {select_by_id(id: &u64) -> Option => "`WHERE id = #{id} LIMIT 1`"}); impl_select!(Author {select_by_slug(slug: &String) -> Option => "`WHERE slug = #{slug} LIMIT 1`"}); @@ -67,7 +67,7 @@ impl AuthorService for RbatisAuthorService { async fn authors(&self, offset: &i64, limit: &i64) -> DResult> { Ok(Author::select_all_with_offset_and_limit(&mut self.rb.clone(), offset, limit).await?) } - async fn author_by_id(&self, id: &i64) -> DResult> { + async fn author_by_id(&self, id: &u64) -> DResult> { Ok(Author::select_by_id(&mut self.rb.clone(), id).await?) } async fn author_by_slug(&self, slug: &String) -> DResult> { diff --git a/blog-server-services/src/impls/rbatis_comment_service.rs b/blog-server-services/src/impls/rbatis_comment_service.rs index 762ac02..674b77d 100644 --- a/blog-server-services/src/impls/rbatis_comment_service.rs +++ b/blog-server-services/src/impls/rbatis_comment_service.rs @@ -16,7 +16,7 @@ impl Comment { WHERE post_comment.post_id = #{post_id} " )] - async fn count_by_post_id(rb: &RBatis, post_id: &i64) -> rbatis::Result { + async fn count_by_post_id(rb: &RBatis, post_id: &u64) -> rbatis::Result { impled!() } #[py_sql( @@ -35,7 +35,7 @@ impl Comment { )] async fn select_all_by_post_id_with_limit_and_offset( rb: &RBatis, - post_id: &i64, + post_id: &u64, limit: &i64, offset: &i64, ) -> rbatis::Result> { @@ -49,12 +49,12 @@ struct RbatisCommentService { #[async_trait] impl CommentService for RbatisCommentService { - async fn comments_count_by_post_id(&self, post_id: &i64) -> DResult { + async fn comments_count_by_post_id(&self, post_id: &u64) -> DResult { Ok(Comment::count_by_post_id(&self.rb, post_id).await?) } async fn comments_by_post_id( &self, - post_id: &i64, + post_id: &u64, offset: &i64, limit: &i64, ) -> DResult> { diff --git a/blog-server-services/src/impls/rbatis_post_service.rs b/blog-server-services/src/impls/rbatis_post_service.rs index 94d4401..e927af1 100644 --- a/blog-server-services/src/impls/rbatis_post_service.rs +++ b/blog-server-services/src/impls/rbatis_post_service.rs @@ -1,6 +1,6 @@ use crate::traits::post_service::{BasePost, Post, PostService, Tag}; use rbatis::rbatis::RBatis; -use screw_components::dyn_result::{DError, DResult}; +use screw_components::dyn_result::DResult; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -13,8 +13,9 @@ impl_insert!(BasePost {}, "post"); #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct TagDto { - post_id: i64, - id: i64, + post_id: u64, + id: u64, + slug: String, title: String, } @@ -23,6 +24,7 @@ impl Into for TagDto { Tag { id: self.id, title: self.title, + slug: self.slug, } } } @@ -60,7 +62,7 @@ impl Post { LIMIT 1 \ " )] - async fn select_by_id(rb: &RBatis, id: &i64) -> rbatis::Result> { + async fn select_by_id(rb: &RBatis, id: &u64) -> rbatis::Result> { impled!() } #[py_sql( @@ -102,6 +104,7 @@ impl Post { SELECT \ tag.id, \ tag.title, \ + tag.slug, \ post_tag.post_id \ FROM post_tag \ JOIN tag ON tag.id = post_tag.tag_id \ @@ -112,7 +115,7 @@ impl Post { ) \ " )] - async fn select_tags_by_posts(rb: &RBatis, post_ids: Vec) -> rbatis::Result> { + async fn select_tags_by_posts(rb: &RBatis, post_ids: Vec) -> rbatis::Result> { impled!() } #[py_sql( @@ -148,6 +151,19 @@ struct RbatisPostService { } impl RbatisPostService { + #[py_sql( + " + INSERT INTO post + (author_id,title,slug,summary,published,created_at,content) + VALUES + (#{post.author_id},#{post.title},#{post.slug},#{post.summary},#{post.published},to_timestamp(#{post.created_at}),#{post.content}) + RETURNING id + " + )] + async fn insert_new_post(rb: &RBatis, post: &BasePost) -> rbatis::Result { + impled!() + } + async fn saturate_with_tags(&self, post_option: Option) -> DResult> { match post_option { None => Ok(None), @@ -170,7 +186,7 @@ impl RbatisPostService { let post_ids = posts.iter().map(|post| post.id).collect(); - let mut grouped_tags: HashMap> = + let mut grouped_tags: HashMap> = Post::select_tags_by_posts(&self.rb, post_ids) .await? .into_iter() @@ -216,7 +232,7 @@ impl PostService for RbatisPostService { RbatisPostService::saturate_posts_with_tags(&self, posts).await } - async fn post_by_id(&self, id: &i64) -> DResult> { + async fn post_by_id(&self, id: &u64) -> DResult> { let post_option = Post::select_by_id(&self.rb, id).await?; RbatisPostService::saturate_with_tags(&self, post_option).await } @@ -226,12 +242,8 @@ impl PostService for RbatisPostService { RbatisPostService::saturate_with_tags(&self, post_option).await } - async fn create_post(&self, post: &BasePost) -> DResult { - let insert_result = BasePost::insert(&mut self.rb.clone(), post).await?; - let last_insert_id = insert_result - .last_insert_id - .as_i64() - .ok_or::("wrond last_insert_id".into())?; - Ok(last_insert_id) + async fn create_post(&self, post: &BasePost) -> DResult { + let inserted_id = RbatisPostService::insert_new_post(&self.rb, post).await?; + Ok(inserted_id) } } diff --git a/blog-server-services/src/traits/author_service.rs b/blog-server-services/src/traits/author_service.rs index 2acf0f4..9bdda41 100644 --- a/blog-server-services/src/traits/author_service.rs +++ b/blog-server-services/src/traits/author_service.rs @@ -18,7 +18,7 @@ pub struct BaseAuthor { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct Author { - pub id: i64, + pub id: u64, #[serde(flatten)] pub base: BaseAuthor, } @@ -34,7 +34,7 @@ pub trait AuthorService: Send + Sync { ) -> DResult>; async fn authors_count(&self) -> DResult; async fn authors(&self, offset: &i64, limit: &i64) -> DResult>; - async fn author_by_id(&self, id: &i64) -> DResult>; + async fn author_by_id(&self, id: &u64) -> DResult>; async fn author_by_slug(&self, slug: &String) -> DResult>; async fn create_author(&self, author: &BaseAuthor) -> DResult; } diff --git a/blog-server-services/src/traits/comment_service.rs b/blog-server-services/src/traits/comment_service.rs index 7912411..9d7d024 100644 --- a/blog-server-services/src/traits/comment_service.rs +++ b/blog-server-services/src/traits/comment_service.rs @@ -24,10 +24,10 @@ pub struct Comment { #[async_trait] pub trait CommentService: Send + Sync { - async fn comments_count_by_post_id(&self, post_id: &i64) -> DResult; + async fn comments_count_by_post_id(&self, post_id: &u64) -> DResult; async fn comments_by_post_id( &self, - post_id: &i64, + post_id: &u64, offset: &i64, limit: &i64, ) -> DResult>; diff --git a/blog-server-services/src/traits/post_service.rs b/blog-server-services/src/traits/post_service.rs index eef337f..889c535 100644 --- a/blog-server-services/src/traits/post_service.rs +++ b/blog-server-services/src/traits/post_service.rs @@ -4,26 +4,27 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct Tag { - pub id: i64, + pub id: u64, + pub slug: String, pub title: String, } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct BasePost { - pub author_id: i64, + pub author_id: u64, pub title: String, pub slug: String, pub summary: String, pub published: u8, - pub created_at: i64, + pub created_at: u64, pub content: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct Post { - pub id: i64, + pub id: u64, pub author_slug: String, pub author_first_name: Option, pub author_last_name: Option, @@ -40,7 +41,7 @@ pub trait PostService: Send + Sync { -> DResult>; async fn posts_count(&self) -> DResult; async fn posts(&self, offset: &i64, limit: &i64) -> DResult>; - async fn post_by_id(&self, id: &i64) -> DResult>; + async fn post_by_id(&self, id: &u64) -> DResult>; async fn post_by_slug(&self, slug: &String) -> DResult>; - async fn create_post(&self, post: &BasePost) -> DResult; + async fn create_post(&self, post: &BasePost) -> DResult; }