Refactor blog module and remove blog-related functionality

- Removed blog router and associated API endpoints.
- Deleted blog persistence functions and related query parameters.
- Removed blog schemas and models from the codebase.
- Introduced common crate for shared types, including DateTimeWithTimeZoneWrapper.
- Added Thought and Follow models with corresponding migrations.
- Updated dependencies in Cargo.toml files to reflect changes.
- Adjusted tests to remove references to the blog module.
This commit is contained in:
2025-09-05 18:10:58 +02:00
parent e5747eaaf3
commit 912259ef54
32 changed files with 543 additions and 333 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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" }

View File

@@ -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<AppState>,
Valid(Json(params)): Valid<Json<CreateBlogParams>>,
) -> Result<impl IntoResponse, ApiError> {
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<AppState>,
query: Query<BlogQuery>,
) -> Result<impl IntoResponse, ApiError> {
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<AppState> {
Router::new().route("/", get(blogs_get).post(blogs_post))
}

View File

@@ -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)
}

View File

@@ -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<Vec<blog::Model>, 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, DbErr> {
blog::ActiveModel {
author_id: Set(params.author_id as i32),
title: Set(params.title),
content: Set(params.content),
..Default::default()
}
.save(db)
.await
}

View File

@@ -1,2 +1 @@
pub mod blog;
pub mod user;

View File

@@ -0,0 +1,9 @@
[package]
name = "common"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { workspace = true }
utoipa = { workspace = true }
sea-orm = { workspace = true }

View File

@@ -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<DateTimeWithTimeZone> for DateTimeWithTimeZoneWrapper {
fn from(value: DateTimeWithTimeZone) -> Self {
DateTimeWithTimeZoneWrapper(value.to_rfc3339())
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<Box<dyn MigrationTrait>> {
vec![
Box::new(m20240101_000001_init::Migration),
Box::new(m20240816_160144_blog::Migration),
Box::new(m20250905_000001_init::Migration),
]
}
}

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -19,3 +19,5 @@ sea-orm = { workspace = true, features = [
] }
validator = { workspace = true, features = ["derive"] }
utoipa = { workspace = true }
common = { path = "../common" }

View File

@@ -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 {}

View File

@@ -2,5 +2,6 @@
pub mod prelude;
pub mod blog;
pub mod follow;
pub mod thought;
pub mod user;

View File

@@ -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;

View File

@@ -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)]

View File

@@ -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<super::blog::Entity> for Entity {
fn to() -> RelationDef {
Relation::Blog.def()
}
}
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -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,
}

View File

@@ -1,2 +1,2 @@
pub mod blog;
pub mod thought;
pub mod user;

View File

@@ -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,
}

View File

@@ -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<String>,
}

View File

@@ -1,2 +1 @@
pub mod blog;
pub mod user;

View File

@@ -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<blog::Model> 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<BlogSchema>,
}
impl From<Vec<blog::Model>> for BlogListSchema {
fn from(blogs: Vec<blog::Model>) -> Self {
Self {
blogs: blogs.into_iter().map(BlogSchema::from).collect(),
}
}
}

View File

@@ -1,2 +1,2 @@
pub mod blog;
pub mod thought;
pub mod user;

View File

@@ -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<thought::Model> 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<ThoughtSchema>,
}
impl From<Vec<thought::Model>> for ThoughtListSchema {
fn from(models: Vec<thought::Model>) -> Self {
Self {
thoughts: models.into_iter().map(ThoughtSchema::from).collect(),
}
}
}

View File

@@ -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");
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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;
}