From 15c744e995805a30700cb04c488cddbb3015316b Mon Sep 17 00:00:00 2001 From: Tolmachev Igor Date: Thu, 25 Sep 2025 01:20:24 +0300 Subject: Add basic queue CRUD --- src/api.rs | 1 + src/error/client.rs | 10 ++- src/models.rs | 19 ++++- src/routers/mod.rs | 5 +- src/routers/queue.rs | 224 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 255 insertions(+), 4 deletions(-) create mode 100644 src/routers/queue.rs (limited to 'src') diff --git a/src/api.rs b/src/api.rs index 23fb74b..8a6d599 100644 --- a/src/api.rs +++ b/src/api.rs @@ -7,6 +7,7 @@ use crate::ErrorResponse; pub mod tags { pub const ACCOUNT: &str = "Account"; + pub const QUEUE: &str = "Queue"; } struct AuthModifier; diff --git a/src/error/client.rs b/src/error/client.rs index 980e3d2..70b6001 100644 --- a/src/error/client.rs +++ b/src/error/client.rs @@ -4,6 +4,8 @@ pub enum ClientError { UserAlreadyExists { username: String }, InvalidPassword, NotAuthorized, + UserNotFound { id: i64 }, + QueueNotFound { id: i64 }, } impl ClientError { @@ -14,6 +16,8 @@ impl ClientError { Self::UserAlreadyExists { .. } => "UserAlreadyExists", Self::InvalidPassword => "InvalidPassword", Self::NotAuthorized => "NotAuthorized", + Self::UserNotFound { .. } => "UserNotFound", + Self::QueueNotFound { .. } => "QueueNotFound", } .to_string() } @@ -25,9 +29,11 @@ impl ClientError { Self::UserAlreadyExists { username } => { format!("user with username `{}` already exists", username) } - Self::InvalidPassword => "password is invalid".to_string(), + Self::InvalidPassword => format!("password is invalid"), - Self::NotAuthorized => "user is not authorized".to_string(), + Self::NotAuthorized => format!("user is not authorized"), + Self::UserNotFound { id } => format!("user with id `{}` not found", id), + Self::QueueNotFound { id } => format!("queue with id `{}` not found", id), } } } diff --git a/src/models.rs b/src/models.rs index b7631a4..4821ec3 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,4 +1,4 @@ -use entity::users; +use entity::{queues, users}; use serde::Serialize; use utoipa::ToSchema; @@ -25,3 +25,20 @@ impl From for Account { } } } + +#[derive(Serialize, ToSchema)] +pub struct Queue { + #[schema(examples(1))] + pub id: i64, + #[schema(examples("John's queue", "Очередь Ивана"))] + pub name: String, +} + +impl From for Queue { + fn from(value: queues::Model) -> Self { + Self { + id: value.id, + name: value.name, + } + } +} diff --git a/src/routers/mod.rs b/src/routers/mod.rs index 80f9e74..c73e1f8 100644 --- a/src/routers/mod.rs +++ b/src/routers/mod.rs @@ -1,4 +1,5 @@ mod account; +mod queue; use utoipa::OpenApi; use utoipa_axum::router::OpenApiRouter; @@ -6,5 +7,7 @@ use utoipa_axum::router::OpenApiRouter; use crate::{AppOpenApi, AppState}; pub fn router() -> OpenApiRouter { - OpenApiRouter::with_openapi(AppOpenApi::openapi()).nest("/account", account::router()) + OpenApiRouter::with_openapi(AppOpenApi::openapi()) + .nest("/account", account::router()) + .nest("/queue", queue::routes()) } diff --git a/src/routers/queue.rs b/src/routers/queue.rs new file mode 100644 index 0000000..1e93b2a --- /dev/null +++ b/src/routers/queue.rs @@ -0,0 +1,224 @@ +use axum::extract::State; +use entity::{queues, users}; +use sea_orm::{ + ActiveModelTrait, ActiveValue::Set, ColumnTrait, DatabaseConnection, EntityTrait, + IntoActiveModel, ModelTrait, QueryFilter, +}; +use serde::Deserialize; +use utoipa::ToSchema; +use utoipa_axum::{router::OpenApiRouter, routes}; + +use crate::{ + ApiError, ApiResult, AppState, ClientError, GlobalResponses, SuccessResponse, + extract::{ApiJson, Auth}, + models::Queue, + tags::QUEUE, +}; + +async fn user_exists(id: i64, db: &DatabaseConnection) -> Result { + Ok(users::Entity::find_by_id(id).one(db).await?.is_some()) +} + +async fn get_owned_queue( + id: i64, + owner_id: i64, + db: &DatabaseConnection, +) -> Result { + Ok(queues::Entity::find_by_id(id) + .filter(queues::Column::OwnerId.eq(owner_id)) + .one(db) + .await? + .ok_or(ClientError::QueueNotFound { id })?) +} + +#[derive(Deserialize, ToSchema)] +struct CreateQueueRequest { + #[schema(examples("John's queue", "Очередь Ивана"))] + name: String, +} + +#[derive(Deserialize, ToSchema)] +struct ChangeQueueNameRequest { + #[schema(examples(1))] + id: i64, + #[schema(examples("John's queue", "Очередь Ивана"))] + new_name: String, +} + +#[derive(Deserialize, ToSchema)] +struct ChangeQueueOwnerRequest { + #[schema(examples(1))] + id: i64, + #[schema(examples(1))] + new_owner_id: i64, +} + +#[derive(Deserialize, ToSchema)] +struct DeleteQueueRequest { + #[schema(examples(1))] + id: i64, +} + +#[utoipa::path( + get, + path = "/owned", + tag = QUEUE, + summary = "Get owned", + description = "Get your own queues", + responses( + ( + status = 200, body = SuccessResponse>, + description = "Success response with your queues" + ), + GlobalResponses + ), + security(("auth" = [])), +)] +async fn owned(State(state): State, Auth(user): Auth) -> ApiResult> { + return Ok(SuccessResponse::ok( + queues::Entity::find() + .filter(queues::Column::OwnerId.eq(user.id)) + .all(&state.db) + .await? + .into_iter() + .map(Into::into) + .collect(), + )); +} + +#[utoipa::path( + post, + path = "/create", + tag = QUEUE, + summary = "Create", + description = "Create a new queue", + request_body = CreateQueueRequest, + responses( + ( + status = 200, body = SuccessResponse, + description = "Success response with created queue" + ), + GlobalResponses + ), + security(("auth" = [])), +)] +async fn create( + State(state): State, + Auth(user): Auth, + ApiJson(req): ApiJson, +) -> ApiResult { + Ok(SuccessResponse::ok( + queues::ActiveModel { + owner_id: Set(user.id), + name: Set(req.name), + ..Default::default() + } + .insert(&state.db) + .await? + .into(), + )) +} + +#[utoipa::path( + put, + path = "/change/name", + tag = QUEUE, + summary = "Change name", + description = "Change queue name", + request_body = ChangeQueueNameRequest, + responses( + ( + status = 200, body = SuccessResponse, + description = "Success response with changed queue data" + ), + GlobalResponses + ), + security(("auth" = [])), +)] +async fn change_name( + State(state): State, + Auth(user): Auth, + ApiJson(req): ApiJson, +) -> ApiResult { + let mut active_queue = get_owned_queue(req.id, user.id, &state.db) + .await? + .into_active_model(); + + active_queue.name = Set(req.new_name); + + let queue = active_queue.update(&state.db).await?; + Ok(SuccessResponse::ok(queue.into())) +} + +#[utoipa::path( + put, + path = "/change/owner", + tag = QUEUE, + summary = "Change owner", + description = "Transfer ownership of the queue", + request_body = ChangeQueueOwnerRequest, + responses( + ( + status = 200, body = SuccessResponse, + description = "Success response with changed queue data" + ), + GlobalResponses + ), + security(("auth" = [])), +)] +async fn change_owner( + State(state): State, + Auth(user): Auth, + ApiJson(req): ApiJson, +) -> ApiResult { + if !user_exists(req.new_owner_id, &state.db).await? { + return Err(ClientError::UserNotFound { + id: req.new_owner_id, + } + .into()); + } + + let mut active_queue = get_owned_queue(req.id, user.id, &state.db) + .await? + .into_active_model(); + + active_queue.owner_id = Set(req.new_owner_id); + + let queue = active_queue.update(&state.db).await?; + Ok(SuccessResponse::ok(queue.into())) +} + +#[utoipa::path( + delete, + path = "/delete", + tag = QUEUE, + summary = "Delete", + description = "Delete queue", + request_body = DeleteQueueRequest, + responses( + ( + status = 200, body = SuccessResponse, + description = "Success response with deleted queue data" + ), + GlobalResponses + ), + security(("auth" = [])), +)] +async fn delete( + State(state): State, + Auth(user): Auth, + ApiJson(req): ApiJson, +) -> ApiResult { + let queue = get_owned_queue(req.id, user.id, &state.db).await?; + queue.clone().delete(&state.db).await?; + Ok(SuccessResponse::ok(queue.into())) +} + +pub fn routes() -> OpenApiRouter { + OpenApiRouter::new() + .routes(routes!(owned)) + .routes(routes!(create)) + .routes(routes!(change_name)) + .routes(routes!(change_owner)) + .routes(routes!(delete)) +} -- cgit v1.3