A simple to-do app focused on tasks that can be completed within a specific time span.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

basic crd tag api

tobinio 16312907 449386bb

+113 -17
+63 -8
api/src/routes/tags.rs
··· 1 + use axum::extract::Path; 2 + use axum::routing::{delete, post}; 1 3 use axum::{Json, Router, extract::State, routing::get}; 2 4 use http::StatusCode; 3 - use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; 4 - use tracing::info; 5 - use types::Tag as TagModel; 5 + use sea_orm::ActiveValue::Set; 6 + use sea_orm::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter}; 7 + use types::{HexColor, Tag as TagModel}; 8 + use uuid::Uuid; 6 9 7 10 use crate::{AppState, auth::User}; 8 11 9 12 use crate::entities::{prelude::*, tag}; 10 13 11 14 pub fn routes(state: AppState) -> Router { 12 - Router::new().route("/", get(get_all)).with_state(state) 15 + Router::new() 16 + .route("/", get(get_all_tags)) 17 + .route("/", post(add_tag)) 18 + .route("/{tag_uuid}", delete(delete_tag)) 19 + .with_state(state) 20 + } 21 + 22 + async fn add_tag( 23 + state: State<AppState>, 24 + user: User, 25 + Json(tag): Json<TagModel>, 26 + ) -> Result<Json<TagModel>, (StatusCode, &'static str)> { 27 + let new_tag = Tag::insert(tag::ActiveModel { 28 + id: Set(tag.uuid), 29 + owner_id: Set(user.uuid), 30 + name: Set(tag.name), 31 + color: Set(tag.color.as_str().to_string()), 32 + }) 33 + .exec_with_returning(&state.db_connection) 34 + .await 35 + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "failed to add tag"))?; 36 + 37 + Ok(Json(TagModel { 38 + uuid: new_tag.id, 39 + name: new_tag.name, 40 + color: HexColor::from_str(&new_tag.color).unwrap(), 41 + })) 13 42 } 14 43 15 - async fn get_all( 44 + async fn get_all_tags( 16 45 state: State<AppState>, 17 46 user: User, 18 47 ) -> Result<Json<Vec<TagModel>>, (StatusCode, &'static str)> { ··· 21 50 .all(&state.db_connection) 22 51 .await 23 52 .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "failed to fetch tags"))?; 24 - 25 - info!("{:?}", user.uuid); 26 53 27 54 Ok(Json( 28 55 tags.into_iter() 29 56 .map(|tag| TagModel { 30 57 uuid: tag.id, 31 58 name: tag.name, 32 - color: tag.color, 59 + color: HexColor::from_str(&tag.color).unwrap(), 33 60 }) 34 61 .collect(), 35 62 )) 36 63 } 64 + 65 + async fn delete_tag( 66 + state: State<AppState>, 67 + Path(tag_uuid): Path<Uuid>, 68 + user: User, 69 + ) -> Result<Json<TagModel>, (StatusCode, &'static str)> { 70 + let deleted_tag = Tag::find_by_id(tag_uuid) 71 + .filter(tag::Column::OwnerId.eq(user.uuid)) 72 + .one(&state.db_connection) 73 + .await 74 + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "failed to fetch tags"))?; 75 + 76 + let Some(tag) = deleted_tag else { 77 + return Err((StatusCode::NOT_FOUND, "tag not found")); 78 + }; 79 + 80 + let deleted_tag = TagModel { 81 + uuid: tag.id, 82 + name: tag.name.to_string(), 83 + color: HexColor::from_str(&tag.color).unwrap(), 84 + }; 85 + 86 + tag.delete(&state.db_connection) 87 + .await 88 + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "failed to delete tag"))?; 89 + 90 + Ok(Json(deleted_tag)) 91 + }
+2 -2
api/tests/categories/add.rs
··· 1 1 use crate::common::{client::get_client, db::clear_db}; 2 2 use serde_json::json; 3 3 use serial_test::serial; 4 - use types::Category; 4 + use types::{Category, HexColor}; 5 5 use uuid::Uuid; 6 6 7 7 #[tokio::test] ··· 14 14 let category = Category { 15 15 uuid: category_uuid, 16 16 name: "Test Tag".to_string(), 17 - color: "#233212".to_string(), 17 + color: HexColor::from_str("#233212").unwrap(), 18 18 icon: "icon".to_string(), 19 19 }; 20 20
+4 -4
api/tests/tags/add.rs
··· 1 1 use crate::common::{client::get_client, db::clear_db}; 2 2 use serde_json::json; 3 3 use serial_test::serial; 4 - use types::Tag; 4 + use types::{HexColor, Tag}; 5 5 use uuid::Uuid; 6 6 7 7 #[tokio::test] ··· 14 14 let tag = Tag { 15 15 uuid: tag_uuid, 16 16 name: "Test Tag".to_string(), 17 - color: "#233212".to_string(), 17 + color: HexColor::from_str("#233212").unwrap(), 18 18 }; 19 19 20 20 let response = client ··· 52 52 } 53 53 )) 54 54 .await; 55 - assert_eq!(response.status().as_str(), "400"); 55 + assert_eq!(response.status().as_str(), "422"); 56 56 57 57 let response = client 58 58 .add_tag(json!( ··· 63 63 } 64 64 )) 65 65 .await; 66 - assert_eq!(response.status().as_str(), "400"); 66 + assert_eq!(response.status().as_str(), "422"); 67 67 }
+44 -3
libs/types/src/lib.rs
··· 1 1 use std::ops::Add; 2 2 3 3 use chrono::{DateTime, Days, NaiveDate, Utc}; 4 - use serde::{Deserialize, Serialize}; 4 + use serde::{Deserialize, Deserializer, Serialize}; 5 5 use uuid::Uuid; 6 6 7 7 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] ··· 124 124 } 125 125 } 126 126 127 + #[derive(Debug, Clone, PartialEq, Eq)] 128 + pub struct HexColor(String); 129 + 130 + impl HexColor { 131 + pub fn as_str(&self) -> &str { 132 + &self.0 133 + } 134 + 135 + pub fn from_str(s: &str) -> Option<Self> { 136 + if is_valid_hex_color(s) { 137 + Some(HexColor(s.to_string())) 138 + } else { 139 + None 140 + } 141 + } 142 + } 143 + 144 + impl<'de> Deserialize<'de> for HexColor { 145 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 146 + where 147 + D: Deserializer<'de>, 148 + { 149 + let s = String::deserialize(deserializer)?; 150 + HexColor::from_str(&s).ok_or(serde::de::Error::custom("invalid hex color")) 151 + } 152 + } 153 + 154 + impl Serialize for HexColor { 155 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 156 + where 157 + S: serde::Serializer, 158 + { 159 + self.0.serialize(serializer) 160 + } 161 + } 162 + 163 + fn is_valid_hex_color(s: &str) -> bool { 164 + let s = s.strip_prefix('#').unwrap_or(s); 165 + matches!(s.len(), 3 | 6) && s.chars().all(|c| c.is_ascii_hexdigit()) 166 + } 167 + 127 168 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 128 169 pub struct Tag { 129 170 pub uuid: Uuid, 130 171 pub name: String, 131 - pub color: String, 172 + pub color: HexColor, 132 173 } 133 174 134 175 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 135 176 pub struct Category { 136 177 pub uuid: Uuid, 137 178 pub name: String, 138 - pub color: String, 179 + pub color: HexColor, 139 180 pub icon: String, 140 181 }