diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fbeeb88e9..c865b0976 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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 diff --git a/examples/axum-graphql_example/.env b/examples/axum-graphql_example/.env new file mode 100644 index 000000000..f1cc324cd --- /dev/null +++ b/examples/axum-graphql_example/.env @@ -0,0 +1 @@ +DATABASE_URL=sqlite:./db?mode=rwc \ No newline at end of file diff --git a/examples/axum-graphql_example/.gitignore b/examples/axum-graphql_example/.gitignore new file mode 100644 index 000000000..8503cc4cd --- /dev/null +++ b/examples/axum-graphql_example/.gitignore @@ -0,0 +1,3 @@ +db +db-shm +db-wal \ No newline at end of file diff --git a/examples/axum-graphql_example/Cargo.toml b/examples/axum-graphql_example/Cargo.toml new file mode 100644 index 000000000..6f8dd51c1 --- /dev/null +++ b/examples/axum-graphql_example/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "axum-graphql" +authors = ["Aaron Leopold "] +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" } diff --git a/examples/axum-graphql_example/README.md b/examples/axum-graphql_example/README.md new file mode 100644 index 000000000..9111c7cc1 --- /dev/null +++ b/examples/axum-graphql_example/README.md @@ -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 diff --git a/examples/axum-graphql_example/Screenshot1.png b/examples/axum-graphql_example/Screenshot1.png new file mode 100644 index 000000000..d2d81dbb2 Binary files /dev/null and b/examples/axum-graphql_example/Screenshot1.png differ diff --git a/examples/axum-graphql_example/Screenshot2.png b/examples/axum-graphql_example/Screenshot2.png new file mode 100644 index 000000000..171199c5b Binary files /dev/null and b/examples/axum-graphql_example/Screenshot2.png differ diff --git a/examples/axum-graphql_example/entity/Cargo.toml b/examples/axum-graphql_example/entity/Cargo.toml new file mode 100644 index 000000000..eecccb61b --- /dev/null +++ b/examples/axum-graphql_example/entity/Cargo.toml @@ -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 \ No newline at end of file diff --git a/examples/axum-graphql_example/entity/src/lib.rs b/examples/axum-graphql_example/entity/src/lib.rs new file mode 100644 index 000000000..e9cf63e58 --- /dev/null +++ b/examples/axum-graphql_example/entity/src/lib.rs @@ -0,0 +1,4 @@ +pub mod note; + +pub use async_graphql; +pub use sea_orm; diff --git a/examples/axum-graphql_example/entity/src/note.rs b/examples/axum-graphql_example/entity/src/note.rs new file mode 100644 index 000000000..d1d30750b --- /dev/null +++ b/examples/axum-graphql_example/entity/src/note.rs @@ -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 { + Self::find().filter(Column::Id.eq(id)) + } + + pub fn find_by_title(title: &str) -> Select { + Self::find().filter(Column::Title.eq(title)) + } + + pub fn delete_by_id(id: i32) -> DeleteMany { + Self::delete_many().filter(Column::Id.eq(id)) + } +} diff --git a/examples/axum-graphql_example/migration/Cargo.toml b/examples/axum-graphql_example/migration/Cargo.toml new file mode 100644 index 000000000..196deacc1 --- /dev/null +++ b/examples/axum-graphql_example/migration/Cargo.toml @@ -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" } \ No newline at end of file diff --git a/examples/axum-graphql_example/migration/README.md b/examples/axum-graphql_example/migration/README.md new file mode 100644 index 000000000..963caaeb6 --- /dev/null +++ b/examples/axum-graphql_example/migration/README.md @@ -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 + ``` diff --git a/examples/axum-graphql_example/migration/src/lib.rs b/examples/axum-graphql_example/migration/src/lib.rs new file mode 100644 index 000000000..339d693bf --- /dev/null +++ b/examples/axum-graphql_example/migration/src/lib.rs @@ -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> { + vec![Box::new(m20220101_000001_create_table::Migration)] + } +} diff --git a/examples/axum-graphql_example/migration/src/m20220101_000001_create_table.rs b/examples/axum-graphql_example/migration/src/m20220101_000001_create_table.rs new file mode 100644 index 000000000..4f2147e94 --- /dev/null +++ b/examples/axum-graphql_example/migration/src/m20220101_000001_create_table.rs @@ -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: 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: 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(()) + } +} diff --git a/examples/axum-graphql_example/migration/src/main.rs b/examples/axum-graphql_example/migration/src/main.rs new file mode 100644 index 000000000..5a5548f80 --- /dev/null +++ b/examples/axum-graphql_example/migration/src/main.rs @@ -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; +} diff --git a/examples/axum-graphql_example/src/db.rs b/examples/axum-graphql_example/src/db.rs new file mode 100644 index 000000000..da323597d --- /dev/null +++ b/examples/axum-graphql_example/src/db.rs @@ -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 + } +} diff --git a/examples/axum-graphql_example/src/graphql/mod.rs b/examples/axum-graphql_example/src/graphql/mod.rs new file mode 100644 index 000000000..c9d58394f --- /dev/null +++ b/examples/axum-graphql_example/src/graphql/mod.rs @@ -0,0 +1,3 @@ +pub mod mutation; +pub mod query; +pub mod schema; diff --git a/examples/axum-graphql_example/src/graphql/mutation/mod.rs b/examples/axum-graphql_example/src/graphql/mutation/mod.rs new file mode 100644 index 000000000..fb3c483ab --- /dev/null +++ b/examples/axum-graphql_example/src/graphql/mutation/mod.rs @@ -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); diff --git a/examples/axum-graphql_example/src/graphql/mutation/note.rs b/examples/axum-graphql_example/src/graphql/mutation/note.rs new file mode 100644 index 000000000..0ead996f5 --- /dev/null +++ b/examples/axum-graphql_example/src/graphql/mutation/note.rs @@ -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 { + let db = ctx.data::().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 { + let db = ctx.data::().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!() + } + } +} diff --git a/examples/axum-graphql_example/src/graphql/query/mod.rs b/examples/axum-graphql_example/src/graphql/query/mod.rs new file mode 100644 index 000000000..b193647ab --- /dev/null +++ b/examples/axum-graphql_example/src/graphql/query/mod.rs @@ -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); diff --git a/examples/axum-graphql_example/src/graphql/query/note.rs b/examples/axum-graphql_example/src/graphql/query/note.rs new file mode 100644 index 000000000..1ab1e760e --- /dev/null +++ b/examples/axum-graphql_example/src/graphql/query/note.rs @@ -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> { + let db = ctx.data::().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> { + let db = ctx.data::().unwrap(); + + Ok(note::Entity::find_by_id(id) + .one(db.get_connection()) + .await + .map_err(|e| e.to_string())?) + } +} diff --git a/examples/axum-graphql_example/src/graphql/schema.rs b/examples/axum-graphql_example/src/graphql/schema.rs new file mode 100644 index 000000000..6224c409e --- /dev/null +++ b/examples/axum-graphql_example/src/graphql/schema.rs @@ -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; + +/// 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() +} diff --git a/examples/axum-graphql_example/src/main.rs b/examples/axum-graphql_example/src/main.rs new file mode 100644 index 000000000..665f79f23 --- /dev/null +++ b/examples/axum-graphql_example/src/main.rs @@ -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, 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(); +}