From 163990880aca102c3e926db9d34e187cf1c445e4 Mon Sep 17 00:00:00 2001 From: ras0q Date: Fri, 24 Jan 2025 00:37:47 +0900 Subject: [PATCH 01/10] impl ReactionService close #47 --- server/migrations/2_init_reaction.sql | 11 +++ server/src/reaction.rs | 23 +++++- server/src/reaction/error.rs | 18 ++++ server/src/reaction/grpc.rs | 110 ++++++++++++++++++++++++ server/src/reaction/impl.rs | 115 ++++++++++++++++++++++++++ 5 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 server/migrations/2_init_reaction.sql create mode 100644 server/src/reaction/error.rs create mode 100644 server/src/reaction/grpc.rs create mode 100644 server/src/reaction/impl.rs diff --git a/server/migrations/2_init_reaction.sql b/server/migrations/2_init_reaction.sql new file mode 100644 index 00000000..db31c1b1 --- /dev/null +++ b/server/migrations/2_init_reaction.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS `reactions` ( + `id` BINARY(16) NOT NULL, + `user_id` BINARY(16) NOT NULL, + `position_x` INT UNSIGNED NOT NULL, + `position_y` INT UNSIGNED NOT NULL, + `kind` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +); diff --git a/server/src/reaction.rs b/server/src/reaction.rs index 8b82a210..eb38807a 100644 --- a/server/src/reaction.rs +++ b/server/src/reaction.rs @@ -1,10 +1,18 @@ //! `reaction.proto` +pub mod error; +pub mod grpc; +mod r#impl; + +use std::sync::Arc; + use futures::future::BoxFuture; use serde::{Deserialize, Serialize}; use crate::prelude::{IntoStatus, Timestamp}; +pub use error::Error; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(transparent)] pub struct ReactionId(pub uuid::Uuid); @@ -26,6 +34,7 @@ pub struct GetReactionParams { #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] pub struct CreateReactionParams { + pub user_id: crate::user::UserId, pub position: crate::world::Coordinate, pub kind: String, } @@ -74,5 +83,17 @@ pub trait ProvideReactionService: Send + Sync + 'static { self.reaction_service().create_reaction(ctx, params) } - // TODO: build_server(this: Arc) -> ReactionServiceServer<...> + fn build_server(this: Arc) -> ReactionServiceServer + where + Self: Sized, + { + let service = grpc::ServiceImpl::new(this); + ReactionServiceServer::new(service) + } } + +#[derive(Debug, Clone, Copy)] +pub struct ReactionServiceImpl; + +pub type ReactionServiceServer = + schema::reaction::reaction_service_server::ReactionServiceServer>; diff --git a/server/src/reaction/error.rs b/server/src/reaction/error.rs new file mode 100644 index 00000000..2b1e5529 --- /dev/null +++ b/server/src/reaction/error.rs @@ -0,0 +1,18 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Not found")] + NotFound, + #[error("Database error")] + Sqlx(#[from] sqlx::Error), +} + +impl From for tonic::Status { + fn from(value: Error) -> Self { + match value { + Error::NotFound => tonic::Status::not_found("Not found"), + Error::Sqlx(_) => tonic::Status::internal("Database error"), + } + } +} + +pub type Result = std::result::Result; diff --git a/server/src/reaction/grpc.rs b/server/src/reaction/grpc.rs new file mode 100644 index 00000000..e5407e8a --- /dev/null +++ b/server/src/reaction/grpc.rs @@ -0,0 +1,110 @@ +use std::sync::Arc; + +use schema::reaction as schema; + +use crate::prelude::IntoStatus; + +// MARK: type conversions + +impl From for schema::Reaction { + fn from(value: super::Reaction) -> Self { + let super::Reaction { + id, + user_id, + position, + kind, + created_at, + updated_at: _, + } = value; + Self { + id: id.0.to_string(), + user_id: user_id.0.to_string(), + position: Some(position.into()), + kind, + created_at: Some(created_at.into()), + expires_at: Some(super::Timestamp(created_at.0 + chrono::Duration::seconds(10)).into()), + } + } +} + +// MARK: ServiceImpl + +pub struct ServiceImpl { + state: Arc, +} + +impl Clone for ServiceImpl +where + State: super::ProvideReactionService, +{ + fn clone(&self) -> Self { + Self { + state: Arc::clone(&self.state), + } + } +} + +impl ServiceImpl +where + State: super::ProvideReactionService, +{ + pub(super) fn new(state: Arc) -> Self { + Self { state } + } +} + +#[async_trait::async_trait] +impl schema::reaction_service_server::ReactionService for ServiceImpl +where + State: super::ProvideReactionService, +{ + async fn get_reaction( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let (_, _, schema::GetReactionRequest { id }) = request.into_parts(); + let params = super::GetReactionParams { + id: super::ReactionId( + uuid::Uuid::parse_str(&id) + .map_err(|_| tonic::Status::invalid_argument("Invalid UUID"))?, + ), + }; + let reaction = self + .state + .get_reaction(params) + .await + .map_err(IntoStatus::into_status)? + .into(); + let res = schema::GetReactionResponse { + reaction: Some(reaction), + }; + Ok(tonic::Response::new(res)) + } + + async fn create_reaction( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let (_, _, schema::CreateReactionRequest { kind, position }) = request.into_parts(); + let Some(position) = position else { + return Err(tonic::Status::invalid_argument("Position is required")); + }; + // TODO: Get user_id from context (cookie?) + let user_id = uuid::Uuid::parse_str("123e4567-e89b-12d3-a456-426614174000").unwrap(); + let params = super::CreateReactionParams { + user_id: crate::user::UserId(user_id), + kind, + position: position.into(), + }; + let reaction = self + .state + .create_reaction(params) + .await + .map_err(IntoStatus::into_status)? + .into(); + let res = schema::CreateReactionResponse { + reaction: Some(reaction), + }; + Ok(tonic::Response::new(res)) + } +} diff --git a/server/src/reaction/impl.rs b/server/src/reaction/impl.rs new file mode 100644 index 00000000..65ee8e25 --- /dev/null +++ b/server/src/reaction/impl.rs @@ -0,0 +1,115 @@ +use futures::{future, FutureExt}; +use serde::{Deserialize, Serialize}; +use sqlx::MySqlPool; + +impl super::ReactionService for super::ReactionServiceImpl +where + Context: AsRef, +{ + type Error = super::Error; + + fn get_reaction<'a>( + &'a self, + ctx: &'a Context, + params: super::GetReactionParams, + ) -> future::BoxFuture<'a, Result> { + get_reaction(ctx.as_ref(), params).boxed() + } + + fn create_reaction<'a>( + &'a self, + ctx: &'a Context, + params: super::CreateReactionParams, + ) -> future::BoxFuture<'a, Result> { + create_reaction(ctx.as_ref(), params).boxed() + } +} + +// MARK: DB operations + +#[derive(Debug, Clone, Hash, Deserialize, Serialize, sqlx::FromRow)] +struct ReactionRow { + pub id: uuid::Uuid, + pub user_id: uuid::Uuid, + pub position_x: u32, + pub position_y: u32, + pub kind: String, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, +} + +impl From for super::Reaction { + fn from(value: ReactionRow) -> Self { + Self { + id: super::ReactionId(value.id), + user_id: crate::user::UserId(value.user_id), + position: crate::world::Coordinate { + x: value.position_x, + y: value.position_y, + }, + kind: value.kind, + created_at: super::Timestamp(value.created_at), + updated_at: super::Timestamp(value.updated_at), + } + } +} + +async fn get_reaction( + pool: &MySqlPool, + params: super::GetReactionParams, +) -> Result { + let super::GetReactionParams { + id: super::ReactionId(id), + } = params; + let reaction: Option = + sqlx::query_as(r#"SELECT * FROM `reactions` WHERE `id` = ?"#) + .bind(id) + .fetch_optional(pool) + .await?; + reaction.map(Into::into).ok_or(super::Error::NotFound) +} + +async fn create_reaction( + pool: &MySqlPool, + params: super::CreateReactionParams, +) -> Result { + let super::CreateReactionParams { + user_id, + position, + kind, + } = params; + let reaction = ReactionRow { + id: uuid::Uuid::now_v7(), + user_id: user_id.0, + position_x: position.x, + position_y: position.y, + kind, + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + sqlx::query( + r#" + INSERT INTO `reactions` + (`id`, `user_id`, `position_x`, `position_y`, `kind`, `created_at`, `updated_at`) + VALUES (?, ?, ?, ?, ?, ?, ?) + "#, + ) + .bind(reaction.id) + .bind(reaction.user_id) + .bind(reaction.position_x) + .bind(reaction.position_y) + .bind(reaction.kind) + .bind(reaction.created_at) + .bind(reaction.updated_at) + .execute(pool) + .await?; + tracing::info!(id = %reaction.id, "Created a reaction"); + let reaction = get_reaction( + pool, + super::GetReactionParams { + id: super::ReactionId(reaction.id), + }, + ) + .await?; + Ok(reaction) +} From bfc7f47b20537159ab6ae7b0ae111b6366f381c5 Mon Sep 17 00:00:00 2001 From: ras0q Date: Fri, 24 Jan 2025 09:23:40 +0900 Subject: [PATCH 02/10] merge migration scripts --- server/migrations/1_init.sql | 12 ++++++++++++ server/migrations/2_init_reaction.sql | 11 ----------- 2 files changed, 12 insertions(+), 11 deletions(-) delete mode 100644 server/migrations/2_init_reaction.sql diff --git a/server/migrations/1_init.sql b/server/migrations/1_init.sql index 739420b0..3fed0478 100644 --- a/server/migrations/1_init.sql +++ b/server/migrations/1_init.sql @@ -18,3 +18,15 @@ CREATE TABLE IF NOT EXISTS `messages` ( `expires_at` TIMESTAMP NOT NULL, PRIMARY KEY (`id`) ); + +CREATE TABLE IF NOT EXISTS `reactions` ( + `id` BINARY(16) NOT NULL, + `user_id` BINARY(16) NOT NULL, + `position_x` INT UNSIGNED NOT NULL, + `position_y` INT UNSIGNED NOT NULL, + `kind` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +); diff --git a/server/migrations/2_init_reaction.sql b/server/migrations/2_init_reaction.sql deleted file mode 100644 index db31c1b1..00000000 --- a/server/migrations/2_init_reaction.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE IF NOT EXISTS `reactions` ( - `id` BINARY(16) NOT NULL, - `user_id` BINARY(16) NOT NULL, - `position_x` INT UNSIGNED NOT NULL, - `position_y` INT UNSIGNED NOT NULL, - `kind` VARCHAR(255) NOT NULL, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) -); From 1e560c693e206ff84bfc8f562942ed1435db1fc5 Mon Sep 17 00:00:00 2001 From: ras0q Date: Fri, 24 Jan 2025 09:24:32 +0900 Subject: [PATCH 03/10] derive Default --- server/src/reaction.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/reaction.rs b/server/src/reaction.rs index eb38807a..4325f14d 100644 --- a/server/src/reaction.rs +++ b/server/src/reaction.rs @@ -92,7 +92,7 @@ pub trait ProvideReactionService: Send + Sync + 'static { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub struct ReactionServiceImpl; pub type ReactionServiceServer = From 9f7b24ee9a82af0394b3025b84745282cd4edd65 Mon Sep 17 00:00:00 2001 From: ras0q Date: Fri, 24 Jan 2025 09:26:00 +0900 Subject: [PATCH 04/10] add comment --- server/src/reaction/grpc.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/reaction/grpc.rs b/server/src/reaction/grpc.rs index e5407e8a..edd3d497 100644 --- a/server/src/reaction/grpc.rs +++ b/server/src/reaction/grpc.rs @@ -22,6 +22,7 @@ impl From for schema::Reaction { position: Some(position.into()), kind, created_at: Some(created_at.into()), + // TODO: duration設定 expires_at: Some(super::Timestamp(created_at.0 + chrono::Duration::seconds(10)).into()), } } From 403898b88b8ad091c836cddeb2d0bbd0425cce27 Mon Sep 17 00:00:00 2001 From: ras0q Date: Fri, 24 Jan 2025 09:27:20 +0900 Subject: [PATCH 05/10] trace database errors --- server/src/reaction/error.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/reaction/error.rs b/server/src/reaction/error.rs index 2b1e5529..7901df09 100644 --- a/server/src/reaction/error.rs +++ b/server/src/reaction/error.rs @@ -10,7 +10,10 @@ impl From for tonic::Status { fn from(value: Error) -> Self { match value { Error::NotFound => tonic::Status::not_found("Not found"), - Error::Sqlx(_) => tonic::Status::internal("Database error"), + Error::Sqlx(e) => { + tracing::error!(error = &e as &dyn std::error::Error); + tonic::Status::internal("Database error") + } } } } From acb8461744657920e65a577d2d480f673efdaa8a Mon Sep 17 00:00:00 2001 From: ras0q Date: Fri, 24 Jan 2025 09:39:38 +0900 Subject: [PATCH 06/10] get user_id from session --- server/src/reaction/grpc.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/server/src/reaction/grpc.rs b/server/src/reaction/grpc.rs index edd3d497..641aa037 100644 --- a/server/src/reaction/grpc.rs +++ b/server/src/reaction/grpc.rs @@ -57,7 +57,7 @@ where #[async_trait::async_trait] impl schema::reaction_service_server::ReactionService for ServiceImpl where - State: super::ProvideReactionService, + State: super::ProvideReactionService + crate::session::ProvideSessionService, { async fn get_reaction( &self, @@ -86,14 +86,21 @@ where &self, request: tonic::Request, ) -> Result, tonic::Status> { - let (_, _, schema::CreateReactionRequest { kind, position }) = request.into_parts(); + let (meta, _, schema::CreateReactionRequest { kind, position }) = request.into_parts(); let Some(position) = position else { return Err(tonic::Status::invalid_argument("Position is required")); }; - // TODO: Get user_id from context (cookie?) - let user_id = uuid::Uuid::parse_str("123e4567-e89b-12d3-a456-426614174000").unwrap(); + + let header_map = meta.into_headers(); + let user_id = self + .state + .extract(crate::session::ExtractParams(&header_map)) + .await + .map_err(IntoStatus::into_status)? + .user_id; + let params = super::CreateReactionParams { - user_id: crate::user::UserId(user_id), + user_id, kind, position: position.into(), }; From ace2514ca3f9661366eb20fbd593d4cea69bc579 Mon Sep 17 00:00:00 2001 From: ras0q Date: Fri, 24 Jan 2025 09:42:26 +0900 Subject: [PATCH 07/10] include expires_at in reaction table --- server/migrations/1_init.sql | 1 + server/src/reaction/impl.rs | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/server/migrations/1_init.sql b/server/migrations/1_init.sql index 3fed0478..e17ee91d 100644 --- a/server/migrations/1_init.sql +++ b/server/migrations/1_init.sql @@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS `reactions` ( `kind` VARCHAR(255) NOT NULL, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `expires_at` TIMESTAMP NOT NULL, PRIMARY KEY (`id`), FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ); diff --git a/server/src/reaction/impl.rs b/server/src/reaction/impl.rs index 65ee8e25..0c30e263 100644 --- a/server/src/reaction/impl.rs +++ b/server/src/reaction/impl.rs @@ -36,6 +36,7 @@ struct ReactionRow { pub kind: String, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, + pub expires_at: chrono::DateTime, } impl From for super::Reaction { @@ -86,12 +87,13 @@ async fn create_reaction( kind, created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), + expires_at: chrono::Utc::now() + chrono::Duration::seconds(10), }; sqlx::query( r#" INSERT INTO `reactions` - (`id`, `user_id`, `position_x`, `position_y`, `kind`, `created_at`, `updated_at`) - VALUES (?, ?, ?, ?, ?, ?, ?) + (`id`, `user_id`, `position_x`, `position_y`, `kind`, `created_at`, `updated_at`, `expires_at`) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) "#, ) .bind(reaction.id) @@ -101,6 +103,7 @@ async fn create_reaction( .bind(reaction.kind) .bind(reaction.created_at) .bind(reaction.updated_at) + .bind(reaction.expires_at) .execute(pool) .await?; tracing::info!(id = %reaction.id, "Created a reaction"); From bef7461c402674f014abdc71aef1adf4bd6e37e3 Mon Sep 17 00:00:00 2001 From: ras0q Date: Fri, 24 Jan 2025 20:58:09 +0900 Subject: [PATCH 08/10] =?UTF-8?q?build=5Fserver=E3=82=92=E5=A4=96=E3=81=AB?= =?UTF-8?q?=E5=87=BA=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/reaction.rs | 14 +++++++------- server/src/reaction/grpc.rs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/reaction.rs b/server/src/reaction.rs index 4325f14d..1c1755ad 100644 --- a/server/src/reaction.rs +++ b/server/src/reaction.rs @@ -82,14 +82,14 @@ pub trait ProvideReactionService: Send + Sync + 'static { let ctx = self.context(); self.reaction_service().create_reaction(ctx, params) } +} - fn build_server(this: Arc) -> ReactionServiceServer - where - Self: Sized, - { - let service = grpc::ServiceImpl::new(this); - ReactionServiceServer::new(service) - } +pub fn build_server(this: Arc) -> ReactionServiceServer +where + State: ProvideReactionService + crate::session::ProvideSessionService, +{ + let service = grpc::ServiceImpl::new(this); + ReactionServiceServer::new(service) } #[derive(Debug, Clone, Copy, Default)] diff --git a/server/src/reaction/grpc.rs b/server/src/reaction/grpc.rs index 641aa037..7093ed3f 100644 --- a/server/src/reaction/grpc.rs +++ b/server/src/reaction/grpc.rs @@ -47,7 +47,7 @@ where impl ServiceImpl where - State: super::ProvideReactionService, + State: super::ProvideReactionService + crate::session::ProvideSessionService, { pub(super) fn new(state: Arc) -> Self { Self { state } From d6cb4702416bc4bae4847cf412dc6fff545446eb Mon Sep 17 00:00:00 2001 From: ras0q Date: Fri, 24 Jan 2025 21:35:13 +0900 Subject: [PATCH 09/10] publish event on reaction created --- server/src/reaction/error.rs | 6 ++++++ server/src/reaction/impl.rs | 13 ++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/server/src/reaction/error.rs b/server/src/reaction/error.rs index 7901df09..613693ed 100644 --- a/server/src/reaction/error.rs +++ b/server/src/reaction/error.rs @@ -4,6 +4,8 @@ pub enum Error { NotFound, #[error("Database error")] Sqlx(#[from] sqlx::Error), + #[error(transparent)] + Status(#[from] tonic::Status), } impl From for tonic::Status { @@ -14,6 +16,10 @@ impl From for tonic::Status { tracing::error!(error = &e as &dyn std::error::Error); tonic::Status::internal("Database error") } + Error::Status(e) => { + tracing::error!(error = &e as &dyn std::error::Error); + tonic::Status::internal("Status error") + } } } } diff --git a/server/src/reaction/impl.rs b/server/src/reaction/impl.rs index 0c30e263..2194424d 100644 --- a/server/src/reaction/impl.rs +++ b/server/src/reaction/impl.rs @@ -4,7 +4,7 @@ use sqlx::MySqlPool; impl super::ReactionService for super::ReactionServiceImpl where - Context: AsRef, + Context: AsRef + crate::event::ProvideEventService, { type Error = super::Error; @@ -21,7 +21,7 @@ where ctx: &'a Context, params: super::CreateReactionParams, ) -> future::BoxFuture<'a, Result> { - create_reaction(ctx.as_ref(), params).boxed() + create_reaction(ctx, ctx.as_ref(), params).boxed() } } @@ -70,7 +70,8 @@ async fn get_reaction( reaction.map(Into::into).ok_or(super::Error::NotFound) } -async fn create_reaction( +async fn create_reaction( + event_service: &P, pool: &MySqlPool, params: super::CreateReactionParams, ) -> Result { @@ -114,5 +115,11 @@ async fn create_reaction( }, ) .await?; + + event_service + .publish_event(crate::event::Event::Reaction(reaction.clone())) + .await + .map_err(|e| super::Error::Status(e.into()))?; + Ok(reaction) } From 18d2a79a0fac381ebbb064e4679933df863bdfa2 Mon Sep 17 00:00:00 2001 From: H1rono_K <54711422+H1rono@users.noreply.github.com> Date: Fri, 24 Jan 2025 21:45:37 +0900 Subject: [PATCH 10/10] Update server/src/reaction/impl.rs --- server/src/reaction/impl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/reaction/impl.rs b/server/src/reaction/impl.rs index 2194424d..e4077541 100644 --- a/server/src/reaction/impl.rs +++ b/server/src/reaction/impl.rs @@ -119,7 +119,7 @@ async fn create_reaction( event_service .publish_event(crate::event::Event::Reaction(reaction.clone())) .await - .map_err(|e| super::Error::Status(e.into()))?; + .map_err(crate::prelude::IntoStatus::into_status)?; Ok(reaction) }