mirror of
https://github.com/actix/examples
synced 2025-06-26 17:17:42 +02:00
Restructure folders (#411)
This commit is contained in:
committed by
GitHub
parent
9db98162b2
commit
c3407627d0
11
graphql/graphql-demo/Cargo.toml
Normal file
11
graphql/graphql-demo/Cargo.toml
Normal 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"
|
33
graphql/graphql-demo/README.md
Normal file
33
graphql/graphql-demo/README.md
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
38
graphql/graphql-demo/src/main.rs
Normal file
38
graphql/graphql-demo/src/main.rs
Normal 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
|
||||
}
|
139
graphql/graphql-demo/src/starwars/mod.rs
Normal file
139
graphql/graphql-demo/src/starwars/mod.rs
Normal 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()
|
||||
}
|
||||
}
|
225
graphql/graphql-demo/src/starwars/model.rs
Normal file
225
graphql/graphql-demo/src/starwars/model.rs
Normal 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
|
||||
}
|
1
graphql/juniper-advanced/.env.example
Normal file
1
graphql/juniper-advanced/.env.example
Normal file
@ -0,0 +1 @@
|
||||
DATABASE_URL=mysql://user:password@127.0.0.1/dbname
|
23
graphql/juniper-advanced/Cargo.toml
Normal file
23
graphql/juniper-advanced/Cargo.toml
Normal 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"] }
|
28
graphql/juniper-advanced/README.md
Normal file
28
graphql/juniper-advanced/README.md
Normal 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
|
77
graphql/juniper-advanced/mysql-schema.sql
Normal file
77
graphql/juniper-advanced/mysql-schema.sql
Normal 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 */;
|
12
graphql/juniper-advanced/src/db.rs
Normal file
12
graphql/juniper-advanced/src/db.rs
Normal 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")
|
||||
}
|
39
graphql/juniper-advanced/src/handlers.rs
Normal file
39
graphql/juniper-advanced/src/handlers.rs
Normal 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));
|
||||
}
|
33
graphql/juniper-advanced/src/main.rs
Normal file
33
graphql/juniper-advanced/src/main.rs
Normal 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
|
||||
}
|
3
graphql/juniper-advanced/src/schemas/mod.rs
Normal file
3
graphql/juniper-advanced/src/schemas/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod product;
|
||||
pub mod root;
|
||||
pub mod user;
|
51
graphql/juniper-advanced/src/schemas/product.rs
Normal file
51
graphql/juniper-advanced/src/schemas/product.rs
Normal 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,
|
||||
}
|
175
graphql/juniper-advanced/src/schemas/root.rs
Normal file
175
graphql/juniper-advanced/src/schemas/root.rs
Normal 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)
|
||||
}
|
58
graphql/juniper-advanced/src/schemas/user.rs
Normal file
58
graphql/juniper-advanced/src/schemas/user.rs
Normal 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()
|
||||
}
|
||||
}
|
14
graphql/juniper/Cargo.toml
Normal file
14
graphql/juniper/Cargo.toml
Normal 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
74
graphql/juniper/README.md
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
63
graphql/juniper/src/main.rs
Normal file
63
graphql/juniper/src/main.rs
Normal 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
|
||||
}
|
62
graphql/juniper/src/schema.rs
Normal file
62
graphql/juniper/src/schema.rs
Normal 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 {})
|
||||
}
|
Reference in New Issue
Block a user