1
0
mirror of https://github.com/actix/examples synced 2025-06-26 17:17:42 +02:00

Restructure folders (#411)

This commit is contained in:
Daniel T. Rodrigues
2021-02-25 21:57:58 -03:00
committed by GitHub
parent 9db98162b2
commit c3407627d0
334 changed files with 127 additions and 120 deletions

View File

@ -0,0 +1,11 @@
[package]
name = "async-graphql-demo"
version = "0.1.0"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
[dependencies]
actix-web = "3.0.0"
async-graphql = "2.0.0"
async-graphql-actix-web = "2.0.0"
slab = "0.4.2"

View File

@ -0,0 +1,33 @@
Getting started using [Async-graphql](https://github.com/async-graphql/async-graphql) with Actix web.
## Run
```bash
cargo run --bin async-graphql-demo
```
## Endpoints
GET http://127.0.0.1:8000/ GraphQL Playground UI
POST http://127.0.0.1:8000/ For GraphQL query
## Query Examples
```graphql
{
humans {
edges {
node {
id
name
friends {
id
name
}
appearsIn
homePlanet
}
}
}
}
```

View File

@ -0,0 +1,38 @@
mod starwars;
use actix_web::{guard, web, App, HttpResponse, HttpServer, Result};
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
use async_graphql::{EmptyMutation, EmptySubscription, Schema};
use async_graphql_actix_web::{Request, Response};
use starwars::{QueryRoot, StarWars, StarWarsSchema};
async fn index(schema: web::Data<StarWarsSchema>, req: Request) -> Response {
schema.execute(req.into_inner()).await.into()
}
async fn index_playground() -> Result<HttpResponse> {
Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source(
GraphQLPlaygroundConfig::new("/").subscription_endpoint("/"),
)))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription)
.data(StarWars::new())
.finish();
println!("Playground: http://localhost:8000");
HttpServer::new(move || {
App::new()
.data(schema.clone())
.service(web::resource("/").guard(guard::Post()).to(index))
.service(web::resource("/").guard(guard::Get()).to(index_playground))
})
.bind("127.0.0.1:8000")?
.run()
.await
}

View File

@ -0,0 +1,139 @@
mod model;
use async_graphql::{EmptyMutation, EmptySubscription, Schema};
use model::Episode;
use slab::Slab;
use std::collections::HashMap;
pub use model::QueryRoot;
pub type StarWarsSchema = Schema<QueryRoot, EmptyMutation, EmptySubscription>;
pub struct StarWarsChar {
id: &'static str,
name: &'static str,
friends: Vec<usize>,
appears_in: Vec<Episode>,
home_planet: Option<&'static str>,
primary_function: Option<&'static str>,
}
pub struct StarWars {
luke: usize,
artoo: usize,
chars: Slab<StarWarsChar>,
human_data: HashMap<&'static str, usize>,
droid_data: HashMap<&'static str, usize>,
}
impl StarWars {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
let mut chars = Slab::new();
let luke = chars.insert(StarWarsChar {
id: "1000",
name: "Luke Skywalker",
friends: vec![],
appears_in: vec![],
home_planet: Some("Tatooine"),
primary_function: None,
});
let vader = chars.insert(StarWarsChar {
id: "1001",
name: "Luke Skywalker",
friends: vec![],
appears_in: vec![],
home_planet: Some("Tatooine"),
primary_function: None,
});
let han = chars.insert(StarWarsChar {
id: "1002",
name: "Han Solo",
friends: vec![],
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
home_planet: None,
primary_function: None,
});
let leia = chars.insert(StarWarsChar {
id: "1003",
name: "Leia Organa",
friends: vec![],
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
home_planet: Some("Alderaa"),
primary_function: None,
});
let tarkin = chars.insert(StarWarsChar {
id: "1004",
name: "Wilhuff Tarkin",
friends: vec![],
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
home_planet: None,
primary_function: None,
});
let threepio = chars.insert(StarWarsChar {
id: "2000",
name: "C-3PO",
friends: vec![],
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
home_planet: None,
primary_function: Some("Protocol"),
});
let artoo = chars.insert(StarWarsChar {
id: "2001",
name: "R2-D2",
friends: vec![],
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
home_planet: None,
primary_function: Some("Astromech"),
});
chars[luke].friends = vec![han, leia, threepio, artoo];
chars[vader].friends = vec![tarkin];
chars[han].friends = vec![luke, leia, artoo];
chars[leia].friends = vec![luke, han, threepio, artoo];
chars[tarkin].friends = vec![vader];
chars[threepio].friends = vec![luke, han, leia, artoo];
chars[artoo].friends = vec![luke, han, leia];
let mut human_data = HashMap::new();
human_data.insert("1000", luke);
human_data.insert("1001", vader);
human_data.insert("1002", han);
human_data.insert("1003", leia);
human_data.insert("1004", tarkin);
let mut droid_data = HashMap::new();
droid_data.insert("2000", threepio);
droid_data.insert("2001", artoo);
Self {
luke,
artoo,
chars,
human_data,
droid_data,
}
}
pub fn human(&self, id: &str) -> Option<usize> {
self.human_data.get(id).cloned()
}
pub fn droid(&self, id: &str) -> Option<usize> {
self.droid_data.get(id).cloned()
}
pub fn humans(&self) -> Vec<usize> {
self.human_data.values().cloned().collect()
}
pub fn droids(&self) -> Vec<usize> {
self.droid_data.values().cloned().collect()
}
}

View File

@ -0,0 +1,225 @@
use super::StarWars;
use async_graphql::connection::{query, Connection, Edge, EmptyFields};
use async_graphql::{Context, Enum, FieldResult, Interface, Object};
/// One of the films in the Star Wars Trilogy
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum Episode {
/// Released in 1977.
NewHope,
/// Released in 1980.
Empire,
/// Released in 1983.
Jedi,
}
pub struct Human(usize);
/// A humanoid creature in the Star Wars universe.
#[Object]
impl Human {
/// The id of the human.
async fn id(&self, ctx: &Context<'_>) -> &str {
ctx.data_unchecked::<StarWars>().chars[self.0].id
}
/// The name of the human.
async fn name(&self, ctx: &Context<'_>) -> &str {
ctx.data_unchecked::<StarWars>().chars[self.0].name
}
/// The friends of the human, or an empty list if they have none.
async fn friends(&self, ctx: &Context<'_>) -> Vec<Character> {
ctx.data_unchecked::<StarWars>().chars[self.0]
.friends
.iter()
.map(|id| Human(*id).into())
.collect()
}
/// Which movies they appear in.
async fn appears_in<'a>(&self, ctx: &'a Context<'_>) -> &'a [Episode] {
&ctx.data_unchecked::<StarWars>().chars[self.0].appears_in
}
/// The home planet of the human, or null if unknown.
async fn home_planet<'a>(&self, ctx: &'a Context<'_>) -> &'a Option<&'a str> {
&ctx.data_unchecked::<StarWars>().chars[self.0].home_planet
}
}
pub struct Droid(usize);
/// A mechanical creature in the Star Wars universe.
#[Object]
impl Droid {
/// The id of the droid.
async fn id(&self, ctx: &Context<'_>) -> &str {
ctx.data_unchecked::<StarWars>().chars[self.0].id
}
/// The name of the droid.
async fn name(&self, ctx: &Context<'_>) -> &str {
ctx.data_unchecked::<StarWars>().chars[self.0].name
}
/// The friends of the droid, or an empty list if they have none.
async fn friends(&self, ctx: &Context<'_>) -> Vec<Character> {
ctx.data_unchecked::<StarWars>().chars[self.0]
.friends
.iter()
.map(|id| Droid(*id).into())
.collect()
}
/// Which movies they appear in.
async fn appears_in<'a>(&self, ctx: &'a Context<'_>) -> &'a [Episode] {
&ctx.data_unchecked::<StarWars>().chars[self.0].appears_in
}
/// The primary function of the droid.
async fn primary_function<'a>(&self, ctx: &'a Context<'_>) -> &'a Option<&'a str> {
&ctx.data_unchecked::<StarWars>().chars[self.0].primary_function
}
}
pub struct QueryRoot;
#[Object]
impl QueryRoot {
async fn hero(
&self,
ctx: &Context<'_>,
#[graphql(
desc = "If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode."
)]
episode: Episode,
) -> Character {
if episode == Episode::Empire {
Human(ctx.data_unchecked::<StarWars>().luke).into()
} else {
Droid(ctx.data_unchecked::<StarWars>().artoo).into()
}
}
async fn human(
&self,
ctx: &Context<'_>,
#[graphql(desc = "id of the human")] id: String,
) -> Option<Human> {
ctx.data_unchecked::<StarWars>().human(&id).map(Human)
}
async fn humans(
&self,
ctx: &Context<'_>,
after: Option<String>,
before: Option<String>,
first: Option<i32>,
last: Option<i32>,
) -> FieldResult<Connection<usize, Human, EmptyFields, EmptyFields>> {
let humans = ctx
.data_unchecked::<StarWars>()
.humans()
.iter()
.copied()
.collect::<Vec<_>>();
query_characters(after, before, first, last, &humans)
.await
.map(|conn| conn.map_node(Human))
}
async fn droid(
&self,
ctx: &Context<'_>,
#[graphql(desc = "id of the droid")] id: String,
) -> Option<Droid> {
ctx.data_unchecked::<StarWars>().droid(&id).map(Droid)
}
async fn droids(
&self,
ctx: &Context<'_>,
after: Option<String>,
before: Option<String>,
first: Option<i32>,
last: Option<i32>,
) -> FieldResult<Connection<usize, Droid, EmptyFields, EmptyFields>> {
let droids = ctx
.data_unchecked::<StarWars>()
.droids()
.iter()
.copied()
.collect::<Vec<_>>();
query_characters(after, before, first, last, &droids)
.await
.map(|conn| conn.map_node(Droid))
}
}
#[derive(Interface)]
#[graphql(
field(name = "id", type = "&str"),
field(name = "name", type = "&str"),
field(name = "friends", type = "Vec<Character>"),
field(name = "appears_in", type = "&'ctx [Episode]")
)]
pub enum Character {
Human(Human),
Droid(Droid),
}
async fn query_characters(
after: Option<String>,
before: Option<String>,
first: Option<i32>,
last: Option<i32>,
characters: &[usize],
) -> FieldResult<Connection<usize, usize, EmptyFields, EmptyFields>> {
query(
after,
before,
first,
last,
|after, before, first, last| async move {
let mut start = 0usize;
let mut end = characters.len();
if let Some(after) = after {
if after >= characters.len() {
return Ok(Connection::new(false, false));
}
start = after + 1;
}
if let Some(before) = before {
if before == 0 {
return Ok(Connection::new(false, false));
}
end = before;
}
let mut slice = &characters[start..end];
if let Some(first) = first {
slice = &slice[..first.min(slice.len())];
end -= first.min(slice.len());
} else if let Some(last) = last {
slice = &slice[slice.len() - last.min(slice.len())..];
start = end - last.min(slice.len());
}
let mut connection = Connection::new(start > 0, end < characters.len());
connection.append(
slice
.iter()
.enumerate()
.map(|(idx, item)| Edge::new(start + idx, *item)),
);
Ok(connection)
},
)
.await
}

View File

@ -0,0 +1 @@
DATABASE_URL=mysql://user:password@127.0.0.1/dbname

View File

@ -0,0 +1,23 @@
[package]
name = "juniper-advanced"
version = "2.0.0"
authors = ["Dwi Sulfahnur <hello@dwisulfahnur.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "3"
juniper = "0.14"
mysql = "17"
r2d2 = "0.8"
r2d2_mysql = "17.0"
dotenv = "0.15"
env_logger = "0.8"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "0.8", features = ["serde", "v4"] }

View File

@ -0,0 +1,28 @@
# juniper-advanced
GraphQL Implementation in Rust using Actix, Juniper, and Mysql as Database
# Prerequites
- Rust Installed
- MySql as Database
# Database Configuration
Create a new database for this project, and import the existing database schema has been provided named ```mysql-schema.sql```.
Create ```.env``` file on the root directory of this project and set environment variable named ```DATABASE_URL```, the example file has been provided named ```.env.example```, you can see the format on there.
# Run
```sh
# go to the root dir
cd juniper-advanced
# Run
cargo run
```
### GraphQL Playground
http://127.0.0.1:8080/graphiql

View File

@ -0,0 +1,77 @@
-- MySQL dump 10.13 Distrib 8.0.16, for osx10.14 (x86_64)
--
-- Host: 127.0.0.1 Database: rust_graphql
-- ------------------------------------------------------
-- Server version 8.0.15
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
SET NAMES utf8mb4 ;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `product`
--
DROP TABLE IF EXISTS `product`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
SET character_set_client = utf8mb4 ;
CREATE TABLE `product` (
`id` varchar(255) NOT NULL,
`user_id` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
`price` decimal(10,0) NOT NULL,
PRIMARY KEY (`id`),
KEY `product_fk0` (`user_id`),
CONSTRAINT `product_fk0` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `product`
--
LOCK TABLES `product` WRITE;
/*!40000 ALTER TABLE `product` DISABLE KEYS */;
/*!40000 ALTER TABLE `product` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `user`
--
DROP TABLE IF EXISTS `user`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
SET character_set_client = utf8mb4 ;
CREATE TABLE `user` (
`id` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `user`
--
LOCK TABLES `user` WRITE;
/*!40000 ALTER TABLE `user` DISABLE KEYS */;
/*!40000 ALTER TABLE `user` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

View File

@ -0,0 +1,12 @@
use r2d2_mysql::mysql::{Opts, OptsBuilder};
use r2d2_mysql::MysqlConnectionManager;
pub type Pool = r2d2::Pool<MysqlConnectionManager>;
pub fn get_db_pool() -> Pool {
let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let opts = Opts::from_url(&db_url).unwrap();
let builder = OptsBuilder::from_opts(opts);
let manager = MysqlConnectionManager::new(builder);
r2d2::Pool::new(manager).expect("Failed to create DB Pool")
}

View File

@ -0,0 +1,39 @@
use actix_web::{web, Error, HttpResponse};
use juniper::http::graphiql::graphiql_source;
use juniper::http::GraphQLRequest;
use crate::db::Pool;
use crate::schemas::root::{create_schema, Context, Schema};
pub async fn graphql(
pool: web::Data<Pool>,
schema: web::Data<Schema>,
data: web::Json<GraphQLRequest>,
) -> Result<HttpResponse, Error> {
let ctx = Context {
dbpool: pool.get_ref().to_owned(),
};
let res = web::block(move || {
let res = data.execute(&schema, &ctx);
Ok::<_, serde_json::error::Error>(serde_json::to_string(&res)?)
})
.await
.map_err(Error::from)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(res))
}
pub async fn graphql_playground() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(graphiql_source("/graphql"))
}
pub fn register(config: &mut web::ServiceConfig) {
config
.data(create_schema())
.route("/graphql", web::post().to(graphql))
.route("/graphiql", web::get().to(graphql_playground));
}

View File

@ -0,0 +1,33 @@
#[macro_use]
extern crate juniper;
extern crate r2d2;
extern crate r2d2_mysql;
extern crate serde_json;
use actix_web::{middleware, web, App, HttpServer};
use crate::db::get_db_pool;
use crate::handlers::register;
mod db;
mod handlers;
mod schemas;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenv::dotenv().ok();
std::env::set_var("RUST_LOG", "actix_web=info,info");
env_logger::init();
let pool = get_db_pool();
HttpServer::new(move || {
App::new()
.data(pool.clone())
.wrap(middleware::Logger::default())
.configure(register)
.default_service(web::to(|| async { "404" }))
})
.bind("127.0.0.1:8080")?
.run()
.await
}

View File

@ -0,0 +1,3 @@
pub mod product;
pub mod root;
pub mod user;

View File

@ -0,0 +1,51 @@
use mysql::{from_row, params, Error as DBError, Row};
use crate::schemas::root::Context;
use crate::schemas::user::User;
/// Product
#[derive(Default, Debug)]
pub struct Product {
pub id: String,
pub user_id: String,
pub name: String,
pub price: f64,
}
#[juniper::object(Context = Context)]
impl Product {
fn id(&self) -> &str {
&self.id
}
fn user_id(&self) -> &str {
&self.user_id
}
fn name(&self) -> &str {
&self.name
}
fn price(&self) -> f64 {
self.price
}
fn user(&self, context: &Context) -> Option<User> {
let mut conn = context.dbpool.get().unwrap();
let user: Result<Option<Row>, DBError> = conn.first_exec(
"SELECT * FROM user WHERE id=:id",
params! {"id" => &self.user_id},
);
if let Err(err) = user {
None
} else {
let (id, name, email) = from_row(user.unwrap().unwrap());
Some(User { id, name, email })
}
}
}
#[derive(GraphQLInputObject)]
#[graphql(description = "Product Input")]
pub struct ProductInput {
pub user_id: String,
pub name: String,
pub price: f64,
}

View File

@ -0,0 +1,175 @@
use juniper::{FieldError, FieldResult, RootNode};
use mysql::{from_row, params, Error as DBError, Row};
use crate::db::Pool;
use super::product::{Product, ProductInput};
use super::user::{User, UserInput};
pub struct Context {
pub dbpool: Pool,
}
impl juniper::Context for Context {}
pub struct QueryRoot;
#[juniper::object(Context = Context)]
impl QueryRoot {
#[graphql(description = "List of all users")]
fn users(context: &Context) -> FieldResult<Vec<User>> {
let mut conn = context.dbpool.get().unwrap();
let users = conn
.prep_exec("select * from user", ())
.map(|result| {
result
.map(|x| x.unwrap())
.map(|mut row| {
let (id, name, email) = from_row(row);
User { id, name, email }
})
.collect()
})
.unwrap();
Ok(users)
}
#[graphql(description = "Get Single user reference by user ID")]
fn user(context: &Context, id: String) -> FieldResult<User> {
let mut conn = context.dbpool.get().unwrap();
let user: Result<Option<Row>, DBError> =
conn.first_exec("SELECT * FROM user WHERE id=:id", params! {"id" => id});
if let Err(err) = user {
return Err(FieldError::new(
"User Not Found",
graphql_value!({ "not_found": "user not found" }),
));
}
let (id, name, email) = from_row(user.unwrap().unwrap());
Ok(User { id, name, email })
}
#[graphql(description = "List of all users")]
fn products(context: &Context) -> FieldResult<Vec<Product>> {
let mut conn = context.dbpool.get().unwrap();
let products = conn
.prep_exec("select * from product", ())
.map(|result| {
result
.map(|x| x.unwrap())
.map(|mut row| {
let (id, user_id, name, price) = from_row(row);
Product {
id,
user_id,
name,
price,
}
})
.collect()
})
.unwrap();
Ok(products)
}
#[graphql(description = "Get Single user reference by user ID")]
fn product(context: &Context, id: String) -> FieldResult<Product> {
let mut conn = context.dbpool.get().unwrap();
let product: Result<Option<Row>, DBError> =
conn.first_exec("SELECT * FROM user WHERE id=:id", params! {"id" => id});
if let Err(err) = product {
return Err(FieldError::new(
"Product Not Found",
graphql_value!({ "not_found": "product not found" }),
));
}
let (id, user_id, name, price) = from_row(product.unwrap().unwrap());
Ok(Product {
id,
user_id,
name,
price,
})
}
}
pub struct MutationRoot;
#[juniper::object(Context = Context)]
impl MutationRoot {
fn create_user(context: &Context, user: UserInput) -> FieldResult<User> {
let mut conn = context.dbpool.get().unwrap();
let new_id = uuid::Uuid::new_v4().to_simple().to_string();
let insert: Result<Option<Row>, DBError> = conn.first_exec(
"INSERT INTO user(id, name, email) VALUES(:id, :name, :email)",
params! {
"id" => &new_id,
"name" => &user.name,
"email" => &user.email,
},
);
match insert {
Ok(opt_row) => Ok(User {
id: new_id,
name: user.name,
email: user.email,
}),
Err(err) => {
let msg = match err {
DBError::MySqlError(err) => err.message,
_ => "internal error".to_owned(),
};
Err(FieldError::new(
"Failed to create new user",
graphql_value!({ "internal_error": msg }),
))
}
}
}
fn create_product(context: &Context, product: ProductInput) -> FieldResult<Product> {
let mut conn = context.dbpool.get().unwrap();
let new_id = uuid::Uuid::new_v4().to_simple().to_string();
let insert: Result<Option<Row>, DBError> = conn.first_exec(
"INSERT INTO product(id, user_id, name, price) VALUES(:id, :user_id, :name, :price)",
params! {
"id" => &new_id,
"user_id" => &product.user_id,
"name" => &product.name,
"price" => &product.price.to_owned(),
},
);
match insert {
Ok(opt_row) => Ok(Product {
id: new_id,
user_id: product.user_id,
name: product.name,
price: product.price,
}),
Err(err) => {
let msg = match err {
DBError::MySqlError(err) => err.message,
_ => "internal error".to_owned(),
};
Err(FieldError::new(
"Failed to create new product",
graphql_value!({ "internal_error": msg }),
))
}
}
}
}
pub type Schema = RootNode<'static, QueryRoot, MutationRoot>;
pub fn create_schema() -> Schema {
Schema::new(QueryRoot, MutationRoot)
}

View File

@ -0,0 +1,58 @@
use mysql::{from_row, params};
use crate::schemas::product::Product;
use crate::schemas::root::Context;
/// User
#[derive(Default, Debug)]
pub struct User {
pub id: String,
pub name: String,
pub email: String,
}
#[derive(GraphQLInputObject)]
#[graphql(description = "User Input")]
pub struct UserInput {
pub name: String,
pub email: String,
}
#[juniper::object(Context = Context)]
impl User {
fn id(&self) -> &str {
&self.id
}
fn name(&self) -> &str {
&self.name
}
fn email(&self) -> &str {
&self.email
}
fn products(&self, context: &Context) -> Vec<Product> {
let mut conn = context.dbpool.get().unwrap();
conn.prep_exec(
"select * from product where user_id=:user_id",
params! {
"user_id" => &self.id
},
)
.map(|result| {
result
.map(|x| x.unwrap())
.map(|mut row| {
let (id, user_id, name, price) = from_row(row);
Product {
id,
user_id,
name,
price,
}
})
.collect()
})
.unwrap()
}
}

View File

@ -0,0 +1,14 @@
[package]
name = "juniper-example"
version = "0.2.0"
authors = ["pyros2097 <pyros2097@gmail.com>"]
edition = "2018"
[dependencies]
actix-web = "3"
actix-cors = "0.4.0"
env_logger = "0.8"
serde = "1.0.103"
serde_json = "1.0.44"
serde_derive = "1.0.103"
juniper = "0.14.2"

74
graphql/juniper/README.md Normal file
View File

@ -0,0 +1,74 @@
# Juniper
[Juniper](https://github.com/graphql-rust/juniper) integration for Actix web.
If you want more advanced example, see also the [juniper-advanced example].
[juniper-advanced example]: https://github.com/actix/examples/tree/master/juniper-advanced
## Usage
### server
```bash
cd examples/juniper
cargo run (or ``cargo watch -x run``)
# Started http server: 127.0.0.1:8080
```
### web client
[http://127.0.0.1:8080/graphiql](http://127.0.0.1:8080/graphiql)
_Query example:_
```graphql
{
human(id: "1234") {
name
appearsIn
homePlanet
}
}
```
_Result:_
```json
{
"data": {
"human": {
"name": "Luke",
"appearsIn": [
"NEW_HOPE"
],
"homePlanet": "Mars"
}
}
}
```
_Mutation example:_
```graphql
mutation {
createHuman(newHuman: {name: "Fresh Kid Ice", appearsIn: EMPIRE, homePlanet: "earth"}) {
id
name
appearsIn
homePlanet
}
}
```
_Result:_
```json
{
"data": {
"createHuman": {
"id": "1234",
"name": "Fresh Kid Ice",
"appearsIn": [
"EMPIRE"
],
"homePlanet": "earth"
}
}
}
```

View File

@ -0,0 +1,63 @@
//! Actix web juniper example
//!
//! A simple example integrating juniper in actix-web
use std::io;
use std::sync::Arc;
use actix_cors::Cors;
use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer};
use juniper::http::graphiql::graphiql_source;
use juniper::http::GraphQLRequest;
mod schema;
use crate::schema::{create_schema, Schema};
async fn graphiql() -> HttpResponse {
let html = graphiql_source("http://127.0.0.1:8080/graphql");
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(html)
}
async fn graphql(
st: web::Data<Arc<Schema>>,
data: web::Json<GraphQLRequest>,
) -> Result<HttpResponse, Error> {
let user = web::block(move || {
let res = data.execute(&st, &());
Ok::<_, serde_json::error::Error>(serde_json::to_string(&res)?)
})
.await?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(user))
}
#[actix_web::main]
async fn main() -> io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
// Create Juniper schema
let schema = std::sync::Arc::new(create_schema());
// Start http server
HttpServer::new(move || {
App::new()
.data(schema.clone())
.wrap(middleware::Logger::default())
.wrap(
Cors::new()
.allowed_methods(vec!["POST", "GET"])
.supports_credentials()
.max_age(3600)
.finish(),
)
.service(web::resource("/graphql").route(web::post().to(graphql)))
.service(web::resource("/graphiql").route(web::get().to(graphiql)))
})
.bind("127.0.0.1:8080")?
.run()
.await
}

View File

@ -0,0 +1,62 @@
use juniper::FieldResult;
use juniper::RootNode;
#[derive(GraphQLEnum)]
enum Episode {
NewHope,
Empire,
Jedi,
}
use juniper::{GraphQLEnum, GraphQLInputObject, GraphQLObject};
#[derive(GraphQLObject)]
#[graphql(description = "A humanoid creature in the Star Wars universe")]
struct Human {
id: String,
name: String,
appears_in: Vec<Episode>,
home_planet: String,
}
#[derive(GraphQLInputObject)]
#[graphql(description = "A humanoid creature in the Star Wars universe")]
struct NewHuman {
name: String,
appears_in: Vec<Episode>,
home_planet: String,
}
pub struct QueryRoot;
#[juniper::object]
impl QueryRoot {
fn human(id: String) -> FieldResult<Human> {
Ok(Human {
id: "1234".to_owned(),
name: "Luke".to_owned(),
appears_in: vec![Episode::NewHope],
home_planet: "Mars".to_owned(),
})
}
}
pub struct MutationRoot;
#[juniper::object]
impl MutationRoot {
fn create_human(new_human: NewHuman) -> FieldResult<Human> {
Ok(Human {
id: "1234".to_owned(),
name: new_human.name,
appears_in: new_human.appears_in,
home_planet: new_human.home_planet,
})
}
}
pub type Schema = RootNode<'static, QueryRoot, MutationRoot>;
pub fn create_schema() -> Schema {
Schema::new(QueryRoot {}, MutationRoot {})
}