diff --git a/thoughts-backend/.env b/thoughts-backend/.env index 791caa4..e12049f 100644 --- a/thoughts-backend/.env +++ b/thoughts-backend/.env @@ -1,6 +1,6 @@ HOST=0.0.0.0 PORT=8000 #DATABASE_URL="sqlite://dev.db" -# DATABASE_URL="postgresql://postgres:postgres@localhost/thoughts" -DATABASE_URL=postgres://thoughts_user:postgres@database:5432/thoughts_db +DATABASE_URL="postgresql://postgres:postgres@localhost/thoughts" +#DATABASE_URL=postgres://thoughts_user:postgres@database:5432/thoughts_db PREFORK=1 diff --git a/thoughts-backend/Cargo.lock b/thoughts-backend/Cargo.lock index 0d290f5..5c1723a 100644 --- a/thoughts-backend/Cargo.lock +++ b/thoughts-backend/Cargo.lock @@ -17,6 +17,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -145,6 +156,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-attributes" version = "1.1.2" @@ -418,6 +435,20 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "bigdecimal" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "bitflags" version = "2.9.1" @@ -427,6 +458,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -449,12 +492,57 @@ dependencies = [ "piper", ] +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -549,6 +637,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "common" +version = "0.1.0" +dependencies = [ + "sea-orm", + "serde", + "utoipa", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -698,6 +795,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -932,6 +1030,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -1123,6 +1227,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.4" @@ -1140,7 +1253,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown", + "hashbrown 0.15.4", ] [[package]] @@ -1476,7 +1589,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.4", "serde", ] @@ -1696,6 +1809,7 @@ dependencies = [ name = "models" version = "0.1.0" dependencies = [ + "common", "sea-orm", "serde", "serde_json", @@ -1725,6 +1839,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1882,6 +2006,15 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pgvector" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +dependencies = [ + "serde", +] + [[package]] name = "phf" version = "0.11.3" @@ -2031,6 +2164,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -2075,6 +2217,26 @@ dependencies = [ "yansi", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quinn" version = "0.11.8" @@ -2145,6 +2307,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -2257,6 +2425,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.12.20" @@ -2324,6 +2501,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rsa" version = "0.9.8" @@ -2378,6 +2584,22 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust_decimal" +version = "1.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.25" @@ -2495,18 +2717,25 @@ checksum = "18b7272b88bd608cd846de24f41b74a0315a135fe761b0aed4ec1ce6a6327a93" dependencies = [ "async-stream", "async-trait", + "bigdecimal", + "chrono", "futures-util", "log", "ouroboros", + "pgvector", + "rust_decimal", "sea-orm-macros", "sea-query", "sea-query-binder", "serde", + "serde_json", "sqlx", "strum 0.26.3", "thiserror 2.0.12", + "time", "tracing", "url", + "uuid", ] [[package]] @@ -2563,9 +2792,15 @@ version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64c91783d1514b99754fc6a4079081dcc2c587dadbff65c48c7f62297443536a" dependencies = [ + "bigdecimal", + "chrono", "inherent", "ordered-float", + "rust_decimal", "sea-query-derive", + "serde_json", + "time", + "uuid", ] [[package]] @@ -2574,8 +2809,14 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", "sea-query", + "serde_json", "sqlx", + "time", + "uuid", ] [[package]] @@ -2617,6 +2858,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "semver" version = "1.0.26" @@ -2863,6 +3110,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.1" @@ -2933,7 +3186,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "base64", + "bigdecimal", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -2942,23 +3197,26 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown", + "hashbrown 0.15.4", "hashlink", "indexmap", "log", "memchr", "once_cell", "percent-encoding", + "rust_decimal", "rustls", "serde", "serde_json", "sha2", "smallvec", "thiserror 2.0.12", + "time", "tokio", "tokio-stream", "tracing", "url", + "uuid", "webpki-roots 0.26.11", ] @@ -3008,9 +3266,11 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", + "bigdecimal", "bitflags", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -3031,6 +3291,7 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "rsa", + "rust_decimal", "serde", "sha1", "sha2", @@ -3038,7 +3299,9 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror 2.0.12", + "time", "tracing", + "uuid", "whoami", ] @@ -3050,8 +3313,10 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", + "bigdecimal", "bitflags", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -3066,8 +3331,10 @@ dependencies = [ "log", "md-5", "memchr", + "num-bigint", "once_cell", "rand 0.8.5", + "rust_decimal", "serde", "serde_json", "sha2", @@ -3075,7 +3342,9 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror 2.0.12", + "time", "tracing", + "uuid", "whoami", ] @@ -3086,6 +3355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -3099,8 +3369,10 @@ dependencies = [ "serde_urlencoded", "sqlx-core", "thiserror 2.0.12", + "time", "tracing", "url", + "uuid", ] [[package]] @@ -3214,6 +3486,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "thiserror" version = "1.0.69" @@ -3446,6 +3724,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.5.2" @@ -3784,6 +4079,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d" +[[package]] +name = "uuid" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "validator" version = "0.20.0" @@ -4309,6 +4615,15 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -4324,6 +4639,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/thoughts-backend/Cargo.toml b/thoughts-backend/Cargo.toml index 45c366f..9d9192a 100644 --- a/thoughts-backend/Cargo.toml +++ b/thoughts-backend/Cargo.toml @@ -16,12 +16,13 @@ members = ["api", "app", "doc", "models", "migration", "utils"] [workspace.dependencies] axum = { version = "0.8.4", default-features = false } tower = { version = "0.5.2", default-features = false } -sea-orm = { version = "1.1.12", default-features = false } +sea-orm = { version = "1.1.12" } serde = { version = "1.0.219", features = ["derive"] } -serde_json = { version = "1.0.140", default-features = false } +serde_json = { version = "1.0.140" } tracing = "0.1.41" -utoipa = { version = "5.4.0", default-features = false, features = ["macros"] } +utoipa = { version = "5.4.0", features = ["macros", "chrono"] } validator = { version = "0.20.0", default-features = false } +chrono = { version = "0.4.41", features = ["serde"] } [dependencies] api = { path = "api" } diff --git a/thoughts-backend/api/src/routers/blog.rs b/thoughts-backend/api/src/routers/blog.rs deleted file mode 100644 index 131b2cb..0000000 --- a/thoughts-backend/api/src/routers/blog.rs +++ /dev/null @@ -1,68 +0,0 @@ -use axum::{ - extract::{Query, State}, - http::StatusCode, - response::IntoResponse, - routing::get, - Router, -}; -use sea_orm::TryIntoModel; - -use app::persistence::blog::{create_blog, search_blogs}; -use app::state::AppState; -use models::params::blog::CreateBlogParams; -use models::queries::blog::BlogQuery; -use models::schemas::blog::{BlogListSchema, BlogSchema}; - -use crate::error::ApiError; -use crate::extractor::{Json, Valid}; -use crate::models::{ApiErrorResponse, ParamsErrorResponse}; - -#[utoipa::path( - post, - path = "", - request_body = CreateBlogParams, - responses( - (status = 201, description = "Blog created", body = BlogSchema), - (status = 400, description = "Bad request", body = ApiErrorResponse), - (status = 422, description = "Validation error", body = ParamsErrorResponse), - (status = 500, description = "Internal server error", body = ApiErrorResponse), - ) -)] -async fn blogs_post( - state: State, - Valid(Json(params)): Valid>, -) -> Result { - let blog = create_blog(&state.conn, params) - .await - .map_err(ApiError::from)?; - - let blog = blog.try_into_model().unwrap(); - Ok((StatusCode::CREATED, Json(BlogSchema::from(blog)))) -} - -#[utoipa::path( - get, - path = "", - params( - BlogQuery - ), - responses( - (status = 200, description = "List blogs", body = BlogListSchema), - (status = 500, description = "Internal server error", body = ApiErrorResponse), - ) -)] -async fn blogs_get( - state: State, - query: Query, -) -> Result { - let Query(query) = query; - - let blogs = search_blogs(&state.conn, query) - .await - .map_err(ApiError::from)?; - Ok(Json(BlogListSchema::from(blogs))) -} - -pub fn create_blog_router() -> Router { - Router::new().route("/", get(blogs_get).post(blogs_post)) -} diff --git a/thoughts-backend/api/src/routers/mod.rs b/thoughts-backend/api/src/routers/mod.rs index caae9d4..f67a55f 100644 --- a/thoughts-backend/api/src/routers/mod.rs +++ b/thoughts-backend/api/src/routers/mod.rs @@ -1,11 +1,9 @@ use axum::Router; -pub mod blog; pub mod root; pub mod user; use app::state::AppState; -use blog::create_blog_router; use root::create_root_router; use user::create_user_router; @@ -13,6 +11,5 @@ pub fn create_router(state: AppState) -> Router { Router::new() .merge(create_root_router()) .nest("/users", create_user_router()) - .nest("/blogs", create_blog_router()) .with_state(state) } diff --git a/thoughts-backend/app/src/persistence/blog.rs b/thoughts-backend/app/src/persistence/blog.rs deleted file mode 100644 index 93451f0..0000000 --- a/thoughts-backend/app/src/persistence/blog.rs +++ /dev/null @@ -1,24 +0,0 @@ -use sea_orm::{ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, QueryFilter, Set}; - -use models::{domains::blog, params::blog::CreateBlogParams, queries::blog::BlogQuery}; - -pub async fn search_blogs(db: &DbConn, query: BlogQuery) -> Result, DbErr> { - blog::Entity::find() - .filter(blog::Column::Title.contains(query.title.unwrap_or_default())) - .all(db) - .await -} - -pub async fn create_blog( - db: &DbConn, - params: CreateBlogParams, -) -> Result { - blog::ActiveModel { - author_id: Set(params.author_id as i32), - title: Set(params.title), - content: Set(params.content), - ..Default::default() - } - .save(db) - .await -} diff --git a/thoughts-backend/app/src/persistence/mod.rs b/thoughts-backend/app/src/persistence/mod.rs index 1d859a5..22d12a3 100644 --- a/thoughts-backend/app/src/persistence/mod.rs +++ b/thoughts-backend/app/src/persistence/mod.rs @@ -1,2 +1 @@ -pub mod blog; pub mod user; diff --git a/thoughts-backend/common/Cargo.toml b/thoughts-backend/common/Cargo.toml new file mode 100644 index 0000000..3ea2c1f --- /dev/null +++ b/thoughts-backend/common/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "common" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { workspace = true } +utoipa = { workspace = true } +sea-orm = { workspace = true } diff --git a/thoughts-backend/common/src/lib.rs b/thoughts-backend/common/src/lib.rs new file mode 100644 index 0000000..a01f8d8 --- /dev/null +++ b/thoughts-backend/common/src/lib.rs @@ -0,0 +1,14 @@ +use sea_orm::prelude::DateTimeWithTimeZone; +use serde::Serialize; +use utoipa::ToSchema; + +// Wrapper type for DateTimeWithTimeZone +#[derive(Serialize, ToSchema)] +#[schema(example = "2025-09-05T12:34:56Z")] // Example for OpenAPI +pub struct DateTimeWithTimeZoneWrapper(String); + +impl From for DateTimeWithTimeZoneWrapper { + fn from(value: DateTimeWithTimeZone) -> Self { + DateTimeWithTimeZoneWrapper(value.to_rfc3339()) + } +} diff --git a/thoughts-backend/doc/src/blog.rs b/thoughts-backend/doc/src/blog.rs deleted file mode 100644 index 418817e..0000000 --- a/thoughts-backend/doc/src/blog.rs +++ /dev/null @@ -1,20 +0,0 @@ -use utoipa::OpenApi; - -use models::params::blog::CreateBlogParams; -use models::schemas::blog::{BlogListSchema, BlogSchema}; - -use api::models::{ApiErrorResponse, ParamsErrorResponse}; -use api::routers::blog::*; - -#[derive(OpenApi)] -#[openapi( - paths(blogs_get, blogs_post), - components(schemas( - CreateBlogParams, - BlogListSchema, - BlogSchema, - ApiErrorResponse, - ParamsErrorResponse, - )) -)] -pub(super) struct BlogApi; diff --git a/thoughts-backend/doc/src/lib.rs b/thoughts-backend/doc/src/lib.rs index 2467b48..5ab5603 100644 --- a/thoughts-backend/doc/src/lib.rs +++ b/thoughts-backend/doc/src/lib.rs @@ -3,7 +3,6 @@ use utoipa::OpenApi; use utoipa_scalar::{Scalar, Servable as ScalarServable}; use utoipa_swagger_ui::SwaggerUi; -mod blog; mod root; mod user; @@ -12,13 +11,10 @@ mod user; nest( (path = "/", api = root::RootApi), (path = "/users", api = user::UserApi), - (path = "/blogs", api = blog::BlogApi), ), tags( (name = "root", description = "Root API"), (name = "user", description = "User API"), - (name = "blog", description = "Blog API"), - ) )] struct _ApiDoc; diff --git a/thoughts-backend/migration/src/lib.rs b/thoughts-backend/migration/src/lib.rs index fd709fb..b3f83ee 100644 --- a/thoughts-backend/migration/src/lib.rs +++ b/thoughts-backend/migration/src/lib.rs @@ -1,7 +1,7 @@ pub use sea_orm_migration::prelude::*; mod m20240101_000001_init; -mod m20240816_160144_blog; +mod m20250905_000001_init; pub struct Migrator; @@ -10,7 +10,7 @@ impl MigratorTrait for Migrator { fn migrations() -> Vec> { vec![ Box::new(m20240101_000001_init::Migration), - Box::new(m20240816_160144_blog::Migration), + Box::new(m20250905_000001_init::Migration), ] } } diff --git a/thoughts-backend/migration/src/m20240816_160144_blog.rs b/thoughts-backend/migration/src/m20240816_160144_blog.rs deleted file mode 100644 index c763872..0000000 --- a/thoughts-backend/migration/src/m20240816_160144_blog.rs +++ /dev/null @@ -1,47 +0,0 @@ -use sea_orm_migration::{prelude::*, schema::*}; - -use super::m20240101_000001_init::User; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .create_table( - Table::create() - .table(Blog::Table) - .if_not_exists() - .col(pk_auto(Blog::Id)) - .col(integer(Blog::AuthorId).not_null()) - .foreign_key( - ForeignKey::create() - .name("fk_blog_author_id") - .from(Blog::Table, Blog::AuthorId) - .to(User::Table, User::Id) - .on_update(ForeignKeyAction::NoAction) - .on_delete(ForeignKeyAction::Cascade), - ) - .col(ColumnDef::new(Blog::Title).string().not_null()) - .col(ColumnDef::new(Blog::Content).string().not_null()) - .to_owned(), - ) - .await - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_table(Table::drop().table(Blog::Table).to_owned()) - .await - } -} - -#[derive(DeriveIden)] -enum Blog { - Table, - Id, - AuthorId, - Title, - Content, -} diff --git a/thoughts-backend/migration/src/m20250905_000001_init.rs b/thoughts-backend/migration/src/m20250905_000001_init.rs new file mode 100644 index 0000000..1c02790 --- /dev/null +++ b/thoughts-backend/migration/src/m20250905_000001_init.rs @@ -0,0 +1,95 @@ +use super::m20240101_000001_init::User; +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // --- Create Thought Table --- + manager + .create_table( + Table::create() + .table(Thought::Table) + .if_not_exists() + .col(pk_auto(Thought::Id)) + .col(integer(Thought::AuthorId).not_null()) + .foreign_key( + ForeignKey::create() + .name("fk_thought_author_id") + .from(Thought::Table, Thought::AuthorId) + .to(User::Table, User::Id) + .on_update(ForeignKeyAction::NoAction) + .on_delete(ForeignKeyAction::Cascade), + ) + .col(string(Thought::Content).not_null()) + .col( + timestamp_with_time_zone(Thought::CreatedAt) + .not_null() + .default(Expr::current_timestamp()), + ) + .to_owned(), + ) + .await?; + + // --- Create Follow Table --- + manager + .create_table( + Table::create() + .table(Follow::Table) + .if_not_exists() + .col(integer(Follow::FollowerId).not_null()) + .col(integer(Follow::FollowedId).not_null()) + // Composite Primary Key to ensure a user can only follow another once + .primary_key( + Index::create() + .col(Follow::FollowerId) + .col(Follow::FollowedId), + ) + .foreign_key( + ForeignKey::create() + .name("fk_follow_follower_id") + .from(Follow::Table, Follow::FollowerId) + .to(User::Table, User::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_follow_followed_id") + .from(Follow::Table, Follow::FollowedId) + .to(User::Table, User::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Follow::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Thought::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum Thought { + Table, + Id, + AuthorId, + Content, + CreatedAt, +} + +#[derive(DeriveIden)] +enum Follow { + Table, + // The user who is initiating the follow + FollowerId, + // The user who is being followed + FollowedId, +} diff --git a/thoughts-backend/models/Cargo.toml b/thoughts-backend/models/Cargo.toml index 77adc79..1d70383 100644 --- a/thoughts-backend/models/Cargo.toml +++ b/thoughts-backend/models/Cargo.toml @@ -19,3 +19,5 @@ sea-orm = { workspace = true, features = [ ] } validator = { workspace = true, features = ["derive"] } utoipa = { workspace = true } + +common = { path = "../common" } diff --git a/thoughts-backend/models/src/domains/follow.rs b/thoughts-backend/models/src/domains/follow.rs new file mode 100644 index 0000000..c9a324f --- /dev/null +++ b/thoughts-backend/models/src/domains/follow.rs @@ -0,0 +1,32 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "follow")] +pub struct Model { + #[sea_orm(primary_key)] + pub follower_id: i32, + #[sea_orm(primary_key)] + pub followed_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::FollowerId", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Follower, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::FollowedId", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Followed, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/thoughts-backend/models/src/domains/mod.rs b/thoughts-backend/models/src/domains/mod.rs index f5e6370..a034f98 100644 --- a/thoughts-backend/models/src/domains/mod.rs +++ b/thoughts-backend/models/src/domains/mod.rs @@ -2,5 +2,6 @@ pub mod prelude; -pub mod blog; +pub mod follow; +pub mod thought; pub mod user; diff --git a/thoughts-backend/models/src/domains/prelude.rs b/thoughts-backend/models/src/domains/prelude.rs index fea014e..575c276 100644 --- a/thoughts-backend/models/src/domains/prelude.rs +++ b/thoughts-backend/models/src/domains/prelude.rs @@ -1,4 +1,5 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 -pub use super::blog::Entity as Blog; +pub use super::follow::Entity as Follow; +pub use super::thought::Entity as Thought; pub use super::user::Entity as User; diff --git a/thoughts-backend/models/src/domains/blog.rs b/thoughts-backend/models/src/domains/thought.rs similarity index 85% rename from thoughts-backend/models/src/domains/blog.rs rename to thoughts-backend/models/src/domains/thought.rs index 3cae4ae..a05bd6a 100644 --- a/thoughts-backend/models/src/domains/blog.rs +++ b/thoughts-backend/models/src/domains/thought.rs @@ -1,15 +1,13 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 - use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] -#[sea_orm(table_name = "blog")] +#[sea_orm(table_name = "thought")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, pub author_id: i32, - pub title: String, pub content: String, + pub created_at: DateTimeWithTimeZone, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/thoughts-backend/models/src/domains/user.rs b/thoughts-backend/models/src/domains/user.rs index 0d55da3..4c4770e 100644 --- a/thoughts-backend/models/src/domains/user.rs +++ b/thoughts-backend/models/src/domains/user.rs @@ -12,15 +12,6 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::blog::Entity")] - Blog, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Blog.def() - } -} +pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} diff --git a/thoughts-backend/models/src/params/blog.rs b/thoughts-backend/models/src/params/blog.rs deleted file mode 100644 index 55371ea..0000000 --- a/thoughts-backend/models/src/params/blog.rs +++ /dev/null @@ -1,14 +0,0 @@ -use serde::Deserialize; -use utoipa::ToSchema; -use validator::Validate; - -#[derive(Deserialize, Validate, ToSchema)] -pub struct CreateBlogParams { - pub author_id: u32, - - #[validate(length(min = 2))] - pub title: String, - - #[validate(length(min = 2))] - pub content: String, -} diff --git a/thoughts-backend/models/src/params/mod.rs b/thoughts-backend/models/src/params/mod.rs index 1d859a5..ca7dd23 100644 --- a/thoughts-backend/models/src/params/mod.rs +++ b/thoughts-backend/models/src/params/mod.rs @@ -1,2 +1,2 @@ -pub mod blog; +pub mod thought; pub mod user; diff --git a/thoughts-backend/models/src/params/thought.rs b/thoughts-backend/models/src/params/thought.rs new file mode 100644 index 0000000..0e2e6e8 --- /dev/null +++ b/thoughts-backend/models/src/params/thought.rs @@ -0,0 +1,11 @@ +use serde::Deserialize; +use utoipa::ToSchema; +use validator::Validate; + +#[derive(Deserialize, Validate, ToSchema)] +pub struct CreateThoughtParams { + pub author_id: i32, + + #[validate(length(min = 1, max = 128))] + pub content: String, +} diff --git a/thoughts-backend/models/src/queries/blog.rs b/thoughts-backend/models/src/queries/blog.rs deleted file mode 100644 index 6a294e0..0000000 --- a/thoughts-backend/models/src/queries/blog.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::Deserialize; -use utoipa::IntoParams; - -#[derive(Deserialize, Default, IntoParams)] -#[into_params(style = Form, parameter_in = Query)] -pub struct BlogQuery { - #[param(nullable = true)] - pub title: Option, -} diff --git a/thoughts-backend/models/src/queries/mod.rs b/thoughts-backend/models/src/queries/mod.rs index 1d859a5..22d12a3 100644 --- a/thoughts-backend/models/src/queries/mod.rs +++ b/thoughts-backend/models/src/queries/mod.rs @@ -1,2 +1 @@ -pub mod blog; pub mod user; diff --git a/thoughts-backend/models/src/schemas/blog.rs b/thoughts-backend/models/src/schemas/blog.rs deleted file mode 100644 index ca0996d..0000000 --- a/thoughts-backend/models/src/schemas/blog.rs +++ /dev/null @@ -1,36 +0,0 @@ -use serde::Serialize; -use utoipa::ToSchema; - -use crate::domains::blog; - -#[derive(Serialize, ToSchema)] -pub struct BlogSchema { - pub id: u32, - pub title: String, - pub content: String, - pub author_id: u32, -} - -impl From for BlogSchema { - fn from(blog: blog::Model) -> Self { - Self { - id: blog.id as u32, - title: blog.title, - content: blog.content, - author_id: blog.author_id as u32, - } - } -} - -#[derive(Serialize, ToSchema)] -pub struct BlogListSchema { - pub blogs: Vec, -} - -impl From> for BlogListSchema { - fn from(blogs: Vec) -> Self { - Self { - blogs: blogs.into_iter().map(BlogSchema::from).collect(), - } - } -} diff --git a/thoughts-backend/models/src/schemas/mod.rs b/thoughts-backend/models/src/schemas/mod.rs index 1d859a5..ca7dd23 100644 --- a/thoughts-backend/models/src/schemas/mod.rs +++ b/thoughts-backend/models/src/schemas/mod.rs @@ -1,2 +1,2 @@ -pub mod blog; +pub mod thought; pub mod user; diff --git a/thoughts-backend/models/src/schemas/thought.rs b/thoughts-backend/models/src/schemas/thought.rs new file mode 100644 index 0000000..55bd5c7 --- /dev/null +++ b/thoughts-backend/models/src/schemas/thought.rs @@ -0,0 +1,36 @@ +use crate::domains::thought; +use common::DateTimeWithTimeZoneWrapper; +use serde::Serialize; +use utoipa::ToSchema; + +#[derive(Serialize, ToSchema)] +pub struct ThoughtSchema { + pub id: i32, + pub author_id: i32, + pub content: String, + pub created_at: DateTimeWithTimeZoneWrapper, +} + +impl From for ThoughtSchema { + fn from(model: thought::Model) -> Self { + Self { + id: model.id, + author_id: model.author_id, + content: model.content, + created_at: model.created_at.into(), + } + } +} + +#[derive(Serialize, ToSchema)] +pub struct ThoughtListSchema { + pub thoughts: Vec, +} + +impl From> for ThoughtListSchema { + fn from(models: Vec) -> Self { + Self { + thoughts: models.into_iter().map(ThoughtSchema::from).collect(), + } + } +} diff --git a/thoughts-backend/tests/api/blog.rs b/thoughts-backend/tests/api/blog.rs deleted file mode 100644 index 409d949..0000000 --- a/thoughts-backend/tests/api/blog.rs +++ /dev/null @@ -1,30 +0,0 @@ -use axum::{http::StatusCode, Router}; -use http_body_util::BodyExt; -use serde_json::Value; - -use utils::testing::{make_get_request, make_post_request}; - -pub(super) async fn test_post_blogs(app: Router) { - let response = make_post_request( - app, - "/blogs", - r#"{"author_id": 1, "title": "title", "content": "test"}"#.to_owned(), - ) - .await; - - assert_eq!(response.status(), StatusCode::CREATED); -} - -pub(super) async fn test_get_blogs(app: Router) { - let response = make_get_request(app, "/blogs").await; - assert_eq!(response.status(), StatusCode::OK); - - let body = response.into_body().collect().await.unwrap().to_bytes(); - let result: Value = serde_json::from_slice(&body).unwrap(); - assert_eq!(result["blogs"].as_array().unwrap().len(), 1); - - let blog = &result["blogs"][0]; - assert_eq!(blog["author_id"], 1); - assert_eq!(blog["title"], "title"); - assert_eq!(blog["content"], "test"); -} diff --git a/thoughts-backend/tests/api/mod.rs b/thoughts-backend/tests/api/mod.rs index 19eae54..0da72bd 100644 --- a/thoughts-backend/tests/api/mod.rs +++ b/thoughts-backend/tests/api/mod.rs @@ -1,11 +1,9 @@ use api::setup_router; use utils::testing::setup_test_db; -mod blog; mod root; mod user; -use blog::*; use root::*; use user::*; @@ -30,15 +28,3 @@ async fn user_main() { test_post_users_error(app.clone()).await; test_get_users(app).await; } - -#[tokio::test] -async fn blog_main() { - let db = setup_test_db("sqlite::blog?mode=memory&cache=shared") - .await - .expect("Set up db failed!"); - - let app = setup_router(db); - test_post_users(app.clone()).await; - test_post_blogs(app.clone()).await; - test_get_blogs(app).await; -} diff --git a/thoughts-backend/tests/app/persistence/blog.rs b/thoughts-backend/tests/app/persistence/blog.rs deleted file mode 100644 index 1829da8..0000000 --- a/thoughts-backend/tests/app/persistence/blog.rs +++ /dev/null @@ -1,22 +0,0 @@ -use sea_orm::{DatabaseConnection, Unchanged}; - -use app::persistence::blog::create_blog; -use models::domains::blog; -use models::params::blog::CreateBlogParams; - -pub(super) async fn test_blog(db: &DatabaseConnection) { - let params = CreateBlogParams { - author_id: 1, - title: "title".to_string(), - content: "test".to_string(), - }; - - let blog = create_blog(db, params).await.expect("Create blog failed!"); - let expected = blog::ActiveModel { - id: Unchanged(1), - author_id: Unchanged(1), - title: Unchanged("title".to_owned()), - content: Unchanged("test".to_owned()), - }; - assert_eq!(blog, expected); -} diff --git a/thoughts-backend/tests/app/persistence/mod.rs b/thoughts-backend/tests/app/persistence/mod.rs index 0ee3a93..50f54a6 100644 --- a/thoughts-backend/tests/app/persistence/mod.rs +++ b/thoughts-backend/tests/app/persistence/mod.rs @@ -1,9 +1,7 @@ use utils::testing::setup_test_db; -mod blog; mod user; -use blog::test_blog; use user::test_user; #[tokio::test] @@ -14,13 +12,3 @@ async fn user_main() { test_user(&db).await; } - -#[tokio::test] -async fn blog_main() { - let db = setup_test_db("sqlite::memory:") - .await - .expect("Set up db failed!"); - - test_user(&db).await; - test_blog(&db).await; -}