|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- use axum::Json;
- use axum::extract::{Path, Query, State};
- use axum::http::StatusCode;
- use uuid::Uuid;
- use validator::Validate;
-
- use crate::error::{ApiError, ApiResult};
- use crate::models::item::{CreateItemRequest, Item, ItemPage, ItemResponse, UpdateItemRequest};
- use crate::models::pagination::PaginationQuery;
- use crate::state::AppState;
-
- /// List items, newest first.
- #[utoipa::path(
- get,
- path = "/api/v1/items",
- tag = "items",
- params(PaginationQuery),
- responses(
- (status = 200, description = "Paged item list", body = ItemPage)
- )
- )]
- pub async fn list_items(
- State(state): State<AppState>,
- Query(query): Query<PaginationQuery>,
- ) -> ApiResult<Json<ItemPage>> {
- let query = query.sanitize();
- let total =
- sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM items").fetch_one(state.db()).await?;
-
- let items = sqlx::query_as::<_, Item>(
- r#"
- SELECT id, name, description, created_at, updated_at
- FROM items
- ORDER BY created_at DESC
- LIMIT $1 OFFSET $2
- "#,
- )
- .bind(query.limit())
- .bind(query.offset())
- .fetch_all(state.db())
- .await?;
-
- Ok(Json(ItemPage {
- items: items.into_iter().map(ItemResponse::from).collect(),
- page: query.page,
- per_page: query.per_page,
- total,
- }))
- }
-
- /// Fetch a single item.
- #[utoipa::path(
- get,
- path = "/api/v1/items/{id}",
- tag = "items",
- params(("id" = Uuid, Path, description = "Item id")),
- responses(
- (status = 200, description = "Item", body = ItemResponse),
- (status = 404, description = "Missing item", body = crate::error::ErrorBody)
- )
- )]
- pub async fn get_item(
- State(state): State<AppState>,
- Path(id): Path<Uuid>,
- ) -> ApiResult<Json<ItemResponse>> {
- let item = sqlx::query_as::<_, Item>(
- r#"
- SELECT id, name, description, created_at, updated_at
- FROM items
- WHERE id = $1
- "#,
- )
- .bind(id)
- .fetch_optional(state.db())
- .await?
- .ok_or_else(|| ApiError::NotFound(format!("item {id} not found")))?;
-
- Ok(Json(item.into()))
- }
-
- /// Create an item.
- #[utoipa::path(
- post,
- path = "/api/v1/items",
- tag = "items",
- request_body = CreateItemRequest,
- responses(
- (status = 201, description = "Created item", body = ItemResponse),
- (status = 400, description = "Validation error", body = crate::error::ErrorBody)
- )
- )]
- pub async fn create_item(
- State(state): State<AppState>,
- Json(body): Json<CreateItemRequest>,
- ) -> ApiResult<(StatusCode, Json<ItemResponse>)> {
- body.validate().map_err(|err| ApiError::Validation(err.to_string()))?;
-
- let item = sqlx::query_as::<_, Item>(
- r#"
- INSERT INTO items (name, description)
- VALUES ($1, $2)
- RETURNING id, name, description, created_at, updated_at
- "#,
- )
- .bind(body.name.trim())
- .bind(body.description.as_deref())
- .fetch_one(state.db())
- .await?;
-
- Ok((StatusCode::CREATED, Json(item.into())))
- }
-
- /// Replace selected item fields.
- #[utoipa::path(
- patch,
- path = "/api/v1/items/{id}",
- tag = "items",
- params(("id" = Uuid, Path, description = "Item id")),
- request_body = UpdateItemRequest,
- responses(
- (status = 200, description = "Updated item", body = ItemResponse),
- (status = 404, description = "Missing item", body = crate::error::ErrorBody)
- )
- )]
- pub async fn update_item(
- State(state): State<AppState>,
- Path(id): Path<Uuid>,
- Json(body): Json<UpdateItemRequest>,
- ) -> ApiResult<Json<ItemResponse>> {
- body.validate().map_err(|err| ApiError::Validation(err.to_string()))?;
-
- let item = sqlx::query_as::<_, Item>(
- r#"
- UPDATE items
- SET
- name = COALESCE($2, name),
- description = COALESCE($3, description),
- updated_at = now()
- WHERE id = $1
- RETURNING id, name, description, created_at, updated_at
- "#,
- )
- .bind(id)
- .bind(body.name.as_deref().map(str::trim))
- .bind(body.description.as_deref())
- .fetch_optional(state.db())
- .await?
- .ok_or_else(|| ApiError::NotFound(format!("item {id} not found")))?;
-
- Ok(Json(item.into()))
- }
-
- /// Delete an item.
- #[utoipa::path(
- delete,
- path = "/api/v1/items/{id}",
- tag = "items",
- params(("id" = Uuid, Path, description = "Item id")),
- responses(
- (status = 204, description = "Deleted"),
- (status = 404, description = "Missing item", body = crate::error::ErrorBody)
- )
- )]
- pub async fn delete_item(
- State(state): State<AppState>,
- Path(id): Path<Uuid>,
- ) -> ApiResult<StatusCode> {
- let result =
- sqlx::query("DELETE FROM items WHERE id = $1").bind(id).execute(state.db()).await?;
- if result.rows_affected() == 0 {
- return Err(ApiError::NotFound(format!("item {id} not found")));
- }
- Ok(StatusCode::NO_CONTENT)
- }
|