Demonstrate how to mock test SeaORM by separating core implementation from the web API (#890)
* Move core implementations to a standalone crate * Set up integration test skeleton in `core` * Demonstrate mock testing with query * Move Rocket api code to a standalone crate * Add mock execution * Add MyDataMyConsent in COMMUNITY.md (#889) * Add MyDataMyConsent in COMMUNITY.md * Update MyDataMyConsent description in COMMUNITY.md * Update COMMUNITY.md Chronological order * [cli] bump sea-schema to 0.9.3 (SeaQL/sea-orm#876) * Update CHNAGELOG PR links * 0.9.1 CHANGELOG * Auto discover and run all issues & examples CI (#903) * Auto discover and run all [issues] CI * Auto discover and run all examples CI * Fixup * Testing * Test [issues] * Compile prepare_mock_db() conditionally based on "mock" feature * Update workflow job to run mock test if `core` folder exists * Update Actix3 example * Fix merge conflict human error * Update usize used in paginate to u64 (PR#789) * Update sea-orm version in the Rocket example to 0.10.0 * Fix GitHub workflow to run mock test for core crates * Increase the robustness of core crate check by verifying that the `core` folder is a crate * Update Actix(4) example * Update Axum example * Update GraphQL example * Update Jsonrpsee example * Update Poem example * Update Tonic example * Cargo fmt * Update Salvo example * Update path of core/Cargo.toml in README.md * Add mock test instruction in README.md * Refactoring * Fix Rocket examples Co-authored-by: Amit Goyani <63532626+itsAmitGoyani@users.noreply.github.com> Co-authored-by: Billy Chan <ccw.billy.123@gmail.com>
This commit is contained in:
parent
f3fb355647
commit
5143307b3e
9
.github/workflows/rust.yml
vendored
9
.github/workflows/rust.yml
vendored
@ -405,6 +405,15 @@ jobs:
|
||||
args: >
|
||||
--manifest-path ${{ matrix.path }}
|
||||
|
||||
- name: Run mock test if it is core crate
|
||||
uses: actions-rs/cargo@v1
|
||||
if: ${{ contains(matrix.path, 'core/Cargo.toml') }}
|
||||
with:
|
||||
command: test
|
||||
args: >
|
||||
--manifest-path ${{ matrix.path }}
|
||||
--features mock
|
||||
|
||||
- name: check rustfmt
|
||||
run: |
|
||||
rustup override set nightly
|
||||
|
@ -6,30 +6,7 @@ edition = "2021"
|
||||
publish = false
|
||||
|
||||
[workspace]
|
||||
members = [".", "entity", "migration"]
|
||||
members = [".", "api", "core", "entity", "migration"]
|
||||
|
||||
[dependencies]
|
||||
actix-http = "2"
|
||||
actix-web = "3"
|
||||
actix-flash = "0.2"
|
||||
actix-files = "0.5"
|
||||
futures = { version = "^0.3" }
|
||||
futures-util = { version = "^0.3" }
|
||||
tera = "1.8.0"
|
||||
dotenv = "0.15"
|
||||
listenfd = "0.3.3"
|
||||
serde = "1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
entity = { path = "entity" }
|
||||
migration = { path = "migration" }
|
||||
|
||||
[dependencies.sea-orm]
|
||||
path = "../../" # remove this line in your own project
|
||||
version = "^0.10.0" # sea-orm version
|
||||
features = [
|
||||
"debug-print",
|
||||
"runtime-async-std-native-tls",
|
||||
"sqlx-mysql",
|
||||
# "sqlx-postgres",
|
||||
# "sqlx-sqlite",
|
||||
]
|
||||
actix3-example-api = { path = "api" }
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
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 `Cargo.toml` (the `"sqlx-mysql",` line)
|
||||
1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-mysql",` line)
|
||||
|
||||
1. Execute `cargo run` to start the server
|
||||
|
||||
@ -18,3 +18,10 @@ Run server with auto-reloading:
|
||||
cargo install systemfd
|
||||
systemfd --no-pid -s http::8000 -- cargo watch -x run
|
||||
```
|
||||
|
||||
Run mock test on the core logic crate:
|
||||
|
||||
```bash
|
||||
cd core
|
||||
cargo test --features mock
|
||||
```
|
||||
|
22
examples/actix3_example/api/Cargo.toml
Normal file
22
examples/actix3_example/api/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "actix3-example-api"
|
||||
version = "0.1.0"
|
||||
authors = ["Sam Samai <sam@studio2pi.com.au>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
actix3-example-core = { path = "../core" }
|
||||
actix-http = "2"
|
||||
actix-web = "3"
|
||||
actix-flash = "0.2"
|
||||
actix-files = "0.5"
|
||||
futures = { version = "^0.3" }
|
||||
futures-util = { version = "^0.3" }
|
||||
tera = "1.8.0"
|
||||
dotenv = "0.15"
|
||||
listenfd = "0.3.3"
|
||||
serde = "1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
entity = { path = "../entity" }
|
||||
migration = { path = "../migration" }
|
219
examples/actix3_example/api/src/lib.rs
Normal file
219
examples/actix3_example/api/src/lib.rs
Normal file
@ -0,0 +1,219 @@
|
||||
use actix3_example_core::{
|
||||
sea_orm::{Database, DatabaseConnection},
|
||||
Mutation, Query,
|
||||
};
|
||||
use actix_files as fs;
|
||||
use actix_web::{
|
||||
error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result,
|
||||
};
|
||||
|
||||
use entity::post;
|
||||
use listenfd::ListenFd;
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use tera::Tera;
|
||||
|
||||
const DEFAULT_POSTS_PER_PAGE: u64 = 5;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AppState {
|
||||
templates: tera::Tera,
|
||||
conn: DatabaseConnection,
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Params {
|
||||
page: Option<u64>,
|
||||
posts_per_page: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
struct FlashData {
|
||||
kind: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn list(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppState>,
|
||||
opt_flash: Option<actix_flash::Message<FlashData>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let template = &data.templates;
|
||||
let conn = &data.conn;
|
||||
|
||||
// get params
|
||||
let params = web::Query::<Params>::from_query(req.query_string()).unwrap();
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE);
|
||||
|
||||
let (posts, num_pages) = Query::find_posts_in_page(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(flash) = opt_flash {
|
||||
let flash_inner = flash.into_inner();
|
||||
ctx.insert("flash", &flash_inner);
|
||||
}
|
||||
|
||||
let body = template
|
||||
.render("index.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[get("/new")]
|
||||
async fn new(data: web::Data<AppState>) -> Result<HttpResponse, Error> {
|
||||
let template = &data.templates;
|
||||
let ctx = tera::Context::new();
|
||||
let body = template
|
||||
.render("new.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[post("/")]
|
||||
async fn create(
|
||||
data: web::Data<AppState>,
|
||||
post_form: web::Form<post::Model>,
|
||||
) -> actix_flash::Response<HttpResponse, FlashData> {
|
||||
let conn = &data.conn;
|
||||
|
||||
let form = post_form.into_inner();
|
||||
|
||||
Mutation::create_post(conn, form)
|
||||
.await
|
||||
.expect("could not insert post");
|
||||
|
||||
let flash = FlashData {
|
||||
kind: "success".to_owned(),
|
||||
message: "Post successfully added.".to_owned(),
|
||||
};
|
||||
|
||||
actix_flash::Response::with_redirect(flash, "/")
|
||||
}
|
||||
|
||||
#[get("/{id}")]
|
||||
async fn edit(data: web::Data<AppState>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
|
||||
let conn = &data.conn;
|
||||
let template = &data.templates;
|
||||
let id = id.into_inner();
|
||||
|
||||
let post: post::Model = Query::find_post_by_id(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 = template
|
||||
.render("edit.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[post("/{id}")]
|
||||
async fn update(
|
||||
data: web::Data<AppState>,
|
||||
id: web::Path<i32>,
|
||||
post_form: web::Form<post::Model>,
|
||||
) -> actix_flash::Response<HttpResponse, FlashData> {
|
||||
let conn = &data.conn;
|
||||
let form = post_form.into_inner();
|
||||
let id = id.into_inner();
|
||||
|
||||
Mutation::update_post_by_id(conn, id, form)
|
||||
.await
|
||||
.expect("could not edit post");
|
||||
|
||||
let flash = FlashData {
|
||||
kind: "success".to_owned(),
|
||||
message: "Post successfully updated.".to_owned(),
|
||||
};
|
||||
|
||||
actix_flash::Response::with_redirect(flash, "/")
|
||||
}
|
||||
|
||||
#[post("/delete/{id}")]
|
||||
async fn delete(
|
||||
data: web::Data<AppState>,
|
||||
id: web::Path<i32>,
|
||||
) -> actix_flash::Response<HttpResponse, FlashData> {
|
||||
let conn = &data.conn;
|
||||
let id = id.into_inner();
|
||||
|
||||
Mutation::delete_post(conn, id)
|
||||
.await
|
||||
.expect("could not delete post");
|
||||
|
||||
let flash = FlashData {
|
||||
kind: "success".to_owned(),
|
||||
message: "Post successfully deleted.".to_owned(),
|
||||
};
|
||||
|
||||
actix_flash::Response::with_redirect(flash, "/")
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn start() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "debug");
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// get env vars
|
||||
dotenv::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);
|
||||
|
||||
// create post table if not exists
|
||||
let conn = Database::connect(&db_url).await.unwrap();
|
||||
Migrator::up(&conn, None).await.unwrap();
|
||||
let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();
|
||||
let state = AppState { templates, conn };
|
||||
|
||||
let mut listenfd = ListenFd::from_env();
|
||||
let mut server = HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(state.clone())
|
||||
.wrap(middleware::Logger::default()) // enable logger
|
||||
.wrap(actix_flash::Flash::default())
|
||||
.configure(init)
|
||||
.service(fs::Files::new("/static", "./api/static").show_files_listing())
|
||||
});
|
||||
|
||||
server = match listenfd.take_tcp_listener(0)? {
|
||||
Some(listener) => server.listen(listener)?,
|
||||
None => server.bind(&server_url)?,
|
||||
};
|
||||
|
||||
println!("Starting server at {}", server_url);
|
||||
server.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(list);
|
||||
cfg.service(new);
|
||||
cfg.service(create);
|
||||
cfg.service(edit);
|
||||
cfg.service(update);
|
||||
cfg.service(delete);
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
let result = start();
|
||||
|
||||
if let Some(err) = result.err() {
|
||||
println!("Error: {}", err)
|
||||
}
|
||||
}
|
30
examples/actix3_example/core/Cargo.toml
Normal file
30
examples/actix3_example/core/Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "actix3-example-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
entity = { path = "../entity" }
|
||||
|
||||
[dependencies.sea-orm]
|
||||
path = "../../../" # remove this line in your own project
|
||||
version = "^0.10.0" # sea-orm version
|
||||
features = [
|
||||
"debug-print",
|
||||
"runtime-async-std-native-tls",
|
||||
"sqlx-mysql",
|
||||
# "sqlx-postgres",
|
||||
# "sqlx-sqlite",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.20.0", features = ["macros", "rt"] }
|
||||
|
||||
[features]
|
||||
mock = ["sea-orm/mock"]
|
||||
|
||||
[[test]]
|
||||
name = "mock"
|
||||
required-features = ["mock"]
|
7
examples/actix3_example/core/src/lib.rs
Normal file
7
examples/actix3_example/core/src/lib.rs
Normal file
@ -0,0 +1,7 @@
|
||||
mod mutation;
|
||||
mod query;
|
||||
|
||||
pub use mutation::*;
|
||||
pub use query::*;
|
||||
|
||||
pub use sea_orm;
|
53
examples/actix3_example/core/src/mutation.rs
Normal file
53
examples/actix3_example/core/src/mutation.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use ::entity::{post, post::Entity as Post};
|
||||
use sea_orm::*;
|
||||
|
||||
pub struct Mutation;
|
||||
|
||||
impl Mutation {
|
||||
pub async fn create_post(
|
||||
db: &DbConn,
|
||||
form_data: post::Model,
|
||||
) -> Result<post::ActiveModel, DbErr> {
|
||||
post::ActiveModel {
|
||||
title: Set(form_data.title.to_owned()),
|
||||
text: Set(form_data.text.to_owned()),
|
||||
..Default::default()
|
||||
}
|
||||
.save(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_post_by_id(
|
||||
db: &DbConn,
|
||||
id: i32,
|
||||
form_data: post::Model,
|
||||
) -> Result<post::Model, DbErr> {
|
||||
let post: post::ActiveModel = Post::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or(DbErr::Custom("Cannot find post.".to_owned()))
|
||||
.map(Into::into)?;
|
||||
|
||||
post::ActiveModel {
|
||||
id: post.id,
|
||||
title: Set(form_data.title.to_owned()),
|
||||
text: Set(form_data.text.to_owned()),
|
||||
}
|
||||
.update(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_post(db: &DbConn, id: i32) -> Result<DeleteResult, DbErr> {
|
||||
let post: post::ActiveModel = Post::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or(DbErr::Custom("Cannot find post.".to_owned()))
|
||||
.map(Into::into)?;
|
||||
|
||||
post.delete(db).await
|
||||
}
|
||||
|
||||
pub async fn delete_all_posts(db: &DbConn) -> Result<DeleteResult, DbErr> {
|
||||
Post::delete_many().exec(db).await
|
||||
}
|
||||
}
|
26
examples/actix3_example/core/src/query.rs
Normal file
26
examples/actix3_example/core/src/query.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use ::entity::{post, post::Entity as Post};
|
||||
use sea_orm::*;
|
||||
|
||||
pub struct Query;
|
||||
|
||||
impl Query {
|
||||
pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result<Option<post::Model>, DbErr> {
|
||||
Post::find_by_id(id).one(db).await
|
||||
}
|
||||
|
||||
/// If ok, returns (post models, num pages).
|
||||
pub async fn find_posts_in_page(
|
||||
db: &DbConn,
|
||||
page: u64,
|
||||
posts_per_page: u64,
|
||||
) -> Result<(Vec<post::Model>, u64), DbErr> {
|
||||
// Setup paginator
|
||||
let paginator = Post::find()
|
||||
.order_by_asc(post::Column::Id)
|
||||
.paginate(db, posts_per_page);
|
||||
let num_pages = paginator.num_pages().await?;
|
||||
|
||||
// Fetch paginated posts
|
||||
paginator.fetch_page(page - 1).await.map(|p| (p, num_pages))
|
||||
}
|
||||
}
|
79
examples/actix3_example/core/tests/mock.rs
Normal file
79
examples/actix3_example/core/tests/mock.rs
Normal file
@ -0,0 +1,79 @@
|
||||
mod prepare;
|
||||
|
||||
use actix3_example_core::{Mutation, Query};
|
||||
use entity::post;
|
||||
use prepare::prepare_mock_db;
|
||||
|
||||
#[tokio::test]
|
||||
async fn main() {
|
||||
let db = &prepare_mock_db();
|
||||
|
||||
{
|
||||
let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(post.id, 1);
|
||||
}
|
||||
|
||||
{
|
||||
let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(post.id, 5);
|
||||
}
|
||||
|
||||
{
|
||||
let post = Mutation::create_post(
|
||||
db,
|
||||
post::Model {
|
||||
id: 0,
|
||||
title: "Title D".to_owned(),
|
||||
text: "Text D".to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
post,
|
||||
post::ActiveModel {
|
||||
id: sea_orm::ActiveValue::Unchanged(6),
|
||||
title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()),
|
||||
text: sea_orm::ActiveValue::Unchanged("Text D".to_owned())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let post = Mutation::update_post_by_id(
|
||||
db,
|
||||
1,
|
||||
post::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
post,
|
||||
post::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let result = Mutation::delete_post(db, 5).await.unwrap();
|
||||
|
||||
assert_eq!(result.rows_affected, 1);
|
||||
}
|
||||
|
||||
{
|
||||
let result = Mutation::delete_all_posts(db).await.unwrap();
|
||||
|
||||
assert_eq!(result.rows_affected, 5);
|
||||
}
|
||||
}
|
50
examples/actix3_example/core/tests/prepare.rs
Normal file
50
examples/actix3_example/core/tests/prepare.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use ::entity::post;
|
||||
use sea_orm::*;
|
||||
|
||||
#[cfg(feature = "mock")]
|
||||
pub fn prepare_mock_db() -> DatabaseConnection {
|
||||
MockDatabase::new(DatabaseBackend::Postgres)
|
||||
.append_query_results(vec![
|
||||
vec![post::Model {
|
||||
id: 1,
|
||||
title: "Title A".to_owned(),
|
||||
text: "Text A".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 5,
|
||||
title: "Title C".to_owned(),
|
||||
text: "Text C".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 6,
|
||||
title: "Title D".to_owned(),
|
||||
text: "Text D".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 1,
|
||||
title: "Title A".to_owned(),
|
||||
text: "Text A".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 5,
|
||||
title: "Title C".to_owned(),
|
||||
text: "Text C".to_owned(),
|
||||
}],
|
||||
])
|
||||
.append_exec_results(vec![
|
||||
MockExecResult {
|
||||
last_insert_id: 6,
|
||||
rows_affected: 1,
|
||||
},
|
||||
MockExecResult {
|
||||
last_insert_id: 6,
|
||||
rows_affected: 5,
|
||||
},
|
||||
])
|
||||
.into_connection()
|
||||
}
|
@ -1,227 +1,3 @@
|
||||
use actix_files as fs;
|
||||
use actix_web::{
|
||||
error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result,
|
||||
};
|
||||
|
||||
use entity::post;
|
||||
use entity::post::Entity as Post;
|
||||
use listenfd::ListenFd;
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use sea_orm::{entity::*, query::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use tera::Tera;
|
||||
|
||||
const DEFAULT_POSTS_PER_PAGE: u64 = 5;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AppState {
|
||||
templates: tera::Tera,
|
||||
conn: DatabaseConnection,
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Params {
|
||||
page: Option<u64>,
|
||||
posts_per_page: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
struct FlashData {
|
||||
kind: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn list(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppState>,
|
||||
opt_flash: Option<actix_flash::Message<FlashData>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let template = &data.templates;
|
||||
let conn = &data.conn;
|
||||
|
||||
// get params
|
||||
let params = web::Query::<Params>::from_query(req.query_string()).unwrap();
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE);
|
||||
let paginator = Post::find()
|
||||
.order_by_asc(post::Column::Id)
|
||||
.paginate(conn, posts_per_page);
|
||||
let num_pages = paginator.num_pages().await.ok().unwrap();
|
||||
|
||||
let posts = paginator
|
||||
.fetch_page(page - 1)
|
||||
.await
|
||||
.expect("could not retrieve posts");
|
||||
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(flash) = opt_flash {
|
||||
let flash_inner = flash.into_inner();
|
||||
ctx.insert("flash", &flash_inner);
|
||||
}
|
||||
|
||||
let body = template
|
||||
.render("index.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[get("/new")]
|
||||
async fn new(data: web::Data<AppState>) -> Result<HttpResponse, Error> {
|
||||
let template = &data.templates;
|
||||
let ctx = tera::Context::new();
|
||||
let body = template
|
||||
.render("new.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[post("/")]
|
||||
async fn create(
|
||||
data: web::Data<AppState>,
|
||||
post_form: web::Form<post::Model>,
|
||||
) -> actix_flash::Response<HttpResponse, FlashData> {
|
||||
let conn = &data.conn;
|
||||
|
||||
let form = post_form.into_inner();
|
||||
|
||||
post::ActiveModel {
|
||||
title: Set(form.title.to_owned()),
|
||||
text: Set(form.text.to_owned()),
|
||||
..Default::default()
|
||||
}
|
||||
.save(conn)
|
||||
.await
|
||||
.expect("could not insert post");
|
||||
|
||||
let flash = FlashData {
|
||||
kind: "success".to_owned(),
|
||||
message: "Post successfully added.".to_owned(),
|
||||
};
|
||||
|
||||
actix_flash::Response::with_redirect(flash, "/")
|
||||
}
|
||||
|
||||
#[get("/{id}")]
|
||||
async fn edit(data: web::Data<AppState>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
|
||||
let conn = &data.conn;
|
||||
let template = &data.templates;
|
||||
|
||||
let post: post::Model = Post::find_by_id(id.into_inner())
|
||||
.one(conn)
|
||||
.await
|
||||
.expect("could not find post")
|
||||
.unwrap();
|
||||
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("post", &post);
|
||||
|
||||
let body = template
|
||||
.render("edit.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[post("/{id}")]
|
||||
async fn update(
|
||||
data: web::Data<AppState>,
|
||||
id: web::Path<i32>,
|
||||
post_form: web::Form<post::Model>,
|
||||
) -> actix_flash::Response<HttpResponse, FlashData> {
|
||||
let conn = &data.conn;
|
||||
let form = post_form.into_inner();
|
||||
|
||||
post::ActiveModel {
|
||||
id: Set(id.into_inner()),
|
||||
title: Set(form.title.to_owned()),
|
||||
text: Set(form.text.to_owned()),
|
||||
}
|
||||
.save(conn)
|
||||
.await
|
||||
.expect("could not edit post");
|
||||
|
||||
let flash = FlashData {
|
||||
kind: "success".to_owned(),
|
||||
message: "Post successfully updated.".to_owned(),
|
||||
};
|
||||
|
||||
actix_flash::Response::with_redirect(flash, "/")
|
||||
}
|
||||
|
||||
#[post("/delete/{id}")]
|
||||
async fn delete(
|
||||
data: web::Data<AppState>,
|
||||
id: web::Path<i32>,
|
||||
) -> actix_flash::Response<HttpResponse, FlashData> {
|
||||
let conn = &data.conn;
|
||||
|
||||
let post: post::ActiveModel = Post::find_by_id(id.into_inner())
|
||||
.one(conn)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
post.delete(conn).await.unwrap();
|
||||
|
||||
let flash = FlashData {
|
||||
kind: "success".to_owned(),
|
||||
message: "Post successfully deleted.".to_owned(),
|
||||
};
|
||||
|
||||
actix_flash::Response::with_redirect(flash, "/")
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "debug");
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// get env vars
|
||||
dotenv::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);
|
||||
|
||||
// create post table if not exists
|
||||
let conn = sea_orm::Database::connect(&db_url).await.unwrap();
|
||||
Migrator::up(&conn, None).await.unwrap();
|
||||
let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();
|
||||
let state = AppState { templates, conn };
|
||||
|
||||
let mut listenfd = ListenFd::from_env();
|
||||
let mut server = HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(state.clone())
|
||||
.wrap(middleware::Logger::default()) // enable logger
|
||||
.wrap(actix_flash::Flash::default())
|
||||
.configure(init)
|
||||
.service(fs::Files::new("/static", "./static").show_files_listing())
|
||||
});
|
||||
|
||||
server = match listenfd.take_tcp_listener(0)? {
|
||||
Some(listener) => server.listen(listener)?,
|
||||
None => server.bind(&server_url)?,
|
||||
};
|
||||
|
||||
println!("Starting server at {}", server_url);
|
||||
server.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(list);
|
||||
cfg.service(new);
|
||||
cfg.service(create);
|
||||
cfg.service(edit);
|
||||
cfg.service(update);
|
||||
cfg.service(delete);
|
||||
fn main() {
|
||||
actix3_example_api::main();
|
||||
}
|
||||
|
@ -6,30 +6,7 @@ edition = "2021"
|
||||
publish = false
|
||||
|
||||
[workspace]
|
||||
members = [".", "entity", "migration"]
|
||||
members = [".", "api", "core", "entity", "migration"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = "0.6"
|
||||
actix-http = "3"
|
||||
actix-rt = "2.7"
|
||||
actix-service = "2"
|
||||
actix-web = "4"
|
||||
|
||||
tera = "1.15.0"
|
||||
dotenv = "0.15"
|
||||
listenfd = "0.5"
|
||||
serde = "1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
entity = { path = "entity" }
|
||||
migration = { path = "migration" }
|
||||
|
||||
[dependencies.sea-orm]
|
||||
path = "../../" # remove this line in your own project
|
||||
version = "^0.10.0" # sea-orm version
|
||||
features = [
|
||||
"debug-print",
|
||||
"runtime-actix-native-tls",
|
||||
"sqlx-mysql",
|
||||
# "sqlx-postgres",
|
||||
# "sqlx-sqlite",
|
||||
]
|
||||
actix-example-api = { path = "api" }
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
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 `Cargo.toml` (the `"sqlx-mysql",` line)
|
||||
1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-mysql",` line)
|
||||
|
||||
1. Execute `cargo run` to start the server
|
||||
|
||||
@ -16,3 +16,10 @@ Run server with auto-reloading:
|
||||
cargo install systemfd cargo-watch
|
||||
systemfd --no-pid -s http::8000 -- cargo watch -x run
|
||||
```
|
||||
|
||||
Run mock test on the core logic crate:
|
||||
|
||||
```bash
|
||||
cd core
|
||||
cargo test --features mock
|
||||
```
|
||||
|
21
examples/actix_example/api/Cargo.toml
Normal file
21
examples/actix_example/api/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "actix-example-api"
|
||||
version = "0.1.0"
|
||||
authors = ["Sam Samai <sam@studio2pi.com.au>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
actix-example-core = { path = "../core" }
|
||||
actix-files = "0.6"
|
||||
actix-http = "3"
|
||||
actix-rt = "2.7"
|
||||
actix-service = "2"
|
||||
actix-web = "4"
|
||||
tera = "1.15.0"
|
||||
dotenv = "0.15"
|
||||
listenfd = "0.5"
|
||||
serde = "1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
entity = { path = "../entity" }
|
||||
migration = { path = "../migration" }
|
215
examples/actix_example/api/src/lib.rs
Normal file
215
examples/actix_example/api/src/lib.rs
Normal file
@ -0,0 +1,215 @@
|
||||
use actix_example_core::{
|
||||
sea_orm::{Database, DatabaseConnection},
|
||||
Mutation, Query,
|
||||
};
|
||||
use actix_files::Files as Fs;
|
||||
use actix_web::{
|
||||
error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result,
|
||||
};
|
||||
|
||||
use entity::post;
|
||||
use listenfd::ListenFd;
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use tera::Tera;
|
||||
|
||||
const DEFAULT_POSTS_PER_PAGE: u64 = 5;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AppState {
|
||||
templates: tera::Tera,
|
||||
conn: DatabaseConnection,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Params {
|
||||
page: Option<u64>,
|
||||
posts_per_page: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
struct FlashData {
|
||||
kind: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn list(req: HttpRequest, data: web::Data<AppState>) -> Result<HttpResponse, Error> {
|
||||
let template = &data.templates;
|
||||
let conn = &data.conn;
|
||||
|
||||
// get params
|
||||
let params = web::Query::<Params>::from_query(req.query_string()).unwrap();
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE);
|
||||
|
||||
let (posts, num_pages) = Query::find_posts_in_page(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);
|
||||
|
||||
let body = template
|
||||
.render("index.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[get("/new")]
|
||||
async fn new(data: web::Data<AppState>) -> Result<HttpResponse, Error> {
|
||||
let template = &data.templates;
|
||||
let ctx = tera::Context::new();
|
||||
let body = template
|
||||
.render("new.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[post("/")]
|
||||
async fn create(
|
||||
data: web::Data<AppState>,
|
||||
post_form: web::Form<post::Model>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let conn = &data.conn;
|
||||
|
||||
let form = post_form.into_inner();
|
||||
|
||||
Mutation::create_post(conn, form)
|
||||
.await
|
||||
.expect("could not insert post");
|
||||
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("location", "/"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[get("/{id}")]
|
||||
async fn edit(data: web::Data<AppState>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
|
||||
let conn = &data.conn;
|
||||
let template = &data.templates;
|
||||
let id = id.into_inner();
|
||||
|
||||
let post: post::Model = Query::find_post_by_id(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 = template
|
||||
.render("edit.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[post("/{id}")]
|
||||
async fn update(
|
||||
data: web::Data<AppState>,
|
||||
id: web::Path<i32>,
|
||||
post_form: web::Form<post::Model>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let conn = &data.conn;
|
||||
let form = post_form.into_inner();
|
||||
let id = id.into_inner();
|
||||
|
||||
Mutation::update_post_by_id(conn, id, form)
|
||||
.await
|
||||
.expect("could not edit post");
|
||||
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("location", "/"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[post("/delete/{id}")]
|
||||
async fn delete(data: web::Data<AppState>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
|
||||
let conn = &data.conn;
|
||||
let id = id.into_inner();
|
||||
|
||||
Mutation::delete_post(conn, id)
|
||||
.await
|
||||
.expect("could not delete post");
|
||||
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("location", "/"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
async fn not_found(data: web::Data<AppState>, request: HttpRequest) -> Result<HttpResponse, Error> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("uri", request.uri().path());
|
||||
|
||||
let template = &data.templates;
|
||||
let body = template
|
||||
.render("error/404.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn start() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "debug");
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// get env vars
|
||||
dotenv::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);
|
||||
|
||||
// establish connection to database and apply migrations
|
||||
// -> create post table if not exists
|
||||
let conn = Database::connect(&db_url).await.unwrap();
|
||||
Migrator::up(&conn, None).await.unwrap();
|
||||
|
||||
// load tera templates and build app state
|
||||
let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();
|
||||
let state = AppState { templates, conn };
|
||||
|
||||
// create server and try to serve over socket if possible
|
||||
let mut listenfd = ListenFd::from_env();
|
||||
let mut server = HttpServer::new(move || {
|
||||
App::new()
|
||||
.service(Fs::new("/static", "./api/static"))
|
||||
.app_data(web::Data::new(state.clone()))
|
||||
.wrap(middleware::Logger::default()) // enable logger
|
||||
.default_service(web::route().to(not_found))
|
||||
.configure(init)
|
||||
});
|
||||
|
||||
server = match listenfd.take_tcp_listener(0)? {
|
||||
Some(listener) => server.listen(listener)?,
|
||||
None => server.bind(&server_url)?,
|
||||
};
|
||||
|
||||
println!("Starting server at {}", server_url);
|
||||
server.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(list);
|
||||
cfg.service(new);
|
||||
cfg.service(create);
|
||||
cfg.service(edit);
|
||||
cfg.service(update);
|
||||
cfg.service(delete);
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
let result = start();
|
||||
|
||||
if let Some(err) = result.err() {
|
||||
println!("Error: {}", err);
|
||||
}
|
||||
}
|
30
examples/actix_example/core/Cargo.toml
Normal file
30
examples/actix_example/core/Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "actix-example-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
entity = { path = "../entity" }
|
||||
|
||||
[dependencies.sea-orm]
|
||||
path = "../../../" # remove this line in your own project
|
||||
version = "^0.10.0" # sea-orm version
|
||||
features = [
|
||||
"debug-print",
|
||||
"runtime-async-std-native-tls",
|
||||
"sqlx-mysql",
|
||||
# "sqlx-postgres",
|
||||
# "sqlx-sqlite",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.20.0", features = ["macros", "rt"] }
|
||||
|
||||
[features]
|
||||
mock = ["sea-orm/mock"]
|
||||
|
||||
[[test]]
|
||||
name = "mock"
|
||||
required-features = ["mock"]
|
7
examples/actix_example/core/src/lib.rs
Normal file
7
examples/actix_example/core/src/lib.rs
Normal file
@ -0,0 +1,7 @@
|
||||
mod mutation;
|
||||
mod query;
|
||||
|
||||
pub use mutation::*;
|
||||
pub use query::*;
|
||||
|
||||
pub use sea_orm;
|
53
examples/actix_example/core/src/mutation.rs
Normal file
53
examples/actix_example/core/src/mutation.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use ::entity::{post, post::Entity as Post};
|
||||
use sea_orm::*;
|
||||
|
||||
pub struct Mutation;
|
||||
|
||||
impl Mutation {
|
||||
pub async fn create_post(
|
||||
db: &DbConn,
|
||||
form_data: post::Model,
|
||||
) -> Result<post::ActiveModel, DbErr> {
|
||||
post::ActiveModel {
|
||||
title: Set(form_data.title.to_owned()),
|
||||
text: Set(form_data.text.to_owned()),
|
||||
..Default::default()
|
||||
}
|
||||
.save(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_post_by_id(
|
||||
db: &DbConn,
|
||||
id: i32,
|
||||
form_data: post::Model,
|
||||
) -> Result<post::Model, DbErr> {
|
||||
let post: post::ActiveModel = Post::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or(DbErr::Custom("Cannot find post.".to_owned()))
|
||||
.map(Into::into)?;
|
||||
|
||||
post::ActiveModel {
|
||||
id: post.id,
|
||||
title: Set(form_data.title.to_owned()),
|
||||
text: Set(form_data.text.to_owned()),
|
||||
}
|
||||
.update(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_post(db: &DbConn, id: i32) -> Result<DeleteResult, DbErr> {
|
||||
let post: post::ActiveModel = Post::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or(DbErr::Custom("Cannot find post.".to_owned()))
|
||||
.map(Into::into)?;
|
||||
|
||||
post.delete(db).await
|
||||
}
|
||||
|
||||
pub async fn delete_all_posts(db: &DbConn) -> Result<DeleteResult, DbErr> {
|
||||
Post::delete_many().exec(db).await
|
||||
}
|
||||
}
|
26
examples/actix_example/core/src/query.rs
Normal file
26
examples/actix_example/core/src/query.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use ::entity::{post, post::Entity as Post};
|
||||
use sea_orm::*;
|
||||
|
||||
pub struct Query;
|
||||
|
||||
impl Query {
|
||||
pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result<Option<post::Model>, DbErr> {
|
||||
Post::find_by_id(id).one(db).await
|
||||
}
|
||||
|
||||
/// If ok, returns (post models, num pages).
|
||||
pub async fn find_posts_in_page(
|
||||
db: &DbConn,
|
||||
page: u64,
|
||||
posts_per_page: u64,
|
||||
) -> Result<(Vec<post::Model>, u64), DbErr> {
|
||||
// Setup paginator
|
||||
let paginator = Post::find()
|
||||
.order_by_asc(post::Column::Id)
|
||||
.paginate(db, posts_per_page);
|
||||
let num_pages = paginator.num_pages().await?;
|
||||
|
||||
// Fetch paginated posts
|
||||
paginator.fetch_page(page - 1).await.map(|p| (p, num_pages))
|
||||
}
|
||||
}
|
79
examples/actix_example/core/tests/mock.rs
Normal file
79
examples/actix_example/core/tests/mock.rs
Normal file
@ -0,0 +1,79 @@
|
||||
mod prepare;
|
||||
|
||||
use actix_example_core::{Mutation, Query};
|
||||
use entity::post;
|
||||
use prepare::prepare_mock_db;
|
||||
|
||||
#[tokio::test]
|
||||
async fn main() {
|
||||
let db = &prepare_mock_db();
|
||||
|
||||
{
|
||||
let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(post.id, 1);
|
||||
}
|
||||
|
||||
{
|
||||
let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(post.id, 5);
|
||||
}
|
||||
|
||||
{
|
||||
let post = Mutation::create_post(
|
||||
db,
|
||||
post::Model {
|
||||
id: 0,
|
||||
title: "Title D".to_owned(),
|
||||
text: "Text D".to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
post,
|
||||
post::ActiveModel {
|
||||
id: sea_orm::ActiveValue::Unchanged(6),
|
||||
title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()),
|
||||
text: sea_orm::ActiveValue::Unchanged("Text D".to_owned())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let post = Mutation::update_post_by_id(
|
||||
db,
|
||||
1,
|
||||
post::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
post,
|
||||
post::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let result = Mutation::delete_post(db, 5).await.unwrap();
|
||||
|
||||
assert_eq!(result.rows_affected, 1);
|
||||
}
|
||||
|
||||
{
|
||||
let result = Mutation::delete_all_posts(db).await.unwrap();
|
||||
|
||||
assert_eq!(result.rows_affected, 5);
|
||||
}
|
||||
}
|
50
examples/actix_example/core/tests/prepare.rs
Normal file
50
examples/actix_example/core/tests/prepare.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use ::entity::post;
|
||||
use sea_orm::*;
|
||||
|
||||
#[cfg(feature = "mock")]
|
||||
pub fn prepare_mock_db() -> DatabaseConnection {
|
||||
MockDatabase::new(DatabaseBackend::Postgres)
|
||||
.append_query_results(vec![
|
||||
vec![post::Model {
|
||||
id: 1,
|
||||
title: "Title A".to_owned(),
|
||||
text: "Text A".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 5,
|
||||
title: "Title C".to_owned(),
|
||||
text: "Text C".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 6,
|
||||
title: "Title D".to_owned(),
|
||||
text: "Text D".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 1,
|
||||
title: "Title A".to_owned(),
|
||||
text: "Text A".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 5,
|
||||
title: "Title C".to_owned(),
|
||||
text: "Text C".to_owned(),
|
||||
}],
|
||||
])
|
||||
.append_exec_results(vec![
|
||||
MockExecResult {
|
||||
last_insert_id: 6,
|
||||
rows_affected: 1,
|
||||
},
|
||||
MockExecResult {
|
||||
last_insert_id: 6,
|
||||
rows_affected: 5,
|
||||
},
|
||||
])
|
||||
.into_connection()
|
||||
}
|
@ -1,223 +1,3 @@
|
||||
use actix_files::Files as Fs;
|
||||
use actix_web::{
|
||||
error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result,
|
||||
};
|
||||
|
||||
use entity::post;
|
||||
use entity::post::Entity as Post;
|
||||
use listenfd::ListenFd;
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use sea_orm::{entity::*, query::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use tera::Tera;
|
||||
|
||||
const DEFAULT_POSTS_PER_PAGE: u64 = 5;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AppState {
|
||||
templates: tera::Tera,
|
||||
conn: DatabaseConnection,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Params {
|
||||
page: Option<u64>,
|
||||
posts_per_page: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
struct FlashData {
|
||||
kind: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn list(req: HttpRequest, data: web::Data<AppState>) -> Result<HttpResponse, Error> {
|
||||
let template = &data.templates;
|
||||
let conn = &data.conn;
|
||||
|
||||
// get params
|
||||
let params = web::Query::<Params>::from_query(req.query_string()).unwrap();
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE);
|
||||
let paginator = Post::find()
|
||||
.order_by_asc(post::Column::Id)
|
||||
.paginate(conn, posts_per_page);
|
||||
let num_pages = paginator.num_pages().await.ok().unwrap();
|
||||
|
||||
let posts = paginator
|
||||
.fetch_page(page - 1)
|
||||
.await
|
||||
.expect("could not retrieve posts");
|
||||
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);
|
||||
|
||||
let body = template
|
||||
.render("index.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[get("/new")]
|
||||
async fn new(data: web::Data<AppState>) -> Result<HttpResponse, Error> {
|
||||
let template = &data.templates;
|
||||
let ctx = tera::Context::new();
|
||||
let body = template
|
||||
.render("new.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[post("/")]
|
||||
async fn create(
|
||||
data: web::Data<AppState>,
|
||||
post_form: web::Form<post::Model>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let conn = &data.conn;
|
||||
|
||||
let form = post_form.into_inner();
|
||||
|
||||
post::ActiveModel {
|
||||
title: Set(form.title.to_owned()),
|
||||
text: Set(form.text.to_owned()),
|
||||
..Default::default()
|
||||
}
|
||||
.save(conn)
|
||||
.await
|
||||
.expect("could not insert post");
|
||||
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("location", "/"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[get("/{id}")]
|
||||
async fn edit(data: web::Data<AppState>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
|
||||
let conn = &data.conn;
|
||||
let template = &data.templates;
|
||||
|
||||
let post: post::Model = Post::find_by_id(id.into_inner())
|
||||
.one(conn)
|
||||
.await
|
||||
.expect("could not find post")
|
||||
.unwrap();
|
||||
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("post", &post);
|
||||
|
||||
let body = template
|
||||
.render("edit.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[post("/{id}")]
|
||||
async fn update(
|
||||
data: web::Data<AppState>,
|
||||
id: web::Path<i32>,
|
||||
post_form: web::Form<post::Model>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let conn = &data.conn;
|
||||
let form = post_form.into_inner();
|
||||
|
||||
post::ActiveModel {
|
||||
id: Set(id.into_inner()),
|
||||
title: Set(form.title.to_owned()),
|
||||
text: Set(form.text.to_owned()),
|
||||
}
|
||||
.save(conn)
|
||||
.await
|
||||
.expect("could not edit post");
|
||||
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("location", "/"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[post("/delete/{id}")]
|
||||
async fn delete(data: web::Data<AppState>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
|
||||
let conn = &data.conn;
|
||||
|
||||
let post: post::ActiveModel = Post::find_by_id(id.into_inner())
|
||||
.one(conn)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
post.delete(conn).await.unwrap();
|
||||
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("location", "/"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
async fn not_found(data: web::Data<AppState>, request: HttpRequest) -> Result<HttpResponse, Error> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("uri", request.uri().path());
|
||||
|
||||
let template = &data.templates;
|
||||
let body = template
|
||||
.render("error/404.html.tera", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "debug");
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// get env vars
|
||||
dotenv::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);
|
||||
|
||||
// establish connection to database and apply migrations
|
||||
// -> create post table if not exists
|
||||
let conn = sea_orm::Database::connect(&db_url).await.unwrap();
|
||||
Migrator::up(&conn, None).await.unwrap();
|
||||
|
||||
// load tera templates and build app state
|
||||
let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();
|
||||
let state = AppState { templates, conn };
|
||||
|
||||
// create server and try to serve over socket if possible
|
||||
let mut listenfd = ListenFd::from_env();
|
||||
let mut server = HttpServer::new(move || {
|
||||
App::new()
|
||||
.service(Fs::new("/static", "./static"))
|
||||
.app_data(web::Data::new(state.clone()))
|
||||
.wrap(middleware::Logger::default()) // enable logger
|
||||
.default_service(web::route().to(not_found))
|
||||
.configure(init)
|
||||
});
|
||||
|
||||
server = match listenfd.take_tcp_listener(0)? {
|
||||
Some(listener) => server.listen(listener)?,
|
||||
None => server.bind(&server_url)?,
|
||||
};
|
||||
|
||||
println!("Starting server at {}", server_url);
|
||||
server.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(list);
|
||||
cfg.service(new);
|
||||
cfg.service(create);
|
||||
cfg.service(edit);
|
||||
cfg.service(update);
|
||||
cfg.service(delete);
|
||||
fn main() {
|
||||
actix_example_api::main();
|
||||
}
|
||||
|
@ -5,32 +5,8 @@ authors = ["Yoshiera Huang <huangjasper@126.com>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[workspace]
|
||||
members = [".", "entity", "migration"]
|
||||
members = [".", "api", "core", "entity", "migration"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.18.1", features = ["full"] }
|
||||
axum = "0.5.4"
|
||||
tower = "0.4.12"
|
||||
tower-http = { version = "0.3.3", features = ["fs"] }
|
||||
tower-cookies = "0.6.0"
|
||||
anyhow = "1.0.57"
|
||||
dotenv = "0.15.0"
|
||||
serde = "1.0.137"
|
||||
serde_json = "1.0.81"
|
||||
tera = "1.15.0"
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
|
||||
entity = { path = "entity" }
|
||||
migration = { path = "migration" }
|
||||
|
||||
[dependencies.sea-orm]
|
||||
path = "../../" # remove this line in your own project
|
||||
version = "^0.10.0" # sea-orm version
|
||||
features = [
|
||||
"debug-print",
|
||||
"runtime-tokio-native-tls",
|
||||
"sqlx-postgres",
|
||||
# "sqlx-mysql",
|
||||
# "sqlx-sqlite",
|
||||
]
|
||||
axum-example-api = { path = "api" }
|
||||
|
@ -4,8 +4,15 @@
|
||||
|
||||
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 `Cargo.toml` (the `"sqlx-postgres",` line)
|
||||
1. Turn on the appropriate database feature for your chosen db in `core/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 core logic crate:
|
||||
|
||||
```bash
|
||||
cd core
|
||||
cargo test --features mock
|
||||
```
|
||||
|
22
examples/axum_example/api/Cargo.toml
Normal file
22
examples/axum_example/api/Cargo.toml
Normal file
@ -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-core = { path = "../core" }
|
||||
tokio = { version = "1.18.1", features = ["full"] }
|
||||
axum = "0.5.4"
|
||||
tower = "0.4.12"
|
||||
tower-http = { version = "0.3.3", features = ["fs"] }
|
||||
tower-cookies = "0.6.0"
|
||||
anyhow = "1.0.57"
|
||||
dotenv = "0.15.0"
|
||||
serde = "1.0.137"
|
||||
serde_json = "1.0.81"
|
||||
tera = "1.15.0"
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
|
||||
entity = { path = "../entity" }
|
||||
migration = { path = "../migration" }
|
210
examples/axum_example/api/src/lib.rs
Normal file
210
examples/axum_example/api/src/lib.rs
Normal file
@ -0,0 +1,210 @@
|
||||
mod flash;
|
||||
|
||||
use axum::{
|
||||
extract::{Extension, Form, Path, Query},
|
||||
http::StatusCode,
|
||||
response::Html,
|
||||
routing::{get, get_service, post},
|
||||
Router, Server,
|
||||
};
|
||||
use axum_example_core::{
|
||||
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::str::FromStr;
|
||||
use std::{env, net::SocketAddr};
|
||||
use tera::Tera;
|
||||
use tower::ServiceBuilder;
|
||||
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();
|
||||
|
||||
dotenv::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(
|
||||
"/static",
|
||||
get_service(ServeDir::new(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/static"
|
||||
)))
|
||||
.handle_error(|error: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(CookieManagerLayer::new())
|
||||
.layer(Extension(conn))
|
||||
.layer(Extension(templates)),
|
||||
);
|
||||
|
||||
let addr = SocketAddr::from_str(&server_url).unwrap();
|
||||
Server::bind(&addr).serve(app.into_make_service()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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(
|
||||
Extension(ref templates): Extension<Tera>,
|
||||
Extension(ref conn): Extension<DatabaseConnection>,
|
||||
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(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 = templates
|
||||
.render("index.html.tera", &ctx)
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
|
||||
|
||||
Ok(Html(body))
|
||||
}
|
||||
|
||||
async fn new_post(
|
||||
Extension(ref templates): Extension<Tera>,
|
||||
) -> Result<Html<String>, (StatusCode, &'static str)> {
|
||||
let ctx = tera::Context::new();
|
||||
let body = templates
|
||||
.render("new.html.tera", &ctx)
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
|
||||
|
||||
Ok(Html(body))
|
||||
}
|
||||
|
||||
async fn create_post(
|
||||
Extension(ref conn): Extension<DatabaseConnection>,
|
||||
form: Form<post::Model>,
|
||||
mut cookies: Cookies,
|
||||
) -> Result<PostResponse, (StatusCode, &'static str)> {
|
||||
let form = form.0;
|
||||
|
||||
MutationCore::create_post(conn, form)
|
||||
.await
|
||||
.expect("could not insert post");
|
||||
|
||||
let data = FlashData {
|
||||
kind: "success".to_owned(),
|
||||
message: "Post succcessfully added".to_owned(),
|
||||
};
|
||||
|
||||
Ok(post_response(&mut cookies, data))
|
||||
}
|
||||
|
||||
async fn edit_post(
|
||||
Extension(ref templates): Extension<Tera>,
|
||||
Extension(ref conn): Extension<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
) -> Result<Html<String>, (StatusCode, &'static str)> {
|
||||
let post: post::Model = QueryCore::find_post_by_id(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 = templates
|
||||
.render("edit.html.tera", &ctx)
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
|
||||
|
||||
Ok(Html(body))
|
||||
}
|
||||
|
||||
async fn update_post(
|
||||
Extension(ref conn): Extension<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
form: Form<post::Model>,
|
||||
mut cookies: Cookies,
|
||||
) -> Result<PostResponse, (StatusCode, String)> {
|
||||
let form = form.0;
|
||||
|
||||
MutationCore::update_post_by_id(conn, id, form)
|
||||
.await
|
||||
.expect("could not edit post");
|
||||
|
||||
let data = FlashData {
|
||||
kind: "success".to_owned(),
|
||||
message: "Post succcessfully updated".to_owned(),
|
||||
};
|
||||
|
||||
Ok(post_response(&mut cookies, data))
|
||||
}
|
||||
|
||||
async fn delete_post(
|
||||
Extension(ref conn): Extension<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
mut cookies: Cookies,
|
||||
) -> Result<PostResponse, (StatusCode, &'static str)> {
|
||||
MutationCore::delete_post(conn, id)
|
||||
.await
|
||||
.expect("could not delete post");
|
||||
|
||||
let data = FlashData {
|
||||
kind: "success".to_owned(),
|
||||
message: "Post succcessfully deleted".to_owned(),
|
||||
};
|
||||
|
||||
Ok(post_response(&mut cookies, data))
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
let result = start();
|
||||
|
||||
if let Some(err) = result.err() {
|
||||
println!("Error: {}", err);
|
||||
}
|
||||
}
|
30
examples/axum_example/core/Cargo.toml
Normal file
30
examples/axum_example/core/Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "axum-example-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
entity = { path = "../entity" }
|
||||
|
||||
[dependencies.sea-orm]
|
||||
path = "../../../" # remove this line in your own project
|
||||
version = "^0.10.0" # sea-orm version
|
||||
features = [
|
||||
"debug-print",
|
||||
"runtime-async-std-native-tls",
|
||||
"sqlx-postgres",
|
||||
# "sqlx-mysql",
|
||||
# "sqlx-sqlite",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.20.0", features = ["macros", "rt"] }
|
||||
|
||||
[features]
|
||||
mock = ["sea-orm/mock"]
|
||||
|
||||
[[test]]
|
||||
name = "mock"
|
||||
required-features = ["mock"]
|
7
examples/axum_example/core/src/lib.rs
Normal file
7
examples/axum_example/core/src/lib.rs
Normal file
@ -0,0 +1,7 @@
|
||||
mod mutation;
|
||||
mod query;
|
||||
|
||||
pub use mutation::*;
|
||||
pub use query::*;
|
||||
|
||||
pub use sea_orm;
|
53
examples/axum_example/core/src/mutation.rs
Normal file
53
examples/axum_example/core/src/mutation.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use ::entity::{post, post::Entity as Post};
|
||||
use sea_orm::*;
|
||||
|
||||
pub struct Mutation;
|
||||
|
||||
impl Mutation {
|
||||
pub async fn create_post(
|
||||
db: &DbConn,
|
||||
form_data: post::Model,
|
||||
) -> Result<post::ActiveModel, DbErr> {
|
||||
post::ActiveModel {
|
||||
title: Set(form_data.title.to_owned()),
|
||||
text: Set(form_data.text.to_owned()),
|
||||
..Default::default()
|
||||
}
|
||||
.save(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_post_by_id(
|
||||
db: &DbConn,
|
||||
id: i32,
|
||||
form_data: post::Model,
|
||||
) -> Result<post::Model, DbErr> {
|
||||
let post: post::ActiveModel = Post::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or(DbErr::Custom("Cannot find post.".to_owned()))
|
||||
.map(Into::into)?;
|
||||
|
||||
post::ActiveModel {
|
||||
id: post.id,
|
||||
title: Set(form_data.title.to_owned()),
|
||||
text: Set(form_data.text.to_owned()),
|
||||
}
|
||||
.update(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_post(db: &DbConn, id: i32) -> Result<DeleteResult, DbErr> {
|
||||
let post: post::ActiveModel = Post::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or(DbErr::Custom("Cannot find post.".to_owned()))
|
||||
.map(Into::into)?;
|
||||
|
||||
post.delete(db).await
|
||||
}
|
||||
|
||||
pub async fn delete_all_posts(db: &DbConn) -> Result<DeleteResult, DbErr> {
|
||||
Post::delete_many().exec(db).await
|
||||
}
|
||||
}
|
26
examples/axum_example/core/src/query.rs
Normal file
26
examples/axum_example/core/src/query.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use ::entity::{post, post::Entity as Post};
|
||||
use sea_orm::*;
|
||||
|
||||
pub struct Query;
|
||||
|
||||
impl Query {
|
||||
pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result<Option<post::Model>, DbErr> {
|
||||
Post::find_by_id(id).one(db).await
|
||||
}
|
||||
|
||||
/// If ok, returns (post models, num pages).
|
||||
pub async fn find_posts_in_page(
|
||||
db: &DbConn,
|
||||
page: u64,
|
||||
posts_per_page: u64,
|
||||
) -> Result<(Vec<post::Model>, u64), DbErr> {
|
||||
// Setup paginator
|
||||
let paginator = Post::find()
|
||||
.order_by_asc(post::Column::Id)
|
||||
.paginate(db, posts_per_page);
|
||||
let num_pages = paginator.num_pages().await?;
|
||||
|
||||
// Fetch paginated posts
|
||||
paginator.fetch_page(page - 1).await.map(|p| (p, num_pages))
|
||||
}
|
||||
}
|
79
examples/axum_example/core/tests/mock.rs
Normal file
79
examples/axum_example/core/tests/mock.rs
Normal file
@ -0,0 +1,79 @@
|
||||
mod prepare;
|
||||
|
||||
use axum_example_core::{Mutation, Query};
|
||||
use entity::post;
|
||||
use prepare::prepare_mock_db;
|
||||
|
||||
#[tokio::test]
|
||||
async fn main() {
|
||||
let db = &prepare_mock_db();
|
||||
|
||||
{
|
||||
let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(post.id, 1);
|
||||
}
|
||||
|
||||
{
|
||||
let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(post.id, 5);
|
||||
}
|
||||
|
||||
{
|
||||
let post = Mutation::create_post(
|
||||
db,
|
||||
post::Model {
|
||||
id: 0,
|
||||
title: "Title D".to_owned(),
|
||||
text: "Text D".to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
post,
|
||||
post::ActiveModel {
|
||||
id: sea_orm::ActiveValue::Unchanged(6),
|
||||
title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()),
|
||||
text: sea_orm::ActiveValue::Unchanged("Text D".to_owned())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let post = Mutation::update_post_by_id(
|
||||
db,
|
||||
1,
|
||||
post::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
post,
|
||||
post::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let result = Mutation::delete_post(db, 5).await.unwrap();
|
||||
|
||||
assert_eq!(result.rows_affected, 1);
|
||||
}
|
||||
|
||||
{
|
||||
let result = Mutation::delete_all_posts(db).await.unwrap();
|
||||
|
||||
assert_eq!(result.rows_affected, 5);
|
||||
}
|
||||
}
|
50
examples/axum_example/core/tests/prepare.rs
Normal file
50
examples/axum_example/core/tests/prepare.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use ::entity::post;
|
||||
use sea_orm::*;
|
||||
|
||||
#[cfg(feature = "mock")]
|
||||
pub fn prepare_mock_db() -> DatabaseConnection {
|
||||
MockDatabase::new(DatabaseBackend::Postgres)
|
||||
.append_query_results(vec![
|
||||
vec![post::Model {
|
||||
id: 1,
|
||||
title: "Title A".to_owned(),
|
||||
text: "Text A".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 5,
|
||||
title: "Title C".to_owned(),
|
||||
text: "Text C".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 6,
|
||||
title: "Title D".to_owned(),
|
||||
text: "Text D".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 1,
|
||||
title: "Title A".to_owned(),
|
||||
text: "Text A".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 5,
|
||||
title: "Title C".to_owned(),
|
||||
text: "Text C".to_owned(),
|
||||
}],
|
||||
])
|
||||
.append_exec_results(vec![
|
||||
MockExecResult {
|
||||
last_insert_id: 6,
|
||||
rows_affected: 1,
|
||||
},
|
||||
MockExecResult {
|
||||
last_insert_id: 6,
|
||||
rows_affected: 5,
|
||||
},
|
||||
])
|
||||
.into_connection()
|
||||
}
|
@ -1,220 +1,3 @@
|
||||
mod flash;
|
||||
|
||||
use axum::{
|
||||
extract::{Extension, Form, Path, Query},
|
||||
http::StatusCode,
|
||||
response::Html,
|
||||
routing::{get, get_service, post},
|
||||
Router, Server,
|
||||
};
|
||||
use entity::post;
|
||||
use flash::{get_flash_cookie, post_response, PostResponse};
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use post::Entity as Post;
|
||||
use sea_orm::{prelude::*, Database, QueryOrder, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use std::{env, net::SocketAddr};
|
||||
use tera::Tera;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_cookies::{CookieManagerLayer, Cookies};
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
env::set_var("RUST_LOG", "debug");
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
dotenv::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(
|
||||
"/static",
|
||||
get_service(ServeDir::new(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/static"
|
||||
)))
|
||||
.handle_error(|error: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(CookieManagerLayer::new())
|
||||
.layer(Extension(conn))
|
||||
.layer(Extension(templates)),
|
||||
);
|
||||
|
||||
let addr = SocketAddr::from_str(&server_url).unwrap();
|
||||
Server::bind(&addr).serve(app.into_make_service()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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(
|
||||
Extension(ref templates): Extension<Tera>,
|
||||
Extension(ref conn): Extension<DatabaseConnection>,
|
||||
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 paginator = Post::find()
|
||||
.order_by_asc(post::Column::Id)
|
||||
.paginate(conn, posts_per_page);
|
||||
let num_pages = paginator.num_pages().await.ok().unwrap();
|
||||
let posts = paginator
|
||||
.fetch_page(page - 1)
|
||||
.await
|
||||
.expect("could not retrieve posts");
|
||||
|
||||
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 = templates
|
||||
.render("index.html.tera", &ctx)
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
|
||||
|
||||
Ok(Html(body))
|
||||
}
|
||||
|
||||
async fn new_post(
|
||||
Extension(ref templates): Extension<Tera>,
|
||||
) -> Result<Html<String>, (StatusCode, &'static str)> {
|
||||
let ctx = tera::Context::new();
|
||||
let body = templates
|
||||
.render("new.html.tera", &ctx)
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
|
||||
|
||||
Ok(Html(body))
|
||||
}
|
||||
|
||||
async fn create_post(
|
||||
Extension(ref conn): Extension<DatabaseConnection>,
|
||||
form: Form<post::Model>,
|
||||
mut cookies: Cookies,
|
||||
) -> Result<PostResponse, (StatusCode, &'static str)> {
|
||||
let model = form.0;
|
||||
|
||||
post::ActiveModel {
|
||||
title: Set(model.title.to_owned()),
|
||||
text: Set(model.text.to_owned()),
|
||||
..Default::default()
|
||||
}
|
||||
.save(conn)
|
||||
.await
|
||||
.expect("could not insert post");
|
||||
|
||||
let data = FlashData {
|
||||
kind: "success".to_owned(),
|
||||
message: "Post succcessfully added".to_owned(),
|
||||
};
|
||||
|
||||
Ok(post_response(&mut cookies, data))
|
||||
}
|
||||
|
||||
async fn edit_post(
|
||||
Extension(ref templates): Extension<Tera>,
|
||||
Extension(ref conn): Extension<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
) -> Result<Html<String>, (StatusCode, &'static str)> {
|
||||
let post: post::Model = Post::find_by_id(id)
|
||||
.one(conn)
|
||||
.await
|
||||
.expect("could not find post")
|
||||
.unwrap();
|
||||
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("post", &post);
|
||||
|
||||
let body = templates
|
||||
.render("edit.html.tera", &ctx)
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
|
||||
|
||||
Ok(Html(body))
|
||||
}
|
||||
|
||||
async fn update_post(
|
||||
Extension(ref conn): Extension<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
form: Form<post::Model>,
|
||||
mut cookies: Cookies,
|
||||
) -> Result<PostResponse, (StatusCode, String)> {
|
||||
let model = form.0;
|
||||
|
||||
post::ActiveModel {
|
||||
id: Set(id),
|
||||
title: Set(model.title.to_owned()),
|
||||
text: Set(model.text.to_owned()),
|
||||
}
|
||||
.save(conn)
|
||||
.await
|
||||
.expect("could not edit post");
|
||||
|
||||
let data = FlashData {
|
||||
kind: "success".to_owned(),
|
||||
message: "Post succcessfully updated".to_owned(),
|
||||
};
|
||||
|
||||
Ok(post_response(&mut cookies, data))
|
||||
}
|
||||
|
||||
async fn delete_post(
|
||||
Extension(ref conn): Extension<DatabaseConnection>,
|
||||
Path(id): Path<i32>,
|
||||
mut cookies: Cookies,
|
||||
) -> Result<PostResponse, (StatusCode, &'static str)> {
|
||||
let post: post::ActiveModel = Post::find_by_id(id)
|
||||
.one(conn)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
post.delete(conn).await.unwrap();
|
||||
|
||||
let data = FlashData {
|
||||
kind: "success".to_owned(),
|
||||
message: "Post succcessfully deleted".to_owned(),
|
||||
};
|
||||
|
||||
Ok(post_response(&mut cookies, data))
|
||||
fn main() {
|
||||
axum_example_api::main();
|
||||
}
|
||||
|
@ -7,22 +7,7 @@ publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[workspace]
|
||||
members = [".", "entity", "migration"]
|
||||
members = [".", "api", "core", "entity", "migration"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
axum = "^0.5.1"
|
||||
dotenv = "0.15.0"
|
||||
async-graphql-axum = "^4.0.6"
|
||||
entity = { path = "entity" }
|
||||
migration = { path = "migration" }
|
||||
|
||||
[dependencies.sea-orm]
|
||||
path = "../../" # remove this line in your own project
|
||||
version = "^0.10.0" # sea-orm version
|
||||
features = [
|
||||
"runtime-tokio-native-tls",
|
||||
# "sqlx-postgres",
|
||||
# "sqlx-mysql",
|
||||
"sqlx-sqlite"
|
||||
]
|
||||
graphql-example-api = { path = "api" }
|
||||
|
@ -6,8 +6,15 @@
|
||||
|
||||
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 `Cargo.toml` (the `"sqlx-sqlite",` line)
|
||||
1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-sqlite",` line)
|
||||
|
||||
1. Execute `cargo run` to start the server
|
||||
|
||||
1. Visit [localhost:3000/api/graphql](http://localhost:3000/api/graphql) in browser
|
||||
|
||||
Run mock test on the core logic crate:
|
||||
|
||||
```bash
|
||||
cd core
|
||||
cargo test --features mock
|
||||
```
|
||||
|
15
examples/graphql_example/api/Cargo.toml
Normal file
15
examples/graphql_example/api/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "graphql-example-api"
|
||||
authors = ["Aaron Leopold <aaronleopold1221@gmail.com>"]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
graphql-example-core = { path = "../core" }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
axum = "^0.5.1"
|
||||
dotenv = "0.15.0"
|
||||
async-graphql-axum = "^4.0.6"
|
||||
entity = { path = "../entity" }
|
||||
migration = { path = "../migration" }
|
21
examples/graphql_example/api/src/db.rs
Normal file
21
examples/graphql_example/api/src/db.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use graphql_example_core::sea_orm::DatabaseConnection;
|
||||
|
||||
pub struct Database {
|
||||
pub connection: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub async fn new() -> Self {
|
||||
let connection = graphql_example_core::sea_orm::Database::connect(
|
||||
std::env::var("DATABASE_URL").unwrap(),
|
||||
)
|
||||
.await
|
||||
.expect("Could not connect to database");
|
||||
|
||||
Database { connection }
|
||||
}
|
||||
|
||||
pub fn get_connection(&self) -> &DatabaseConnection {
|
||||
&self.connection
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use async_graphql::{Context, Object, Result};
|
||||
use entity::async_graphql::{self, InputObject, SimpleObject};
|
||||
use entity::note;
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
use graphql_example_core::Mutation;
|
||||
|
||||
use crate::db::Database;
|
||||
|
||||
@ -14,6 +14,16 @@ pub struct CreateNoteInput {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl CreateNoteInput {
|
||||
fn into_model_with_arbitrary_id(self) -> note::Model {
|
||||
note::Model {
|
||||
id: 0,
|
||||
title: self.title,
|
||||
text: self.text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct DeleteResult {
|
||||
pub success: bool,
|
||||
@ -31,22 +41,18 @@ impl NoteMutation {
|
||||
input: CreateNoteInput,
|
||||
) -> Result<note::Model> {
|
||||
let db = ctx.data::<Database>().unwrap();
|
||||
let conn = db.get_connection();
|
||||
|
||||
let note = note::ActiveModel {
|
||||
title: Set(input.title),
|
||||
text: Set(input.text),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(note.insert(db.get_connection()).await?)
|
||||
Ok(Mutation::create_note(conn, input.into_model_with_arbitrary_id()).await?)
|
||||
}
|
||||
|
||||
pub async fn delete_note(&self, ctx: &Context<'_>, id: i32) -> Result<DeleteResult> {
|
||||
let db = ctx.data::<Database>().unwrap();
|
||||
let conn = db.get_connection();
|
||||
|
||||
let res = note::Entity::delete_by_id(id)
|
||||
.exec(db.get_connection())
|
||||
.await?;
|
||||
let res = Mutation::delete_note(conn, id)
|
||||
.await
|
||||
.expect("Cannot delete note");
|
||||
|
||||
if res.rows_affected <= 1 {
|
||||
Ok(DeleteResult {
|
@ -1,6 +1,6 @@
|
||||
use async_graphql::{Context, Object, Result};
|
||||
use entity::{async_graphql, note};
|
||||
use sea_orm::EntityTrait;
|
||||
use graphql_example_core::Query;
|
||||
|
||||
use crate::db::Database;
|
||||
|
||||
@ -11,18 +11,18 @@ pub struct NoteQuery;
|
||||
impl NoteQuery {
|
||||
async fn get_notes(&self, ctx: &Context<'_>) -> Result<Vec<note::Model>> {
|
||||
let db = ctx.data::<Database>().unwrap();
|
||||
let conn = db.get_connection();
|
||||
|
||||
Ok(note::Entity::find()
|
||||
.all(db.get_connection())
|
||||
Ok(Query::get_all_notes(conn)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?)
|
||||
}
|
||||
|
||||
async fn get_note_by_id(&self, ctx: &Context<'_>, id: i32) -> Result<Option<note::Model>> {
|
||||
let db = ctx.data::<Database>().unwrap();
|
||||
let conn = db.get_connection();
|
||||
|
||||
Ok(note::Entity::find_by_id(id)
|
||||
.one(db.get_connection())
|
||||
Ok(Query::find_note_by_id(conn, id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?)
|
||||
}
|
49
examples/graphql_example/api/src/lib.rs
Normal file
49
examples/graphql_example/api/src/lib.rs
Normal file
@ -0,0 +1,49 @@
|
||||
mod db;
|
||||
mod graphql;
|
||||
|
||||
use entity::async_graphql;
|
||||
|
||||
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
|
||||
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
|
||||
use axum::{
|
||||
extract::Extension,
|
||||
response::{Html, IntoResponse},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use graphql::schema::{build_schema, AppSchema};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use dotenv::dotenv;
|
||||
|
||||
async fn graphql_handler(schema: Extension<AppSchema>, req: GraphQLRequest) -> GraphQLResponse {
|
||||
schema.execute(req.into_inner()).await.into()
|
||||
}
|
||||
|
||||
async fn graphql_playground() -> impl IntoResponse {
|
||||
Html(playground_source(GraphQLPlaygroundConfig::new(
|
||||
"/api/graphql",
|
||||
)))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() {
|
||||
#[cfg(debug_assertions)]
|
||||
dotenv().ok();
|
||||
|
||||
let schema = build_schema().await;
|
||||
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/api/graphql",
|
||||
get(graphql_playground).post(graphql_handler),
|
||||
)
|
||||
.layer(Extension(schema));
|
||||
|
||||
println!("Playground: http://localhost:3000/api/graphql");
|
||||
|
||||
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
30
examples/graphql_example/core/Cargo.toml
Normal file
30
examples/graphql_example/core/Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "graphql-example-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
entity = { path = "../entity" }
|
||||
|
||||
[dependencies.sea-orm]
|
||||
path = "../../../" # remove this line in your own project
|
||||
version = "^0.10.0" # sea-orm version
|
||||
features = [
|
||||
"debug-print",
|
||||
"runtime-async-std-native-tls",
|
||||
# "sqlx-postgres",
|
||||
# "sqlx-mysql",
|
||||
"sqlx-sqlite",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.20.0", features = ["macros", "rt"] }
|
||||
|
||||
[features]
|
||||
mock = ["sea-orm/mock"]
|
||||
|
||||
[[test]]
|
||||
name = "mock"
|
||||
required-features = ["mock"]
|
7
examples/graphql_example/core/src/lib.rs
Normal file
7
examples/graphql_example/core/src/lib.rs
Normal file
@ -0,0 +1,7 @@
|
||||
mod mutation;
|
||||
mod query;
|
||||
|
||||
pub use mutation::*;
|
||||
pub use query::*;
|
||||
|
||||
pub use sea_orm;
|
54
examples/graphql_example/core/src/mutation.rs
Normal file
54
examples/graphql_example/core/src/mutation.rs
Normal file
@ -0,0 +1,54 @@
|
||||
use ::entity::{note, note::Entity as Note};
|
||||
use sea_orm::*;
|
||||
|
||||
pub struct Mutation;
|
||||
|
||||
impl Mutation {
|
||||
pub async fn create_note(db: &DbConn, form_data: note::Model) -> Result<note::Model, DbErr> {
|
||||
let active_model = note::ActiveModel {
|
||||
title: Set(form_data.title.to_owned()),
|
||||
text: Set(form_data.text.to_owned()),
|
||||
..Default::default()
|
||||
};
|
||||
let res = Note::insert(active_model).exec(db).await?;
|
||||
|
||||
Ok(note::Model {
|
||||
id: res.last_insert_id,
|
||||
..form_data
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_note_by_id(
|
||||
db: &DbConn,
|
||||
id: i32,
|
||||
form_data: note::Model,
|
||||
) -> Result<note::Model, DbErr> {
|
||||
let note: note::ActiveModel = Note::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or(DbErr::Custom("Cannot find note.".to_owned()))
|
||||
.map(Into::into)?;
|
||||
|
||||
note::ActiveModel {
|
||||
id: note.id,
|
||||
title: Set(form_data.title.to_owned()),
|
||||
text: Set(form_data.text.to_owned()),
|
||||
}
|
||||
.update(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_note(db: &DbConn, id: i32) -> Result<DeleteResult, DbErr> {
|
||||
let note: note::ActiveModel = Note::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or(DbErr::Custom("Cannot find note.".to_owned()))
|
||||
.map(Into::into)?;
|
||||
|
||||
note.delete(db).await
|
||||
}
|
||||
|
||||
pub async fn delete_all_notes(db: &DbConn) -> Result<DeleteResult, DbErr> {
|
||||
Note::delete_many().exec(db).await
|
||||
}
|
||||
}
|
30
examples/graphql_example/core/src/query.rs
Normal file
30
examples/graphql_example/core/src/query.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use ::entity::{note, note::Entity as Note};
|
||||
use sea_orm::*;
|
||||
|
||||
pub struct Query;
|
||||
|
||||
impl Query {
|
||||
pub async fn find_note_by_id(db: &DbConn, id: i32) -> Result<Option<note::Model>, DbErr> {
|
||||
Note::find_by_id(id).one(db).await
|
||||
}
|
||||
|
||||
pub async fn get_all_notes(db: &DbConn) -> Result<Vec<note::Model>, DbErr> {
|
||||
Note::find().all(db).await
|
||||
}
|
||||
|
||||
/// If ok, returns (note models, num pages).
|
||||
pub async fn find_notes_in_page(
|
||||
db: &DbConn,
|
||||
page: u64,
|
||||
notes_per_page: u64,
|
||||
) -> Result<(Vec<note::Model>, u64), DbErr> {
|
||||
// Setup paginator
|
||||
let paginator = Note::find()
|
||||
.order_by_asc(note::Column::Id)
|
||||
.paginate(db, notes_per_page);
|
||||
let num_pages = paginator.num_pages().await?;
|
||||
|
||||
// Fetch paginated notes
|
||||
paginator.fetch_page(page - 1).await.map(|p| (p, num_pages))
|
||||
}
|
||||
}
|
79
examples/graphql_example/core/tests/mock.rs
Normal file
79
examples/graphql_example/core/tests/mock.rs
Normal file
@ -0,0 +1,79 @@
|
||||
mod prepare;
|
||||
|
||||
use entity::note;
|
||||
use graphql_example_core::{Mutation, Query};
|
||||
use prepare::prepare_mock_db;
|
||||
|
||||
#[tokio::test]
|
||||
async fn main() {
|
||||
let db = &prepare_mock_db();
|
||||
|
||||
{
|
||||
let note = Query::find_note_by_id(db, 1).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(note.id, 1);
|
||||
}
|
||||
|
||||
{
|
||||
let note = Query::find_note_by_id(db, 5).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(note.id, 5);
|
||||
}
|
||||
|
||||
{
|
||||
let note = Mutation::create_note(
|
||||
db,
|
||||
note::Model {
|
||||
id: 0,
|
||||
title: "Title D".to_owned(),
|
||||
text: "Text D".to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
note,
|
||||
note::Model {
|
||||
id: 6,
|
||||
title: "Title D".to_owned(),
|
||||
text: "Text D".to_owned(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let note = Mutation::update_note_by_id(
|
||||
db,
|
||||
1,
|
||||
note::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
note,
|
||||
note::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let result = Mutation::delete_note(db, 5).await.unwrap();
|
||||
|
||||
assert_eq!(result.rows_affected, 1);
|
||||
}
|
||||
|
||||
{
|
||||
let result = Mutation::delete_all_notes(db).await.unwrap();
|
||||
|
||||
assert_eq!(result.rows_affected, 5);
|
||||
}
|
||||
}
|
50
examples/graphql_example/core/tests/prepare.rs
Normal file
50
examples/graphql_example/core/tests/prepare.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use ::entity::note;
|
||||
use sea_orm::*;
|
||||
|
||||
#[cfg(feature = "mock")]
|
||||
pub fn prepare_mock_db() -> DatabaseConnection {
|
||||
MockDatabase::new(DatabaseBackend::Postgres)
|
||||
.append_query_results(vec![
|
||||
vec![note::Model {
|
||||
id: 1,
|
||||
title: "Title A".to_owned(),
|
||||
text: "Text A".to_owned(),
|
||||
}],
|
||||
vec![note::Model {
|
||||
id: 5,
|
||||
title: "Title C".to_owned(),
|
||||
text: "Text C".to_owned(),
|
||||
}],
|
||||
vec![note::Model {
|
||||
id: 6,
|
||||
title: "Title D".to_owned(),
|
||||
text: "Text D".to_owned(),
|
||||
}],
|
||||
vec![note::Model {
|
||||
id: 1,
|
||||
title: "Title A".to_owned(),
|
||||
text: "Text A".to_owned(),
|
||||
}],
|
||||
vec![note::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
}],
|
||||
vec![note::Model {
|
||||
id: 5,
|
||||
title: "Title C".to_owned(),
|
||||
text: "Text C".to_owned(),
|
||||
}],
|
||||
])
|
||||
.append_exec_results(vec![
|
||||
MockExecResult {
|
||||
last_insert_id: 6,
|
||||
rows_affected: 1,
|
||||
},
|
||||
MockExecResult {
|
||||
last_insert_id: 6,
|
||||
rows_affected: 5,
|
||||
},
|
||||
])
|
||||
.into_connection()
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
pub struct Database {
|
||||
pub connection: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub async fn new() -> Self {
|
||||
let connection = sea_orm::Database::connect(std::env::var("DATABASE_URL").unwrap())
|
||||
.await
|
||||
.expect("Could not connect to database");
|
||||
|
||||
Database { connection }
|
||||
}
|
||||
|
||||
pub fn get_connection(&self) -> &DatabaseConnection {
|
||||
&self.connection
|
||||
}
|
||||
}
|
@ -1,49 +1,3 @@
|
||||
mod db;
|
||||
mod graphql;
|
||||
|
||||
use entity::async_graphql;
|
||||
|
||||
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
|
||||
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
|
||||
use axum::{
|
||||
extract::Extension,
|
||||
response::{Html, IntoResponse},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use graphql::schema::{build_schema, AppSchema};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use dotenv::dotenv;
|
||||
|
||||
async fn graphql_handler(schema: Extension<AppSchema>, req: GraphQLRequest) -> GraphQLResponse {
|
||||
schema.execute(req.into_inner()).await.into()
|
||||
}
|
||||
|
||||
async fn graphql_playground() -> impl IntoResponse {
|
||||
Html(playground_source(GraphQLPlaygroundConfig::new(
|
||||
"/api/graphql",
|
||||
)))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
#[cfg(debug_assertions)]
|
||||
dotenv().ok();
|
||||
|
||||
let schema = build_schema().await;
|
||||
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/api/graphql",
|
||||
get(graphql_playground).post(graphql_handler),
|
||||
)
|
||||
.layer(Extension(schema));
|
||||
|
||||
println!("Playground: http://localhost:3000/api/graphql");
|
||||
|
||||
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
fn main() {
|
||||
graphql_example_api::main();
|
||||
}
|
||||
|
@ -6,28 +6,7 @@ publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[workspace]
|
||||
members = [".", "entity", "migration"]
|
||||
members = [".", "api", "core", "entity", "migration"]
|
||||
|
||||
[dependencies]
|
||||
jsonrpsee = { version = "^0.8.0", features = ["full"] }
|
||||
jsonrpsee-core = "0.9.0"
|
||||
tokio = { version = "1.8.0", features = ["full"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
dotenv = "0.15"
|
||||
entity = { path = "entity" }
|
||||
migration = { path = "migration" }
|
||||
anyhow = "1.0.52"
|
||||
async-trait = "0.1.52"
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
simplelog = "*"
|
||||
|
||||
[dependencies.sea-orm]
|
||||
path = "../../" # remove this line in your own project
|
||||
version = "^0.10.0" # sea-orm version
|
||||
features = [
|
||||
"debug-print",
|
||||
"runtime-tokio-native-tls",
|
||||
"sqlx-sqlite",
|
||||
# "sqlx-postgres",
|
||||
# "sqlx-mysql",
|
||||
]
|
||||
jsonrpsee-example-api = { path = "api" }
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
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 `Cargo.toml` (the `"sqlx-sqlite",` line)
|
||||
1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-sqlite",` line)
|
||||
|
||||
1. Execute `cargo run` to start the server
|
||||
|
||||
2. Send jsonrpc request to server
|
||||
1. Send jsonrpc request to server
|
||||
|
||||
```shell
|
||||
#insert
|
||||
@ -20,7 +20,7 @@ curl --location --request POST 'http://127.0.0.1:8000' \
|
||||
}
|
||||
], "id": 2}'
|
||||
|
||||
#list
|
||||
#list
|
||||
curl --location --request POST 'http://127.0.0.1:8000' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
@ -33,7 +33,7 @@ curl --location --request POST 'http://127.0.0.1:8000' \
|
||||
"id": 2
|
||||
}'
|
||||
|
||||
#delete
|
||||
#delete
|
||||
curl --location --request POST 'http://127.0.0.1:8000' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
@ -61,4 +61,11 @@ curl --location --request POST 'http://127.0.0.1:8000' \
|
||||
"id": 2
|
||||
}'
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
Run mock test on the core logic crate:
|
||||
|
||||
```bash
|
||||
cd core
|
||||
cargo test --features mock
|
||||
```
|
||||
|
19
examples/jsonrpsee_example/api/Cargo.toml
Normal file
19
examples/jsonrpsee_example/api/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "jsonrpsee-example-api"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
jsonrpsee-example-core = { path = "../core" }
|
||||
jsonrpsee = { version = "^0.8.0", features = ["full"] }
|
||||
jsonrpsee-core = "0.9.0"
|
||||
tokio = { version = "1.8.0", features = ["full"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
dotenv = "0.15"
|
||||
entity = { path = "../entity" }
|
||||
migration = { path = "../migration" }
|
||||
anyhow = "1.0.52"
|
||||
async-trait = "0.1.52"
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
simplelog = "*"
|
143
examples/jsonrpsee_example/api/src/lib.rs
Normal file
143
examples/jsonrpsee_example/api/src/lib.rs
Normal file
@ -0,0 +1,143 @@
|
||||
use std::env;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use entity::post;
|
||||
use jsonrpsee::core::{async_trait, RpcResult};
|
||||
use jsonrpsee::http_server::HttpServerBuilder;
|
||||
use jsonrpsee::proc_macros::rpc;
|
||||
use jsonrpsee::types::error::CallError;
|
||||
use jsonrpsee_example_core::sea_orm::{Database, DatabaseConnection};
|
||||
use jsonrpsee_example_core::{Mutation, Query};
|
||||
use log::info;
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use simplelog::*;
|
||||
use std::fmt::Display;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::signal::ctrl_c;
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
|
||||
const DEFAULT_POSTS_PER_PAGE: u64 = 5;
|
||||
|
||||
#[rpc(server, client)]
|
||||
trait PostRpc {
|
||||
#[method(name = "Post.List")]
|
||||
async fn list(
|
||||
&self,
|
||||
page: Option<u64>,
|
||||
posts_per_page: Option<u64>,
|
||||
) -> RpcResult<Vec<post::Model>>;
|
||||
|
||||
#[method(name = "Post.Insert")]
|
||||
async fn insert(&self, p: post::Model) -> RpcResult<i32>;
|
||||
|
||||
#[method(name = "Post.Update")]
|
||||
async fn update(&self, p: post::Model) -> RpcResult<bool>;
|
||||
|
||||
#[method(name = "Post.Delete")]
|
||||
async fn delete(&self, id: i32) -> RpcResult<bool>;
|
||||
}
|
||||
|
||||
struct PpcImpl {
|
||||
conn: DatabaseConnection,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PostRpcServer for PpcImpl {
|
||||
async fn list(
|
||||
&self,
|
||||
page: Option<u64>,
|
||||
posts_per_page: Option<u64>,
|
||||
) -> RpcResult<Vec<post::Model>> {
|
||||
let page = page.unwrap_or(1);
|
||||
let posts_per_page = posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE);
|
||||
|
||||
Query::find_posts_in_page(&self.conn, page, posts_per_page)
|
||||
.await
|
||||
.map(|(p, _)| p)
|
||||
.internal_call_error()
|
||||
}
|
||||
|
||||
async fn insert(&self, p: post::Model) -> RpcResult<i32> {
|
||||
let new_post = Mutation::create_post(&self.conn, p)
|
||||
.await
|
||||
.internal_call_error()?;
|
||||
|
||||
Ok(new_post.id.unwrap())
|
||||
}
|
||||
|
||||
async fn update(&self, p: post::Model) -> RpcResult<bool> {
|
||||
Mutation::update_post_by_id(&self.conn, p.id, p)
|
||||
.await
|
||||
.map(|_| true)
|
||||
.internal_call_error()
|
||||
}
|
||||
async fn delete(&self, id: i32) -> RpcResult<bool> {
|
||||
Mutation::delete_post(&self.conn, id)
|
||||
.await
|
||||
.map(|res| res.rows_affected == 1)
|
||||
.internal_call_error()
|
||||
}
|
||||
}
|
||||
|
||||
trait IntoJsonRpcResult<T> {
|
||||
fn internal_call_error(self) -> RpcResult<T>;
|
||||
}
|
||||
|
||||
impl<T, E> IntoJsonRpcResult<T> for Result<T, E>
|
||||
where
|
||||
E: Display,
|
||||
{
|
||||
fn internal_call_error(self) -> RpcResult<T> {
|
||||
self.map_err(|e| jsonrpsee::core::Error::Call(CallError::Failed(anyhow!("{}", e))))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn start() -> std::io::Result<()> {
|
||||
let _ = TermLogger::init(
|
||||
LevelFilter::Trace,
|
||||
Config::default(),
|
||||
TerminalMode::Mixed,
|
||||
ColorChoice::Auto,
|
||||
);
|
||||
|
||||
// get env vars
|
||||
dotenv::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);
|
||||
|
||||
// create post table if not exists
|
||||
let conn = Database::connect(&db_url).await.unwrap();
|
||||
Migrator::up(&conn, None).await.unwrap();
|
||||
|
||||
let server = HttpServerBuilder::default()
|
||||
.build(server_url.parse::<SocketAddr>().unwrap())
|
||||
.unwrap();
|
||||
|
||||
let rpc_impl = PpcImpl { conn };
|
||||
let server_addr = server.local_addr().unwrap();
|
||||
let handle = server.start(rpc_impl.into_rpc()).unwrap();
|
||||
|
||||
info!("starting listening {}", server_addr);
|
||||
let mut sig_int = signal(SignalKind::interrupt()).unwrap();
|
||||
let mut sig_term = signal(SignalKind::terminate()).unwrap();
|
||||
|
||||
tokio::select! {
|
||||
_ = sig_int.recv() => info!("receive SIGINT"),
|
||||
_ = sig_term.recv() => info!("receive SIGTERM"),
|
||||
_ = ctrl_c() => info!("receive Ctrl C"),
|
||||
}
|
||||
handle.stop().unwrap();
|
||||
info!("Shutdown program");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
let result = start();
|
||||
|
||||
if let Some(err) = result.err() {
|
||||
println!("Error: {}", err);
|
||||
}
|
||||
}
|
30
examples/jsonrpsee_example/core/Cargo.toml
Normal file
30
examples/jsonrpsee_example/core/Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "jsonrpsee-example-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
entity = { path = "../entity" }
|
||||
|
||||
[dependencies.sea-orm]
|
||||
path = "../../../" # remove this line in your own project
|
||||
version = "^0.10.0" # sea-orm version
|
||||
features = [
|
||||
"debug-print",
|
||||
"runtime-tokio-native-tls",
|
||||
"sqlx-sqlite",
|
||||
# "sqlx-postgres",
|
||||
# "sqlx-mysql",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.20.0", features = ["macros", "rt"] }
|
||||
|
||||
[features]
|
||||
mock = ["sea-orm/mock"]
|
||||
|
||||
[[test]]
|
||||
name = "mock"
|
||||
required-features = ["mock"]
|
7
examples/jsonrpsee_example/core/src/lib.rs
Normal file
7
examples/jsonrpsee_example/core/src/lib.rs
Normal file
@ -0,0 +1,7 @@
|
||||
mod mutation;
|
||||
mod query;
|
||||
|
||||
pub use mutation::*;
|
||||
pub use query::*;
|
||||
|
||||
pub use sea_orm;
|
53
examples/jsonrpsee_example/core/src/mutation.rs
Normal file
53
examples/jsonrpsee_example/core/src/mutation.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use ::entity::{post, post::Entity as Post};
|
||||
use sea_orm::*;
|
||||
|
||||
pub struct Mutation;
|
||||
|
||||
impl Mutation {
|
||||
pub async fn create_post(
|
||||
db: &DbConn,
|
||||
form_data: post::Model,
|
||||
) -> Result<post::ActiveModel, DbErr> {
|
||||
post::ActiveModel {
|
||||
title: Set(form_data.title.to_owned()),
|
||||
text: Set(form_data.text.to_owned()),
|
||||
..Default::default()
|
||||
}
|
||||
.save(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_post_by_id(
|
||||
db: &DbConn,
|
||||
id: i32,
|
||||
form_data: post::Model,
|
||||
) -> Result<post::Model, DbErr> {
|
||||
let post: post::ActiveModel = Post::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or(DbErr::Custom("Cannot find post.".to_owned()))
|
||||
.map(Into::into)?;
|
||||
|
||||
post::ActiveModel {
|
||||
id: post.id,
|
||||
title: Set(form_data.title.to_owned()),
|
||||
text: Set(form_data.text.to_owned()),
|
||||
}
|
||||
.update(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_post(db: &DbConn, id: i32) -> Result<DeleteResult, DbErr> {
|
||||
let post: post::ActiveModel = Post::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or(DbErr::Custom("Cannot find post.".to_owned()))
|
||||
.map(Into::into)?;
|
||||
|
||||
post.delete(db).await
|
||||
}
|
||||
|
||||
pub async fn delete_all_posts(db: &DbConn) -> Result<DeleteResult, DbErr> {
|
||||
Post::delete_many().exec(db).await
|
||||
}
|
||||
}
|
26
examples/jsonrpsee_example/core/src/query.rs
Normal file
26
examples/jsonrpsee_example/core/src/query.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use ::entity::{post, post::Entity as Post};
|
||||
use sea_orm::*;
|
||||
|
||||
pub struct Query;
|
||||
|
||||
impl Query {
|
||||
pub async fn find_post_by_id(db: &DbConn, id: i32) -> Result<Option<post::Model>, DbErr> {
|
||||
Post::find_by_id(id).one(db).await
|
||||
}
|
||||
|
||||
/// If ok, returns (post models, num pages).
|
||||
pub async fn find_posts_in_page(
|
||||
db: &DbConn,
|
||||
page: u64,
|
||||
posts_per_page: u64,
|
||||
) -> Result<(Vec<post::Model>, u64), DbErr> {
|
||||
// Setup paginator
|
||||
let paginator = Post::find()
|
||||
.order_by_asc(post::Column::Id)
|
||||
.paginate(db, posts_per_page);
|
||||
let num_pages = paginator.num_pages().await?;
|
||||
|
||||
// Fetch paginated posts
|
||||
paginator.fetch_page(page - 1).await.map(|p| (p, num_pages))
|
||||
}
|
||||
}
|
79
examples/jsonrpsee_example/core/tests/mock.rs
Normal file
79
examples/jsonrpsee_example/core/tests/mock.rs
Normal file
@ -0,0 +1,79 @@
|
||||
mod prepare;
|
||||
|
||||
use entity::post;
|
||||
use jsonrpsee_example_core::{Mutation, Query};
|
||||
use prepare::prepare_mock_db;
|
||||
|
||||
#[tokio::test]
|
||||
async fn main() {
|
||||
let db = &prepare_mock_db();
|
||||
|
||||
{
|
||||
let post = Query::find_post_by_id(db, 1).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(post.id, 1);
|
||||
}
|
||||
|
||||
{
|
||||
let post = Query::find_post_by_id(db, 5).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(post.id, 5);
|
||||
}
|
||||
|
||||
{
|
||||
let post = Mutation::create_post(
|
||||
db,
|
||||
post::Model {
|
||||
id: 0,
|
||||
title: "Title D".to_owned(),
|
||||
text: "Text D".to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
post,
|
||||
post::ActiveModel {
|
||||
id: sea_orm::ActiveValue::Unchanged(6),
|
||||
title: sea_orm::ActiveValue::Unchanged("Title D".to_owned()),
|
||||
text: sea_orm::ActiveValue::Unchanged("Text D".to_owned())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let post = Mutation::update_post_by_id(
|
||||
db,
|
||||
1,
|
||||
post::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
post,
|
||||
post::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let result = Mutation::delete_post(db, 5).await.unwrap();
|
||||
|
||||
assert_eq!(result.rows_affected, 1);
|
||||
}
|
||||
|
||||
{
|
||||
let result = Mutation::delete_all_posts(db).await.unwrap();
|
||||
|
||||
assert_eq!(result.rows_affected, 5);
|
||||
}
|
||||
}
|
50
examples/jsonrpsee_example/core/tests/prepare.rs
Normal file
50
examples/jsonrpsee_example/core/tests/prepare.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use ::entity::post;
|
||||
use sea_orm::*;
|
||||
|
||||
#[cfg(feature = "mock")]
|
||||
pub fn prepare_mock_db() -> DatabaseConnection {
|
||||
MockDatabase::new(DatabaseBackend::Postgres)
|
||||
.append_query_results(vec![
|
||||
vec![post::Model {
|
||||
id: 1,
|
||||
title: "Title A".to_owned(),
|
||||
text: "Text A".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 5,
|
||||
title: "Title C".to_owned(),
|
||||
text: "Text C".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 6,
|
||||
title: "Title D".to_owned(),
|
||||
text: "Text D".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 1,
|
||||
title: "Title A".to_owned(),
|
||||
text: "Text A".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 1,
|
||||
title: "New Title A".to_owned(),
|
||||
text: "New Text A".to_owned(),
|
||||
}],
|
||||
vec![post::Model {
|
||||
id: 5,
|
||||
title: "Title C".to_owned(),
|
||||
text: "Text C".to_owned(),
|
||||
}],
|
||||
])
|
||||
.append_exec_results(vec![
|
||||
MockExecResult {
|
||||
last_insert_id: 6,
|
||||
rows_affected: 1,
|
||||
},
|
||||
MockExecResult {
|
||||
last_insert_id: 6,
|
||||
rows_affected: 5,
|
||||
},
|
||||
])
|
||||
.into_connection()
|
||||
}
|
@ -1,148 +1,3 @@
|
||||
use std::env;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use entity::post;
|
||||
use jsonrpsee::core::{async_trait, RpcResult};
|
||||
use jsonrpsee::http_server::HttpServerBuilder;
|
||||
use jsonrpsee::proc_macros::rpc;
|
||||
use jsonrpsee::types::error::CallError;
|
||||
use log::info;
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use sea_orm::NotSet;
|
||||
use sea_orm::{entity::*, query::*, DatabaseConnection};
|
||||
use simplelog::*;
|
||||
use std::fmt::Display;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::signal::ctrl_c;
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
|
||||
const DEFAULT_POSTS_PER_PAGE: u64 = 5;
|
||||
|
||||
#[rpc(server, client)]
|
||||
pub trait PostRpc {
|
||||
#[method(name = "Post.List")]
|
||||
async fn list(
|
||||
&self,
|
||||
page: Option<u64>,
|
||||
posts_per_page: Option<u64>,
|
||||
) -> RpcResult<Vec<post::Model>>;
|
||||
|
||||
#[method(name = "Post.Insert")]
|
||||
async fn insert(&self, p: post::Model) -> RpcResult<i32>;
|
||||
|
||||
#[method(name = "Post.Update")]
|
||||
async fn update(&self, p: post::Model) -> RpcResult<bool>;
|
||||
|
||||
#[method(name = "Post.Delete")]
|
||||
async fn delete(&self, id: i32) -> RpcResult<bool>;
|
||||
}
|
||||
|
||||
pub struct PpcImpl {
|
||||
conn: DatabaseConnection,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PostRpcServer for PpcImpl {
|
||||
async fn list(
|
||||
&self,
|
||||
page: Option<u64>,
|
||||
posts_per_page: Option<u64>,
|
||||
) -> RpcResult<Vec<post::Model>> {
|
||||
let page = page.unwrap_or(1);
|
||||
let posts_per_page = posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE);
|
||||
let paginator = post::Entity::find()
|
||||
.order_by_asc(post::Column::Id)
|
||||
.paginate(&self.conn, posts_per_page);
|
||||
paginator.fetch_page(page - 1).await.internal_call_error()
|
||||
}
|
||||
|
||||
async fn insert(&self, p: post::Model) -> RpcResult<i32> {
|
||||
let active_post = post::ActiveModel {
|
||||
id: NotSet,
|
||||
title: Set(p.title),
|
||||
text: Set(p.text),
|
||||
};
|
||||
let new_post = active_post.insert(&self.conn).await.internal_call_error()?;
|
||||
Ok(new_post.id)
|
||||
}
|
||||
|
||||
async fn update(&self, p: post::Model) -> RpcResult<bool> {
|
||||
let update_post = post::ActiveModel {
|
||||
id: Set(p.id),
|
||||
title: Set(p.title),
|
||||
text: Set(p.text),
|
||||
};
|
||||
update_post
|
||||
.update(&self.conn)
|
||||
.await
|
||||
.map(|_| true)
|
||||
.internal_call_error()
|
||||
}
|
||||
async fn delete(&self, id: i32) -> RpcResult<bool> {
|
||||
let post = post::Entity::find_by_id(id)
|
||||
.one(&self.conn)
|
||||
.await
|
||||
.internal_call_error()?;
|
||||
|
||||
post.unwrap()
|
||||
.delete(&self.conn)
|
||||
.await
|
||||
.map(|res| res.rows_affected == 1)
|
||||
.internal_call_error()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoJsonRpcResult<T> {
|
||||
fn internal_call_error(self) -> RpcResult<T>;
|
||||
}
|
||||
|
||||
impl<T, E> IntoJsonRpcResult<T> for Result<T, E>
|
||||
where
|
||||
E: Display,
|
||||
{
|
||||
fn internal_call_error(self) -> RpcResult<T> {
|
||||
self.map_err(|e| jsonrpsee::core::Error::Call(CallError::Failed(anyhow!("{}", e))))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let _ = TermLogger::init(
|
||||
LevelFilter::Trace,
|
||||
Config::default(),
|
||||
TerminalMode::Mixed,
|
||||
ColorChoice::Auto,
|
||||
);
|
||||
|
||||
// get env vars
|
||||
dotenv::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);
|
||||
|
||||
// create post table if not exists
|
||||
let conn = sea_orm::Database::connect(&db_url).await.unwrap();
|
||||
Migrator::up(&conn, None).await.unwrap();
|
||||
|
||||
let server = HttpServerBuilder::default()
|
||||
.build(server_url.parse::<SocketAddr>().unwrap())
|
||||
.unwrap();
|
||||
|
||||
let rpc_impl = PpcImpl { conn };
|
||||
let server_addr = server.local_addr().unwrap();
|
||||
let handle = server.start(rpc_impl.into_rpc()).unwrap();
|
||||
|
||||
info!("starting listening {}", server_addr);
|
||||
let mut sig_int = signal(SignalKind::interrupt()).unwrap();
|
||||
let mut sig_term = signal(SignalKind::terminate()).unwrap();
|
||||
|
||||
tokio::select! {
|
||||
_ = sig_int.recv() => info!("receive SIGINT"),
|
||||
_ = sig_term.recv() => info!("receive SIGTERM"),
|
||||
_ = ctrl_c() => info!("receive Ctrl C"),
|
||||
}
|
||||
handle.stop().unwrap();
|
||||
info!("Shutdown program");
|
||||
Ok(())
|
||||
fn main() {
|
||||
jsonrpsee_example_api::main();
|
||||
}
|
||||
|
@ -5,25 +5,7 @@ edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[workspace]
|
||||
members = [".", "entity", "migration"]
|
||||
members = [".", "api", "core", "entity", "migration"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] }
|
||||
poem = { version = "1.2.33", features = ["static-files"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tera = "1.8.0"
|
||||
dotenv = "0.15"
|
||||
entity = { path = "entity" }
|
||||
migration = { path = "migration" }
|
||||
|
||||
[dependencies.sea-orm]
|
||||
path = "../../" # remove this line in your own project
|
||||
version = "^0.10.0" # sea-orm version
|
||||
features = [
|
||||
"debug-print",
|
||||
"runtime-tokio-native-tls",
|
||||
"sqlx-sqlite",
|
||||
# "sqlx-postgres",
|
||||
# "sqlx-mysql",
|
||||
]
|
||||
poem-example-api = { path = "api" }
|
||||
|
@ -4,8 +4,15 @@
|
||||
|
||||
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 `Cargo.toml` (the `"sqlx-sqlite",` line)
|
||||
1. Turn on the appropriate database feature for your chosen db in `core/Cargo.toml` (the `"sqlx-sqlite",` line)
|
||||
|
||||
1. Execute `cargo run` to start the server
|
||||
|
||||
1. Visit [localhost:8000](http://localhost:8000) in browser after seeing the `server started` line
|
||||
|
||||
Run mock test on the core logic crate:
|
||||
|
||||
```bash
|
||||
cd core
|
||||
cargo test --features mock
|
||||
```
|
||||
|
15
examples/poem_example/api/Cargo.toml
Normal file
15
examples/poem_example/api/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "poem-example-api"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
poem-example-core = { path = "../core" }
|
||||
tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] }
|
||||
poem = { version = "1.2.33", features = ["static-files"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tera = "1.8.0"
|
||||
dotenv = "0.15"
|
||||
entity = { path = "../entity" }
|
||||
migration = { path = "../migration" }
|
163
examples/poem_example/api/src/lib.rs
Normal file
163
examples/poem_example/api/src/lib.rs
Normal file
@ -0,0 +1,163 @@
|
||||
use std::env;
|
||||
|
||||
use entity::post;
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use poem::endpoint::StaticFilesEndpoint;
|
||||
use poem::error::InternalServerError;
|
||||
use poem::http::StatusCode;
|
||||
use poem::listener::TcpListener;
|
||||
use poem::web::{Data, Form, Html, Path, Query};
|
||||
use poem::{get, handler, post, EndpointExt, Error, IntoResponse, Result, Route, Server};
|
||||
use poem_example_core::{
|
||||
sea_orm::{Database, DatabaseConnection},
|
||||
Mutation as MutationCore, Query as QueryCore,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tera::Tera;
|
||||
|
||||
const DEFAULT_POSTS_PER_PAGE: u64 = 5;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AppState {
|
||||
templates: tera::Tera,
|
||||
conn: DatabaseConnection,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Params {
|
||||
page: Option<u64>,
|
||||
posts_per_page: Option<u64>,
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn create(state: Data<&AppState>, form: Form<post::Model>) -> Result<impl IntoResponse> {
|
||||
let form = form.0;
|
||||
let conn = &state.conn;
|
||||
|
||||
MutationCore::create_post(conn, form)
|
||||
.await
|
||||
.map_err(InternalServerError)?;
|
||||
|
||||
Ok(StatusCode::FOUND.with_header("location", "/"))
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn list(state: Data<&AppState>, Query(params): Query<Params>) -> Result<impl IntoResponse> {
|
||||
let conn = &state.conn;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE);
|
||||
|
||||
let (posts, num_pages) = QueryCore::find_posts_in_page(conn, page, posts_per_page)
|
||||
.await
|
||||
.map_err(InternalServerError)?;
|
||||
|
||||
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);
|
||||
|
||||
let body = state
|
||||
.templates
|
||||
.render("index.html.tera", &ctx)
|
||||
.map_err(InternalServerError)?;
|
||||
Ok(Html(body))
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn new(state: Data<&AppState>) -> Result<impl IntoResponse> {
|
||||
let ctx = tera::Context::new();
|
||||
let body = state
|
||||
.templates
|
||||
.render("new.html.tera", &ctx)
|
||||
.map_err(InternalServerError)?;
|
||||
Ok(Html(body))
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn edit(state: Data<&AppState>, Path(id): Path<i32>) -> Result<impl IntoResponse> {
|
||||
let conn = &state.conn;
|
||||
|
||||
let post: post::Model = QueryCore::find_post_by_id(conn, id)
|
||||
.await
|
||||
.map_err(InternalServerError)?
|
||||
.ok_or_else(|| Error::from_status(StatusCode::NOT_FOUND))?;
|
||||
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("post", &post);
|
||||
|
||||
let body = state
|
||||
.templates
|
||||
.render("edit.html.tera", &ctx)
|
||||
.map_err(InternalServerError)?;
|
||||
Ok(Html(body))
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn update(
|
||||
state: Data<&AppState>,
|
||||
Path(id): Path<i32>,
|
||||
form: Form<post::Model>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let conn = &state.conn;
|
||||
let form = form.0;
|
||||
|
||||
MutationCore::update_post_by_id(conn, id, form)
|
||||
.await
|
||||
.map_err(InternalServerError)?;
|
||||
|
||||
Ok(StatusCode::FOUND.with_header("location", "/"))
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn delete(state: Data<&AppState>, Path(id): Path<i32>) -> Result<impl IntoResponse> {
|
||||
let conn = &state.conn;
|
||||
|
||||
MutationCore::delete_post(conn, id)
|
||||
.await
|
||||
.map_err(InternalServerError)?;
|
||||
|
||||
Ok(StatusCode::FOUND.with_header("location", "/"))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn start() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "debug");
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// get env vars
|
||||
dotenv::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);
|
||||
|
||||
// create post table if not exists
|
||||
let conn = Database::connect(&db_url).await.unwrap();
|
||||
Migrator::up(&conn, None).await.unwrap();
|
||||
let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();
|
||||
let state = AppState { templates, conn };
|
||||
|
||||
println!("Starting server at {}", server_url);
|
||||
|
||||
let app = Route::new()
|
||||
.at("/", post(create).get(list))
|
||||
.at("/new", new)
|
||||
.at("/:id", get(edit).post(update))
|
||||
.at("/delete/:id", post(delete))
|
||||
.nest(
|
||||
"/static",
|
||||
StaticFilesEndpoint::new(concat!(env!("CARGO_MANIFEST_DIR"), "/static")),
|
||||
)
|
||||
.data(state);
|
||||
let server = Server::new(TcpListener::bind(format!("{}:{}", host, port)));
|
||||
server.run(app).await
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
let result = start();
|
||||
|
||||
if let Some(err) = result.err() {
|
||||
println!("Error: {}", err);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user