Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

items.rs 4.8 KiB

há 3 semanas
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. use axum::Json;
  2. use axum::extract::{Path, Query, State};
  3. use axum::http::StatusCode;
  4. use uuid::Uuid;
  5. use validator::Validate;
  6. use crate::error::{ApiError, ApiResult};
  7. use crate::models::item::{CreateItemRequest, Item, ItemPage, ItemResponse, UpdateItemRequest};
  8. use crate::models::pagination::PaginationQuery;
  9. use crate::state::AppState;
  10. /// List items, newest first.
  11. #[utoipa::path(
  12. get,
  13. path = "/api/v1/items",
  14. tag = "items",
  15. params(PaginationQuery),
  16. responses(
  17. (status = 200, description = "Paged item list", body = ItemPage)
  18. )
  19. )]
  20. pub async fn list_items(
  21. State(state): State<AppState>,
  22. Query(query): Query<PaginationQuery>,
  23. ) -> ApiResult<Json<ItemPage>> {
  24. let query = query.sanitize();
  25. let total =
  26. sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM items").fetch_one(state.db()).await?;
  27. let items = sqlx::query_as::<_, Item>(
  28. r#"
  29. SELECT id, name, description, created_at, updated_at
  30. FROM items
  31. ORDER BY created_at DESC
  32. LIMIT $1 OFFSET $2
  33. "#,
  34. )
  35. .bind(query.limit())
  36. .bind(query.offset())
  37. .fetch_all(state.db())
  38. .await?;
  39. Ok(Json(ItemPage {
  40. items: items.into_iter().map(ItemResponse::from).collect(),
  41. page: query.page,
  42. per_page: query.per_page,
  43. total,
  44. }))
  45. }
  46. /// Fetch a single item.
  47. #[utoipa::path(
  48. get,
  49. path = "/api/v1/items/{id}",
  50. tag = "items",
  51. params(("id" = Uuid, Path, description = "Item id")),
  52. responses(
  53. (status = 200, description = "Item", body = ItemResponse),
  54. (status = 404, description = "Missing item", body = crate::error::ErrorBody)
  55. )
  56. )]
  57. pub async fn get_item(
  58. State(state): State<AppState>,
  59. Path(id): Path<Uuid>,
  60. ) -> ApiResult<Json<ItemResponse>> {
  61. let item = sqlx::query_as::<_, Item>(
  62. r#"
  63. SELECT id, name, description, created_at, updated_at
  64. FROM items
  65. WHERE id = $1
  66. "#,
  67. )
  68. .bind(id)
  69. .fetch_optional(state.db())
  70. .await?
  71. .ok_or_else(|| ApiError::NotFound(format!("item {id} not found")))?;
  72. Ok(Json(item.into()))
  73. }
  74. /// Create an item.
  75. #[utoipa::path(
  76. post,
  77. path = "/api/v1/items",
  78. tag = "items",
  79. request_body = CreateItemRequest,
  80. responses(
  81. (status = 201, description = "Created item", body = ItemResponse),
  82. (status = 400, description = "Validation error", body = crate::error::ErrorBody)
  83. )
  84. )]
  85. pub async fn create_item(
  86. State(state): State<AppState>,
  87. Json(body): Json<CreateItemRequest>,
  88. ) -> ApiResult<(StatusCode, Json<ItemResponse>)> {
  89. body.validate().map_err(|err| ApiError::Validation(err.to_string()))?;
  90. let item = sqlx::query_as::<_, Item>(
  91. r#"
  92. INSERT INTO items (name, description)
  93. VALUES ($1, $2)
  94. RETURNING id, name, description, created_at, updated_at
  95. "#,
  96. )
  97. .bind(body.name.trim())
  98. .bind(body.description.as_deref())
  99. .fetch_one(state.db())
  100. .await?;
  101. Ok((StatusCode::CREATED, Json(item.into())))
  102. }
  103. /// Replace selected item fields.
  104. #[utoipa::path(
  105. patch,
  106. path = "/api/v1/items/{id}",
  107. tag = "items",
  108. params(("id" = Uuid, Path, description = "Item id")),
  109. request_body = UpdateItemRequest,
  110. responses(
  111. (status = 200, description = "Updated item", body = ItemResponse),
  112. (status = 404, description = "Missing item", body = crate::error::ErrorBody)
  113. )
  114. )]
  115. pub async fn update_item(
  116. State(state): State<AppState>,
  117. Path(id): Path<Uuid>,
  118. Json(body): Json<UpdateItemRequest>,
  119. ) -> ApiResult<Json<ItemResponse>> {
  120. body.validate().map_err(|err| ApiError::Validation(err.to_string()))?;
  121. let item = sqlx::query_as::<_, Item>(
  122. r#"
  123. UPDATE items
  124. SET
  125. name = COALESCE($2, name),
  126. description = COALESCE($3, description),
  127. updated_at = now()
  128. WHERE id = $1
  129. RETURNING id, name, description, created_at, updated_at
  130. "#,
  131. )
  132. .bind(id)
  133. .bind(body.name.as_deref().map(str::trim))
  134. .bind(body.description.as_deref())
  135. .fetch_optional(state.db())
  136. .await?
  137. .ok_or_else(|| ApiError::NotFound(format!("item {id} not found")))?;
  138. Ok(Json(item.into()))
  139. }
  140. /// Delete an item.
  141. #[utoipa::path(
  142. delete,
  143. path = "/api/v1/items/{id}",
  144. tag = "items",
  145. params(("id" = Uuid, Path, description = "Item id")),
  146. responses(
  147. (status = 204, description = "Deleted"),
  148. (status = 404, description = "Missing item", body = crate::error::ErrorBody)
  149. )
  150. )]
  151. pub async fn delete_item(
  152. State(state): State<AppState>,
  153. Path(id): Path<Uuid>,
  154. ) -> ApiResult<StatusCode> {
  155. let result =
  156. sqlx::query("DELETE FROM items WHERE id = $1").bind(id).execute(state.db()).await?;
  157. if result.rows_affected() == 0 {
  158. return Err(ApiError::NotFound(format!("item {id} not found")));
  159. }
  160. Ok(StatusCode::NO_CONTENT)
  161. }