Added axum graphql example (#587)

* added example for axum + graphql

* clean up

* removed macos file

* Pr/587 (#1)

* Migrate on startup

* Update CI

* Add .gitignore

* Add README

Co-authored-by: Billy Chan <ccw.billy.123@gmail.com>

Co-authored-by: Billy Chan <ccw.billy.123@gmail.com>
This commit is contained in:
Aaron Leopold 2022-03-16 03:16:19 -07:00 committed by GitHub
parent af235168db
commit 7ba6144ead
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 446 additions and 1 deletions

View File

@ -293,7 +293,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
path: [basic, actix_example, actix4_example, axum_example, rocket_example, poem_example]
path: [basic, actix_example, actix4_example, axum_example, axum-graphql_example, rocket_example, poem_example]
steps:
- uses: actions/checkout@v2

View File

@ -0,0 +1 @@
DATABASE_URL=sqlite:./db?mode=rwc

View File

@ -0,0 +1,3 @@
db
db-shm
db-wal

View File

@ -0,0 +1,17 @@
[package]
name = "axum-graphql"
authors = ["Aaron Leopold <aaronleopold1221@gmail.com>"]
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[workspace]
members = [".", "entity", "migration"]
[dependencies]
tokio = { version = "1.0", features = ["full"] }
axum = "0.4.8"
dotenv = "0.15.0"
async-graphql-axum = "3.0.31"
entity = { path = "entity" }
migration = { path = "migration" }

View File

@ -0,0 +1,13 @@
![screenshot](Screenshot1.png)
![screenshot](Screenshot2.png)
# Axum-GraphQL with SeaORM example app
1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database
1. Turn on the appropriate database feature for your chosen db in `entity/Cargo.toml` (the `"sqlx-sqlite",` line)
1. Execute `cargo run` to start the server
1. Visit [localhost:3000/api/graphql](http://localhost:3000/api/graphql) in browser

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

View File

@ -0,0 +1,26 @@
[package]
name = "entity"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "entity"
path = "src/lib.rs"
[dependencies]
serde = { version = "1", features = ["derive"] }
[dependencies.async-graphql]
version = "3.0.12"
[dependencies.sea-orm]
version = "^0.6.0"
features = [
"macros",
"runtime-tokio-native-tls",
# "sqlx-postgres",
# "sqlx-mysql",
"sqlx-sqlite"
]
default-features = false

View File

@ -0,0 +1,4 @@
pub mod note;
pub use async_graphql;
pub use sea_orm;

View File

@ -0,0 +1,39 @@
use async_graphql::*;
use sea_orm::{entity::prelude::*, DeleteMany};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, SimpleObject)]
#[sea_orm(table_name = "notes")]
#[graphql(concrete(name = "Note", params()))]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: i32,
pub title: String,
pub text: String,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}
impl Entity {
pub fn find_by_id(id: i32) -> Select<Entity> {
Self::find().filter(Column::Id.eq(id))
}
pub fn find_by_title(title: &str) -> Select<Entity> {
Self::find().filter(Column::Title.eq(title))
}
pub fn delete_by_id(id: i32) -> DeleteMany<Entity> {
Self::delete_many().filter(Column::Id.eq(id))
}
}

View File

@ -0,0 +1,14 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
sea-schema = { version = "0.5.0", default-features = false, features = [ "migration", "debug-print" ] }
dotenv = "0.15.0"
entity = { path = "../entity" }

View File

@ -0,0 +1,37 @@
# Running Migrator CLI
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

View File

@ -0,0 +1,12 @@
pub use sea_schema::migration::*;
mod m20220101_000001_create_table;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_create_table::Migration)]
}
}

View File

@ -0,0 +1,52 @@
use entity::{
note,
sea_orm::{DbBackend, EntityTrait, Schema},
};
use sea_schema::migration::{
sea_query::{self, *},
*,
};
pub struct Migration;
fn get_seaorm_create_stmt<E: EntityTrait>(e: E) -> TableCreateStatement {
let schema = Schema::new(DbBackend::Sqlite);
schema
.create_table_from_entity(e)
.if_not_exists()
.to_owned()
}
fn get_seaorm_drop_stmt<E: EntityTrait>(e: E) -> TableDropStatement {
Table::drop().table(e).if_exists().to_owned()
}
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20220101_000001_create_table"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let stmts = vec![get_seaorm_create_stmt(note::Entity)];
for stmt in stmts {
manager.create_table(stmt.to_owned()).await?;
}
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let stmts = vec![get_seaorm_drop_stmt(note::Entity)];
for stmt in stmts {
manager.drop_table(stmt.to_owned()).await?;
}
Ok(())
}
}

View File

@ -0,0 +1,26 @@
use migration::Migrator;
use sea_schema::migration::*;
use std::path::PathBuf;
#[cfg(debug_assertions)]
use dotenv::dotenv;
#[async_std::main]
async fn main() {
#[cfg(debug_assertions)]
dotenv().ok();
let fallback = "sqlite:./db?mode=rwc";
match std::env::var("DATABASE_URL") {
Ok(val) => {
println!("Using DATABASE_URL: {}", val);
}
Err(_) => {
std::env::set_var("DATABASE_URL", fallback);
println!("Set DATABASE_URL: {}", fallback);
}
};
cli::run_cli(Migrator).await;
}

View File

@ -0,0 +1,20 @@
use entity::sea_orm;
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
}
}

View File

@ -0,0 +1,3 @@
pub mod mutation;
pub mod query;
pub mod schema;

View File

@ -0,0 +1,10 @@
use entity::async_graphql;
pub mod note;
pub use note::NoteMutation;
// Add your other ones here to create a unified Mutation object
// e.x. Mutation(NoteMutation, OtherMutation, OtherOtherMutation)
#[derive(async_graphql::MergedObject, Default)]
pub struct Mutation(NoteMutation);

View File

@ -0,0 +1,60 @@
use async_graphql::{Context, Object, Result};
use entity::async_graphql::{self, InputObject, SimpleObject};
use entity::note;
use entity::sea_orm::{ActiveModelTrait, Set};
use crate::db::Database;
// I normally separate the input types into separate files/modules, but this is just
// a quick example.
#[derive(InputObject)]
pub struct CreateNoteInput {
pub title: String,
pub text: String,
}
#[derive(SimpleObject)]
pub struct DeleteResult {
pub success: bool,
pub rows_affected: u64,
}
#[derive(Default)]
pub struct NoteMutation;
#[Object]
impl NoteMutation {
pub async fn create_note(
&self,
ctx: &Context<'_>,
input: CreateNoteInput,
) -> Result<note::Model> {
let db = ctx.data::<Database>().unwrap();
let note = note::ActiveModel {
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> {
let db = ctx.data::<Database>().unwrap();
let res = note::Entity::delete_by_id(id)
.exec(db.get_connection())
.await?;
if res.rows_affected <= 1 {
Ok(DeleteResult {
success: true,
rows_affected: res.rows_affected,
})
} else {
unimplemented!()
}
}
}

View File

@ -0,0 +1,10 @@
use entity::async_graphql;
pub mod note;
pub use note::NoteQuery;
// Add your other ones here to create a unified Query object
// e.x. Query(NoteQuery, OtherQuery, OtherOtherQuery)
#[derive(async_graphql::MergedObject, Default)]
pub struct Query(NoteQuery);

View File

@ -0,0 +1,28 @@
use async_graphql::{Context, Object, Result};
use entity::{async_graphql, note, sea_orm::EntityTrait};
use crate::db::Database;
#[derive(Default)]
pub struct NoteQuery;
#[Object]
impl NoteQuery {
async fn get_notes(&self, ctx: &Context<'_>) -> Result<Vec<note::Model>> {
let db = ctx.data::<Database>().unwrap();
Ok(note::Entity::find()
.all(db.get_connection())
.await
.map_err(|e| e.to_string())?)
}
async fn get_note_by_id(&self, ctx: &Context<'_>, id: i32) -> Result<Option<note::Model>> {
let db = ctx.data::<Database>().unwrap();
Ok(note::Entity::find_by_id(id)
.one(db.get_connection())
.await
.map_err(|e| e.to_string())?)
}
}

View File

@ -0,0 +1,21 @@
use async_graphql::{EmptySubscription, Schema};
use entity::async_graphql;
use migration::{Migrator, MigratorTrait};
use crate::{
db::Database,
graphql::{mutation::Mutation, query::Query},
};
pub type AppSchema = Schema<Query, Mutation, EmptySubscription>;
/// Builds the GraphQL Schema, attaching the Database to the context
pub async fn build_schema() -> AppSchema {
let db = Database::new().await;
Migrator::up(db.get_connection(), None).await.unwrap();
Schema::build(Query::default(), Mutation::default(), EmptySubscription)
.data(db)
.finish()
}

View 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]
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();
}