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: >
|
args: >
|
||||||
--manifest-path ${{ matrix.path }}
|
--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
|
- name: check rustfmt
|
||||||
run: |
|
run: |
|
||||||
rustup override set nightly
|
rustup override set nightly
|
||||||
|
@ -6,30 +6,7 @@ edition = "2021"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "entity", "migration"]
|
members = [".", "api", "core", "entity", "migration"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-http = "2"
|
actix3-example-api = { path = "api" }
|
||||||
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",
|
|
||||||
]
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database
|
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
|
1. Execute `cargo run` to start the server
|
||||||
|
|
||||||
@ -18,3 +18,10 @@ Run server with auto-reloading:
|
|||||||
cargo install systemfd
|
cargo install systemfd
|
||||||
systemfd --no-pid -s http::8000 -- cargo watch -x run
|
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;
|
fn main() {
|
||||||
use actix_web::{
|
actix3_example_api::main();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
@ -6,30 +6,7 @@ edition = "2021"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "entity", "migration"]
|
members = [".", "api", "core", "entity", "migration"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-files = "0.6"
|
actix-example-api = { path = "api" }
|
||||||
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",
|
|
||||||
]
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database
|
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
|
1. Execute `cargo run` to start the server
|
||||||
|
|
||||||
@ -16,3 +16,10 @@ Run server with auto-reloading:
|
|||||||
cargo install systemfd cargo-watch
|
cargo install systemfd cargo-watch
|
||||||
systemfd --no-pid -s http::8000 -- cargo watch -x run
|
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;
|
fn main() {
|
||||||
use actix_web::{
|
actix_example_api::main();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
@ -5,32 +5,8 @@ authors = ["Yoshiera Huang <huangjasper@126.com>"]
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "entity", "migration"]
|
members = [".", "api", "core", "entity", "migration"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.18.1", features = ["full"] }
|
axum-example-api = { path = "api" }
|
||||||
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",
|
|
||||||
]
|
|
||||||
|
@ -4,8 +4,15 @@
|
|||||||
|
|
||||||
1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database
|
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. Execute `cargo run` to start the server
|
||||||
|
|
||||||
1. Visit [localhost:8000](http://localhost:8000) in browser
|
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;
|
fn main() {
|
||||||
|
axum_example_api::main();
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
@ -7,22 +7,7 @@ publish = false
|
|||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "entity", "migration"]
|
members = [".", "api", "core", "entity", "migration"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
graphql-example-api = { path = "api" }
|
||||||
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"
|
|
||||||
]
|
|
||||||
|
@ -6,8 +6,15 @@
|
|||||||
|
|
||||||
1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database
|
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. Execute `cargo run` to start the server
|
||||||
|
|
||||||
1. Visit [localhost:3000/api/graphql](http://localhost:3000/api/graphql) in browser
|
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 async_graphql::{Context, Object, Result};
|
||||||
use entity::async_graphql::{self, InputObject, SimpleObject};
|
use entity::async_graphql::{self, InputObject, SimpleObject};
|
||||||
use entity::note;
|
use entity::note;
|
||||||
use sea_orm::{ActiveModelTrait, Set};
|
use graphql_example_core::Mutation;
|
||||||
|
|
||||||
use crate::db::Database;
|
use crate::db::Database;
|
||||||
|
|
||||||
@ -14,6 +14,16 @@ pub struct CreateNoteInput {
|
|||||||
pub text: String,
|
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)]
|
#[derive(SimpleObject)]
|
||||||
pub struct DeleteResult {
|
pub struct DeleteResult {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
@ -31,22 +41,18 @@ impl NoteMutation {
|
|||||||
input: CreateNoteInput,
|
input: CreateNoteInput,
|
||||||
) -> Result<note::Model> {
|
) -> Result<note::Model> {
|
||||||
let db = ctx.data::<Database>().unwrap();
|
let db = ctx.data::<Database>().unwrap();
|
||||||
|
let conn = db.get_connection();
|
||||||
|
|
||||||
let note = note::ActiveModel {
|
Ok(Mutation::create_note(conn, input.into_model_with_arbitrary_id()).await?)
|
||||||
title: Set(input.title),
|
|
||||||
text: Set(input.text),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(note.insert(db.get_connection()).await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_note(&self, ctx: &Context<'_>, id: i32) -> Result<DeleteResult> {
|
pub async fn delete_note(&self, ctx: &Context<'_>, id: i32) -> Result<DeleteResult> {
|
||||||
let db = ctx.data::<Database>().unwrap();
|
let db = ctx.data::<Database>().unwrap();
|
||||||
|
let conn = db.get_connection();
|
||||||
|
|
||||||
let res = note::Entity::delete_by_id(id)
|
let res = Mutation::delete_note(conn, id)
|
||||||
.exec(db.get_connection())
|
.await
|
||||||
.await?;
|
.expect("Cannot delete note");
|
||||||
|
|
||||||
if res.rows_affected <= 1 {
|
if res.rows_affected <= 1 {
|
||||||
Ok(DeleteResult {
|
Ok(DeleteResult {
|
@ -1,6 +1,6 @@
|
|||||||
use async_graphql::{Context, Object, Result};
|
use async_graphql::{Context, Object, Result};
|
||||||
use entity::{async_graphql, note};
|
use entity::{async_graphql, note};
|
||||||
use sea_orm::EntityTrait;
|
use graphql_example_core::Query;
|
||||||
|
|
||||||
use crate::db::Database;
|
use crate::db::Database;
|
||||||
|
|
||||||
@ -11,18 +11,18 @@ pub struct NoteQuery;
|
|||||||
impl NoteQuery {
|
impl NoteQuery {
|
||||||
async fn get_notes(&self, ctx: &Context<'_>) -> Result<Vec<note::Model>> {
|
async fn get_notes(&self, ctx: &Context<'_>) -> Result<Vec<note::Model>> {
|
||||||
let db = ctx.data::<Database>().unwrap();
|
let db = ctx.data::<Database>().unwrap();
|
||||||
|
let conn = db.get_connection();
|
||||||
|
|
||||||
Ok(note::Entity::find()
|
Ok(Query::get_all_notes(conn)
|
||||||
.all(db.get_connection())
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?)
|
.map_err(|e| e.to_string())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_note_by_id(&self, ctx: &Context<'_>, id: i32) -> Result<Option<note::Model>> {
|
async fn get_note_by_id(&self, ctx: &Context<'_>, id: i32) -> Result<Option<note::Model>> {
|
||||||
let db = ctx.data::<Database>().unwrap();
|
let db = ctx.data::<Database>().unwrap();
|
||||||
|
let conn = db.get_connection();
|
||||||
|
|
||||||
Ok(note::Entity::find_by_id(id)
|
Ok(Query::find_note_by_id(conn, id)
|
||||||
.one(db.get_connection())
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?)
|
.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;
|
fn main() {
|
||||||
mod graphql;
|
graphql_example_api::main();
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
@ -6,28 +6,7 @@ publish = false
|
|||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "entity", "migration"]
|
members = [".", "api", "core", "entity", "migration"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
jsonrpsee = { version = "^0.8.0", features = ["full"] }
|
jsonrpsee-example-api = { path = "api" }
|
||||||
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",
|
|
||||||
]
|
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database
|
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. Execute `cargo run` to start the server
|
||||||
|
|
||||||
2. Send jsonrpc request to server
|
1. Send jsonrpc request to server
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
#insert
|
#insert
|
||||||
@ -62,3 +62,10 @@ curl --location --request POST 'http://127.0.0.1:8000' \
|
|||||||
}'
|
}'
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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;
|
fn main() {
|
||||||
|
jsonrpsee_example_api::main();
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
|
@ -5,25 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "entity", "migration"]
|
members = [".", "api", "core", "entity", "migration"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] }
|
poem-example-api = { path = "api" }
|
||||||
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",
|
|
||||||
]
|
|
||||||
|
@ -4,8 +4,15 @@
|
|||||||
|
|
||||||
1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database
|
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. Execute `cargo run` to start the server
|
||||||
|
|
||||||
1. Visit [localhost:8000](http://localhost:8000) in browser after seeing the `server started` line
|
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