Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions seaorm-postgres/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "sea-orm-axum-example"
version = "0.1.0"
authors = ["Yoshiera Huang <huangjasper@126.com>"]
edition = "2021"
publish = false

[workspace]
members = [".", "api", "service", "entity", "migration"]

[dependencies]
axum-example-api = { path = "api" }
18 changes: 18 additions & 0 deletions seaorm-postgres/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
![screenshot](Screenshot.png)

# Axum with SeaORM example app

1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database

1. Turn on the appropriate database feature for your chosen db in `service/Cargo.toml` (the `"sqlx-postgres",` line)

1. Execute `cargo run` to start the server

1. Visit [localhost:8000](http://localhost:8000) in browser

Run mock test on the service logic crate:

```bash
cd service
cargo test --features mock
```
Binary file added seaorm-postgres/Screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions seaorm-postgres/api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "axum-example-api"
version = "0.1.0"
authors = ["Yoshiera Huang <huangjasper@126.com>"]
edition = "2021"
publish = false

[dependencies]
axum-example-service = { path = "../service" }
tokio = { version = "1.34.0", features = ["full"] }
axum = "0.7.1"
tower = "0.4.13"
tower-http = { version = "0.5.0", features = ["fs"] }
tower-cookies = "0.10.0"
anyhow = "1.0.75"
dotenvy = "0.15.7"
serde = "1.0.193"
serde_json = "1.0.108"
tera = "1.19.1"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
entity = { path = "../entity" }
migration = { path = "../migration" }
51 changes: 51 additions & 0 deletions seaorm-postgres/api/src/flash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tower_cookies::{Cookie, Cookies};

#[derive(Deserialize)]
struct ValuedMessage<T> {
#[serde(rename = "_")]
value: T,
}

#[derive(Serialize)]
struct ValuedMessageRef<'a, T> {
#[serde(rename = "_")]
value: &'a T,
}

const FLASH_COOKIE_NAME: &str = "_flash";

pub fn get_flash_cookie<T>(cookies: &Cookies) -> Option<T>
where
T: DeserializeOwned,
{
cookies.get(FLASH_COOKIE_NAME).and_then(|flash_cookie| {
if let Ok(ValuedMessage::<T> { value }) = serde_json::from_str(flash_cookie.value()) {
Some(value)
} else {
None
}
})
}

pub type PostResponse = (StatusCode, HeaderMap);

pub fn post_response<T>(cookies: &mut Cookies, data: T) -> PostResponse
where
T: Serialize,
{
let valued_message_ref = ValuedMessageRef { value: &data };

let mut cookie = Cookie::new(
FLASH_COOKIE_NAME,
serde_json::to_string(&valued_message_ref).unwrap(),
);
cookie.set_path("/");
cookies.add(cookie);

let mut header = HeaderMap::new();
header.insert(header::LOCATION, HeaderValue::from_static("/"));

(StatusCode::SEE_OTHER, header)
}
211 changes: 211 additions & 0 deletions seaorm-postgres/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
mod flash;

use axum::{
extract::{Form, Path, Query, State},
http::StatusCode,
response::Html,
routing::{get, get_service, post},
Router,
};
use axum_example_service::{
sea_orm::{Database, DatabaseConnection},
Mutation as MutationCore, Query as QueryCore,
};
use entity::post;
use flash::{get_flash_cookie, post_response, PostResponse};
use migration::{Migrator, MigratorTrait};
use serde::{Deserialize, Serialize};
use std::env;
use tera::Tera;
use tower_cookies::{CookieManagerLayer, Cookies};
use tower_http::services::ServeDir;

#[tokio::main]
async fn start() -> anyhow::Result<()> {
env::set_var("RUST_LOG", "debug");
tracing_subscriber::fmt::init();

dotenvy::dotenv().ok();
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file");
let host = env::var("HOST").expect("HOST is not set in .env file");
let port = env::var("PORT").expect("PORT is not set in .env file");
let server_url = format!("{host}:{port}");

let conn = Database::connect(db_url)
.await
.expect("Database connection failed");
Migrator::up(&conn, None).await.unwrap();

let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*"))
.expect("Tera initialization failed");

let state = AppState { templates, conn };

let app = Router::new()
.route("/", get(list_posts).post(create_post))
.route("/:id", get(edit_post).post(update_post))
.route("/new", get(new_post))
.route("/delete/:id", post(delete_post))
.nest_service(
"/static",
get_service(ServeDir::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/static"
)))
.handle_error(|error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {error}"),
)
}),
)
.layer(CookieManagerLayer::new())
.with_state(state);

let listener = tokio::net::TcpListener::bind(&server_url).await.unwrap();
axum::serve(listener, app).await?;

Ok(())
}

#[derive(Clone)]
struct AppState {
templates: Tera,
conn: DatabaseConnection,
}

#[derive(Deserialize)]
struct Params {
page: Option<u64>,
posts_per_page: Option<u64>,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
struct FlashData {
kind: String,
message: String,
}

async fn list_posts(
state: State<AppState>,
Query(params): Query<Params>,
cookies: Cookies,
) -> Result<Html<String>, (StatusCode, &'static str)> {
let page = params.page.unwrap_or(1);
let posts_per_page = params.posts_per_page.unwrap_or(5);

let (posts, num_pages) = QueryCore::find_posts_in_page(&state.conn, page, posts_per_page)
.await
.expect("Cannot find posts in page");

let mut ctx = tera::Context::new();
ctx.insert("posts", &posts);
ctx.insert("page", &page);
ctx.insert("posts_per_page", &posts_per_page);
ctx.insert("num_pages", &num_pages);

if let Some(value) = get_flash_cookie::<FlashData>(&cookies) {
ctx.insert("flash", &value);
}

let body = state
.templates
.render("index.html.tera", &ctx)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;

Ok(Html(body))
}

async fn new_post(state: State<AppState>) -> Result<Html<String>, (StatusCode, &'static str)> {
let ctx = tera::Context::new();
let body = state
.templates
.render("new.html.tera", &ctx)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;

Ok(Html(body))
}

async fn create_post(
state: State<AppState>,
mut cookies: Cookies,
form: Form<post::Model>,
) -> Result<PostResponse, (StatusCode, &'static str)> {
let form = form.0;

MutationCore::create_post(&state.conn, form)
.await
.expect("could not insert post");

let data = FlashData {
kind: "success".to_owned(),
message: "Post successfully added".to_owned(),
};

Ok(post_response(&mut cookies, data))
}

async fn edit_post(
state: State<AppState>,
Path(id): Path<i32>,
) -> Result<Html<String>, (StatusCode, &'static str)> {
let post: post::Model = QueryCore::find_post_by_id(&state.conn, id)
.await
.expect("could not find post")
.unwrap_or_else(|| panic!("could not find post with id {id}"));

let mut ctx = tera::Context::new();
ctx.insert("post", &post);

let body = state
.templates
.render("edit.html.tera", &ctx)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;

Ok(Html(body))
}

async fn update_post(
state: State<AppState>,
Path(id): Path<i32>,
mut cookies: Cookies,
form: Form<post::Model>,
) -> Result<PostResponse, (StatusCode, String)> {
let form = form.0;

MutationCore::update_post_by_id(&state.conn, id, form)
.await
.expect("could not edit post");

let data = FlashData {
kind: "success".to_owned(),
message: "Post successfully updated".to_owned(),
};

Ok(post_response(&mut cookies, data))
}

async fn delete_post(
state: State<AppState>,
Path(id): Path<i32>,
mut cookies: Cookies,
) -> Result<PostResponse, (StatusCode, &'static str)> {
MutationCore::delete_post(&state.conn, id)
.await
.expect("could not delete post");

let data = FlashData {
kind: "success".to_owned(),
message: "Post successfully deleted".to_owned(),
};

Ok(post_response(&mut cookies, data))
}

pub fn main() {
let result = start();

if let Some(err) = result.err() {
println!("Error: {err}");
}
}
Loading