Skip to content

Commit

Permalink
Add blog API (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
kigawas authored Aug 18, 2024
1 parent 6ec02fd commit 69e12b9
Show file tree
Hide file tree
Showing 29 changed files with 510 additions and 123 deletions.
248 changes: 136 additions & 112 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ members = ["api", "app", "doc", "models", "migration", "utils"]

[workspace.dependencies]
axum = { version = "0.7.5", default-features = false }
tower = { version = "0.4.13", default-features = false }
tower = { version = "0.5.0", default-features = false }
sea-orm = { version = "1.0.0", default-features = false }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", default-features = false }
Expand Down
67 changes: 67 additions & 0 deletions api/src/routers/blog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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};

#[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: Option<Query<BlogQuery>>,
) -> Result<impl IntoResponse, ApiError> {
let Query(query) = query.unwrap_or_default();

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))
}
3 changes: 3 additions & 0 deletions api/src/routers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
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;

pub fn create_router(state: AppState) -> Router {
Router::new()
.nest("/users", create_user_router())
.nest("/blogs", create_blog_router())
.nest("/", create_root_router())
.with_state(state)
}
1 change: 1 addition & 0 deletions api/src/routers/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ async fn users_get(
.map_err(ApiError::from)?;
Ok(Json(UserListSchema::from(users)))
}

#[utoipa::path(
get,
path = "/{id}",
Expand Down
24 changes: 24 additions & 0 deletions app/src/persistence/blog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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))
.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
}
1 change: 1 addition & 0 deletions app/src/persistence/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod blog;
pub mod user;
20 changes: 20 additions & 0 deletions doc/src/blog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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;
5 changes: 4 additions & 1 deletion doc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use utoipa::OpenApi;
use utoipa_scalar::{Scalar, Servable as ScalarServable};
use utoipa_swagger_ui::SwaggerUi;

mod blog;
mod root;
mod user;

Expand All @@ -11,10 +12,12 @@ 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 = "user", description = "User API"),
(name = "blog", description = "Blog API"),
)
)]
Expand Down
6 changes: 5 additions & 1 deletion migration/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
pub use sea_orm_migration::prelude::*;

mod m20240101_000001_init;
mod m20240816_160144_blog;

pub struct Migrator;

#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20240101_000001_init::Migration)]
vec![
Box::new(m20240101_000001_init::Migration),
Box::new(m20240816_160144_blog::Migration),
]
}
}
2 changes: 1 addition & 1 deletion migration/src/m20240101_000001_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl MigrationTrait for Migration {
}

#[derive(DeriveIden)]
enum User {
pub(super) enum User {
Table,
Id,
Username,
Expand Down
47 changes: 47 additions & 0 deletions migration/src/m20240816_160144_blog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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,
}
4 changes: 4 additions & 0 deletions models/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ Only dependencies for modelling are allowed:
- SeaORM (domain models and database)
- validator (parameter validation)
- utoipa (openapi)

## SeaORM

Write migration files first, then generate models.
33 changes: 33 additions & 0 deletions models/src/domains/blog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! `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")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub author_id: i32,
pub title: String,
pub content: String,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::AuthorId",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
User,
}

impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}

impl ActiveModelBehavior for ActiveModel {}
3 changes: 2 additions & 1 deletion models/src/domains/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0

pub mod prelude;

pub mod blog;
pub mod user;
3 changes: 2 additions & 1 deletion models/src/domains/prelude.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0

pub use super::blog::Entity as Blog;
pub use super::user::Entity as User;
15 changes: 12 additions & 3 deletions models/src/domains/user.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0

use sea_orm::entity::prelude::*;

Expand All @@ -7,11 +7,20 @@ use sea_orm::entity::prelude::*;
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(column_type = "Text", unique)]
#[sea_orm(unique)]
pub username: String,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
pub enum Relation {
#[sea_orm(has_many = "super::blog::Entity")]
Blog,
}

impl Related<super::blog::Entity> for Entity {
fn to() -> RelationDef {
Relation::Blog.def()
}
}

impl ActiveModelBehavior for ActiveModel {}
14 changes: 14 additions & 0 deletions models/src/params/blog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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,
}
1 change: 1 addition & 0 deletions models/src/params/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod blog;
pub mod user;
9 changes: 9 additions & 0 deletions models/src/queries/blog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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: String,
}
1 change: 1 addition & 0 deletions models/src/queries/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod blog;
pub mod user;
Loading

0 comments on commit 69e12b9

Please sign in to comment.