v1.9.0
This commit is contained in:
parent
a53e61abf9
commit
09d5e5f489
12
Cargo.toml
12
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "async-graphql"
|
||||
version = "1.8.1"
|
||||
version = "1.9.0"
|
||||
authors = ["sunli <scott_s829@163.com>"]
|
||||
edition = "2018"
|
||||
description = "The GraphQL server library implemented by rust"
|
||||
|
@ -18,7 +18,7 @@ default = ["bson", "uuid", "url", "chrono-tz", "validators"]
|
|||
validators = ["regex"]
|
||||
|
||||
[dependencies]
|
||||
async-graphql-derive = { path = "async-graphql-derive", version = "1.8.1" }
|
||||
async-graphql-derive = { path = "async-graphql-derive", version = "1.9.0" }
|
||||
graphql-parser = "=0.2.3"
|
||||
anyhow = "1.0.26"
|
||||
thiserror = "1.0.11"
|
||||
|
@ -48,12 +48,12 @@ chrono-tz = { version = "0.5.1", optional = true }
|
|||
|
||||
[dev-dependencies]
|
||||
async-std = { version = "1.5.0", features = ["attributes"] }
|
||||
actix-rt = "1.0.0"
|
||||
tide = "0.6.0"
|
||||
actix-web = "2.0.0"
|
||||
serde = "1.0.104"
|
||||
serde_derive = "1.0.104"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"async-graphql-derive",
|
||||
"async-graphql-actix-web"
|
||||
"async-graphql-actix-web",
|
||||
"async-graphql-warp",
|
||||
]
|
|
@ -31,13 +31,9 @@ It supports all of the GraphQL specifications and is easy to integrate into exis
|
|||
* [Cargo package](https://crates.io/crates/async-graphql)
|
||||
* Minimum supported Rust version: 1.42 or later
|
||||
|
||||
## Example
|
||||
## Examples
|
||||
|
||||
```shell script
|
||||
cargo run --example actix_web
|
||||
```
|
||||
|
||||
Open `http://localhost:8000` in browser
|
||||
https://github.com/sunli829/async-graphql-examples
|
||||
|
||||
## Benchmark
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "async-graphql-actix-web"
|
||||
version = "0.8.1"
|
||||
version = "1.0.0"
|
||||
authors = ["sunli <scott_s829@163.com>"]
|
||||
edition = "2018"
|
||||
description = "async-graphql for actix-web"
|
||||
|
@ -13,19 +13,11 @@ keywords = ["futures", "async", "graphql"]
|
|||
categories = ["network-programming", "asynchronous"]
|
||||
|
||||
[dependencies]
|
||||
async-graphql = { path = "..", version = "1.8.1" }
|
||||
async-graphql = { path = "..", version = "1.9.0" }
|
||||
actix-web = "2.0.0"
|
||||
actix-multipart = "0.2.0"
|
||||
actix-web-actors = "2.0.0"
|
||||
actix = "0.9.0"
|
||||
actix-rt = "1.0.0"
|
||||
futures = "0.3.0"
|
||||
mime = "0.3.16"
|
||||
bytes = "0.5.4"
|
||||
serde = "1.0.104"
|
||||
serde_derive = "1.0.104"
|
||||
serde_json = "1.0.48"
|
||||
slab = "0.4.2"
|
||||
actix_derive = "0.5.0"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "1.0.0"
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
use actix::clock::Duration;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use async_graphql::{Context, FieldResult, Schema, SimpleBroker, ID};
|
||||
use futures::lock::Mutex;
|
||||
use futures::{Stream, StreamExt};
|
||||
use slab::Slab;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Book {
|
||||
id: ID,
|
||||
name: String,
|
||||
author: String,
|
||||
}
|
||||
|
||||
#[async_graphql::Object]
|
||||
impl Book {
|
||||
#[field]
|
||||
async fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[field]
|
||||
async fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
#[field]
|
||||
async fn author(&self) -> &str {
|
||||
&self.author
|
||||
}
|
||||
}
|
||||
|
||||
type Storage = Arc<Mutex<Slab<Book>>>;
|
||||
|
||||
struct QueryRoot;
|
||||
|
||||
#[async_graphql::Object]
|
||||
impl QueryRoot {
|
||||
#[field]
|
||||
async fn books(&self, ctx: &Context<'_>) -> Vec<Book> {
|
||||
let books = ctx.data::<Storage>().lock().await;
|
||||
books.iter().map(|(_, book)| book).cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
struct MutationRoot;
|
||||
|
||||
#[async_graphql::Object]
|
||||
impl MutationRoot {
|
||||
#[field]
|
||||
async fn create_book(&self, ctx: &Context<'_>, name: String, author: String) -> ID {
|
||||
let mut books = ctx.data::<Storage>().lock().await;
|
||||
let entry = books.vacant_entry();
|
||||
let id: ID = entry.key().into();
|
||||
let book = Book {
|
||||
id: id.clone(),
|
||||
name,
|
||||
author,
|
||||
};
|
||||
entry.insert(book);
|
||||
SimpleBroker::publish(BookChanged {
|
||||
mutation_type: MutationType::Created,
|
||||
id: id.clone(),
|
||||
});
|
||||
id
|
||||
}
|
||||
|
||||
#[field]
|
||||
async fn delete_book(&self, ctx: &Context<'_>, id: ID) -> FieldResult<bool> {
|
||||
let mut books = ctx.data::<Storage>().lock().await;
|
||||
let id = id.parse::<usize>()?;
|
||||
if books.contains(id) {
|
||||
books.remove(id);
|
||||
SimpleBroker::publish(BookChanged {
|
||||
mutation_type: MutationType::Deleted,
|
||||
id: id.into(),
|
||||
});
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_graphql::Enum]
|
||||
enum MutationType {
|
||||
Created,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
#[async_graphql::SimpleObject]
|
||||
#[derive(Clone)]
|
||||
struct BookChanged {
|
||||
#[field]
|
||||
mutation_type: MutationType,
|
||||
|
||||
#[field]
|
||||
id: ID,
|
||||
}
|
||||
|
||||
struct SubscriptionRoot;
|
||||
|
||||
#[async_graphql::Subscription]
|
||||
impl SubscriptionRoot {
|
||||
#[field]
|
||||
async fn interval(&self, n: i32) -> impl Stream<Item = i32> {
|
||||
let mut value = 0;
|
||||
actix_rt::time::interval(Duration::from_secs(1)).map(move |_| {
|
||||
value += n;
|
||||
value
|
||||
})
|
||||
}
|
||||
|
||||
#[field]
|
||||
async fn books(&self, mutation_type: Option<MutationType>) -> impl Stream<Item = BookChanged> {
|
||||
SimpleBroker::<BookChanged>::subscribe().filter(move |event| {
|
||||
let res = if let Some(mutation_type) = mutation_type {
|
||||
event.mutation_type == mutation_type
|
||||
} else {
|
||||
true
|
||||
};
|
||||
futures::future::ready(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(move || {
|
||||
let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
|
||||
.data(Storage::default())
|
||||
.finish();
|
||||
let handler = async_graphql_actix_web::HandlerBuilder::new(schema)
|
||||
.enable_ui("http://localhost:8000", Some("ws://localhost:8000"))
|
||||
.enable_subscription()
|
||||
.build();
|
||||
App::new().service(web::resource("/").to(handler))
|
||||
})
|
||||
.bind("127.0.0.1:8000")?
|
||||
.run()
|
||||
.await
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
use actix_web::{web, App, HttpServer};
|
||||
use async_graphql::{Context, EmptyMutation, EmptySubscription, Schema};
|
||||
|
||||
struct MyToken(Option<String>);
|
||||
|
||||
struct QueryRoot;
|
||||
|
||||
#[async_graphql::Object]
|
||||
impl QueryRoot {
|
||||
#[field]
|
||||
async fn current_token<'a>(&self, ctx: &'a Context<'_>) -> Option<&'a str> {
|
||||
ctx.data::<MyToken>().0.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(move || {
|
||||
let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
|
||||
let handler = async_graphql_actix_web::HandlerBuilder::new(schema)
|
||||
.enable_subscription()
|
||||
.enable_ui("http://localhost:8000", None)
|
||||
.on_request(|req, builder| {
|
||||
builder.data(MyToken(
|
||||
req.headers()
|
||||
.get("Token")
|
||||
.and_then(|value| value.to_str().map(ToString::to_string).ok()),
|
||||
))
|
||||
})
|
||||
.build();
|
||||
App::new().service(web::resource("/").to(handler))
|
||||
})
|
||||
.bind("127.0.0.1:8000")?
|
||||
.run()
|
||||
.await
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
use actix_web::{web, App, HttpServer};
|
||||
use async_graphql::{Context, EmptySubscription, Schema, Upload, ID};
|
||||
use futures::lock::Mutex;
|
||||
use slab::Slab;
|
||||
|
||||
#[async_graphql::SimpleObject]
|
||||
#[derive(Clone)]
|
||||
struct FileInfo {
|
||||
#[field]
|
||||
id: ID,
|
||||
|
||||
#[field]
|
||||
filename: String,
|
||||
|
||||
#[field]
|
||||
mimetype: Option<String>,
|
||||
|
||||
#[field]
|
||||
path: String,
|
||||
}
|
||||
|
||||
type Storage = Mutex<Slab<FileInfo>>;
|
||||
|
||||
struct QueryRoot;
|
||||
|
||||
#[async_graphql::Object]
|
||||
impl QueryRoot {
|
||||
#[field]
|
||||
async fn uploads(&self, ctx: &Context<'_>) -> Vec<FileInfo> {
|
||||
let storage = ctx.data::<Storage>().lock().await;
|
||||
storage.iter().map(|(_, file)| file).cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
struct MutationRoot;
|
||||
|
||||
#[async_graphql::Object]
|
||||
impl MutationRoot {
|
||||
#[field]
|
||||
async fn single_upload(&self, ctx: &Context<'_>, file: Upload) -> FileInfo {
|
||||
let mut storage = ctx.data::<Storage>().lock().await;
|
||||
println!("files count: {}", storage.len());
|
||||
let entry = storage.vacant_entry();
|
||||
let info = FileInfo {
|
||||
id: entry.key().into(),
|
||||
filename: file.filename,
|
||||
mimetype: file.content_type,
|
||||
path: file.path.display().to_string(),
|
||||
};
|
||||
entry.insert(info.clone());
|
||||
info
|
||||
}
|
||||
|
||||
#[field]
|
||||
async fn multiple_upload(&self, ctx: &Context<'_>, files: Vec<Upload>) -> Vec<FileInfo> {
|
||||
let mut infos = Vec::new();
|
||||
let mut storage = ctx.data::<Storage>().lock().await;
|
||||
for file in files {
|
||||
let entry = storage.vacant_entry();
|
||||
let info = FileInfo {
|
||||
id: entry.key().into(),
|
||||
filename: file.filename,
|
||||
mimetype: file.content_type,
|
||||
path: file.path.display().to_string(),
|
||||
};
|
||||
entry.insert(info.clone());
|
||||
infos.push(info)
|
||||
}
|
||||
infos
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription)
|
||||
.data(Storage::default())
|
||||
.finish();
|
||||
|
||||
HttpServer::new(move || {
|
||||
let handler = async_graphql_actix_web::HandlerBuilder::new(schema.clone())
|
||||
.enable_subscription()
|
||||
.build();
|
||||
App::new().service(web::resource("/").to(handler))
|
||||
})
|
||||
.bind("127.0.0.1:8000")?
|
||||
.run()
|
||||
.await
|
||||
}
|
|
@ -1,166 +1,60 @@
|
|||
mod request;
|
||||
mod session;
|
||||
//! Async-graphql integration with Actix-web
|
||||
|
||||
use crate::request::RequestWrapper;
|
||||
use crate::session::WsSession;
|
||||
use actix_web::http::{header, Method};
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Responder};
|
||||
use actix_web_actors::ws;
|
||||
use async_graphql::http::{playground_source, GQLHttpRequest, GQLResponse, IntoQueryBuilder};
|
||||
use async_graphql::{ObjectType, QueryBuilder, Schema, SubscriptionType};
|
||||
use futures::Future;
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod subscription;
|
||||
|
||||
use actix_web::dev::{Payload, PayloadStream};
|
||||
use actix_web::{http, web, Error, FromRequest, HttpRequest};
|
||||
use async_graphql::http::StreamBody;
|
||||
use async_graphql::{IntoQueryBuilder, QueryBuilder};
|
||||
use futures::channel::mpsc;
|
||||
use futures::{Future, SinkExt, StreamExt, TryFutureExt};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
type BoxOnRequestFn<Query, Mutation, Subscription> = Arc<
|
||||
dyn for<'a> Fn(
|
||||
&HttpRequest,
|
||||
QueryBuilder<Query, Mutation, Subscription>,
|
||||
) -> QueryBuilder<Query, Mutation, Subscription>,
|
||||
>;
|
||||
pub use subscription::WSSubscription;
|
||||
|
||||
pub struct HandlerBuilder<Query, Mutation, Subscription> {
|
||||
schema: Schema<Query, Mutation, Subscription>,
|
||||
enable_subscription: bool,
|
||||
enable_ui: Option<(String, Option<String>)>,
|
||||
on_request: Option<BoxOnRequestFn<Query, Mutation, Subscription>>,
|
||||
}
|
||||
/// Extractor for GraphQL request
|
||||
///
|
||||
/// It's a wrapper of `QueryBuilder`, you can use `GQLRequest::into_inner` unwrap it to `QueryBuilder`.
|
||||
pub struct GQLRequest(QueryBuilder);
|
||||
|
||||
impl<Query, Mutation, Subscription> HandlerBuilder<Query, Mutation, Subscription>
|
||||
where
|
||||
Query: ObjectType + Send + Sync + 'static,
|
||||
Mutation: ObjectType + Send + Sync + 'static,
|
||||
Subscription: SubscriptionType + Send + Sync + 'static,
|
||||
{
|
||||
/// Create an HTTP handler builder
|
||||
pub fn new(schema: Schema<Query, Mutation, Subscription>) -> Self {
|
||||
Self {
|
||||
schema,
|
||||
enable_subscription: false,
|
||||
enable_ui: None,
|
||||
on_request: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable GraphQL playground
|
||||
///
|
||||
/// 'endpoint' is the endpoint of the GraphQL Request.
|
||||
/// 'subscription_endpoint' is the endpoint of the GraphQL Subscription.
|
||||
pub fn enable_ui(self, endpoint: &str, subscription_endpoint: Option<&str>) -> Self {
|
||||
Self {
|
||||
enable_ui: Some((
|
||||
endpoint.to_string(),
|
||||
subscription_endpoint.map(|s| s.to_string()),
|
||||
)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable GraphQL Subscription.
|
||||
pub fn enable_subscription(self) -> Self {
|
||||
Self {
|
||||
enable_subscription: true,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// When a new request arrives, you can use this closure to append your own data to the `QueryBuilder`.
|
||||
pub fn on_request<
|
||||
F: for<'a> Fn(
|
||||
&HttpRequest,
|
||||
QueryBuilder<Query, Mutation, Subscription>,
|
||||
) -> QueryBuilder<Query, Mutation, Subscription>
|
||||
+ 'static,
|
||||
>(
|
||||
self,
|
||||
f: F,
|
||||
) -> Self {
|
||||
Self {
|
||||
on_request: Some(Arc::new(f)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an HTTP handler.
|
||||
pub fn build(
|
||||
self,
|
||||
) -> impl Fn(
|
||||
HttpRequest,
|
||||
web::Payload,
|
||||
) -> Pin<Box<dyn Future<Output = actix_web::Result<HttpResponse>>>>
|
||||
+ Clone
|
||||
+ 'static {
|
||||
let schema = self.schema.clone();
|
||||
let enable_ui = self.enable_ui;
|
||||
let enable_subscription = self.enable_subscription;
|
||||
let on_request = self.on_request;
|
||||
|
||||
move |req: HttpRequest, payload: web::Payload| {
|
||||
let schema = schema.clone();
|
||||
let enable_ui = enable_ui.clone();
|
||||
let on_request = on_request.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
if req.method() == Method::GET {
|
||||
if enable_subscription {
|
||||
if let Some(s) = req.headers().get(header::UPGRADE) {
|
||||
if let Ok(s) = s.to_str() {
|
||||
if s.to_ascii_lowercase().contains("websocket") {
|
||||
return ws::start_with_protocols(
|
||||
WsSession::new(schema.clone()),
|
||||
&["graphql-ws"],
|
||||
&req,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((endpoint, subscription_endpoint)) = &enable_ui {
|
||||
return Ok(HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(playground_source(
|
||||
endpoint,
|
||||
subscription_endpoint.as_deref(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if req.method() == Method::POST {
|
||||
let content_type = req
|
||||
.headers()
|
||||
.get(header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(|s| s.to_string());
|
||||
let r = RequestWrapper(content_type, payload);
|
||||
r.content_type();
|
||||
let mut builder = match IntoQueryBuilder::into_query_builder(r, &schema).await {
|
||||
Ok(builder) => builder,
|
||||
Err(err) => {
|
||||
return Ok(web::Json(GQLResponse(Err(err))).respond_to(&req).await?)
|
||||
}
|
||||
};
|
||||
|
||||
let cache_control = builder.cache_control().value();
|
||||
if let Some(on_request) = &on_request {
|
||||
builder = on_request(&req, builder);
|
||||
}
|
||||
|
||||
let mut resp = web::Json(GQLResponse(builder.execute().await))
|
||||
.respond_to(&req)
|
||||
.await?;
|
||||
if let Some(cache_control) = cache_control {
|
||||
resp.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
header::HeaderValue::from_str(&cache_control).unwrap(),
|
||||
);
|
||||
}
|
||||
Ok(resp)
|
||||
} else {
|
||||
Ok(HttpResponse::MethodNotAllowed().finish())
|
||||
}
|
||||
})
|
||||
}
|
||||
impl GQLRequest {
|
||||
/// Unwrap it to `QueryBuilder`.
|
||||
pub fn into_inner(self) -> QueryBuilder {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for GQLRequest {
|
||||
type Error = Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<GQLRequest, Error>>>>;
|
||||
type Config = ();
|
||||
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload<PayloadStream>) -> Self::Future {
|
||||
let content_type = req
|
||||
.headers()
|
||||
.get(http::header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(|value| value.to_string());
|
||||
|
||||
let (mut tx, rx) = mpsc::channel(16);
|
||||
|
||||
// Because Payload is !Send, so forward it to mpsc::Sender
|
||||
let mut payload = web::Payload(payload.take());
|
||||
actix_rt::spawn(async move {
|
||||
while let Some(item) = payload.next().await {
|
||||
if tx.send(item).await.is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Box::pin(
|
||||
(content_type, StreamBody::new(rx))
|
||||
.into_query_builder()
|
||||
.map_ok(GQLRequest)
|
||||
.map_err(actix_web::error::ErrorBadRequest),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,21 +11,23 @@ use std::time::{Duration, Instant};
|
|||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
pub struct WsSession<Query, Mutation, Subscription> {
|
||||
/// Actor for subscription via websocket
|
||||
pub struct WSSubscription<Query, Mutation, Subscription> {
|
||||
schema: Schema<Query, Mutation, Subscription>,
|
||||
hb: Instant,
|
||||
sink: Option<mpsc::Sender<Bytes>>,
|
||||
}
|
||||
|
||||
impl<Query, Mutation, Subscription> WsSession<Query, Mutation, Subscription>
|
||||
impl<Query, Mutation, Subscription> WSSubscription<Query, Mutation, Subscription>
|
||||
where
|
||||
Query: ObjectType + Send + Sync + 'static,
|
||||
Mutation: ObjectType + Send + Sync + 'static,
|
||||
Subscription: SubscriptionType + Send + Sync + 'static,
|
||||
{
|
||||
pub fn new(schema: Schema<Query, Mutation, Subscription>) -> Self {
|
||||
/// Create an actor for subscription connection via websocket.
|
||||
pub fn new(schema: &Schema<Query, Mutation, Subscription>) -> Self {
|
||||
Self {
|
||||
schema,
|
||||
schema: schema.clone(),
|
||||
hb: Instant::now(),
|
||||
sink: None,
|
||||
}
|
||||
|
@ -41,7 +43,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<Query, Mutation, Subscription> Actor for WsSession<Query, Mutation, Subscription>
|
||||
impl<Query, Mutation, Subscription> Actor for WSSubscription<Query, Mutation, Subscription>
|
||||
where
|
||||
Query: ObjectType + Sync + Send + 'static,
|
||||
Mutation: ObjectType + Sync + Send + 'static,
|
||||
|
@ -59,7 +61,7 @@ where
|
|||
}
|
||||
|
||||
impl<Query, Mutation, Subscription> StreamHandler<Result<Message, ProtocolError>>
|
||||
for WsSession<Query, Mutation, Subscription>
|
||||
for WSSubscription<Query, Mutation, Subscription>
|
||||
where
|
||||
Query: ObjectType + Sync + Send + 'static,
|
||||
Mutation: ObjectType + Sync + Send + 'static,
|
||||
|
@ -99,7 +101,7 @@ where
|
|||
}
|
||||
|
||||
impl<Query, Mutation, Subscription> StreamHandler<Bytes>
|
||||
for WsSession<Query, Mutation, Subscription>
|
||||
for WSSubscription<Query, Mutation, Subscription>
|
||||
where
|
||||
Query: ObjectType + Send + Sync + 'static,
|
||||
Mutation: ObjectType + Send + Sync + 'static,
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "async-graphql-derive"
|
||||
version = "1.8.1"
|
||||
version = "1.9.0"
|
||||
authors = ["sunli <scott_s829@163.com>"]
|
||||
edition = "2018"
|
||||
description = "Macros for async-graphql"
|
||||
|
|
|
@ -427,7 +427,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
|
|||
}.into_error(field.position))
|
||||
}
|
||||
|
||||
async fn find_entity(&self, ctx: &#crate_name::Context<'_>, pos: #crate_name::Pos, params: &#crate_name::Value) -> #crate_name::Result<serde_json::Value> {
|
||||
async fn find_entity(&self, ctx: &#crate_name::Context<'_>, pos: #crate_name::Pos, params: &#crate_name::Value) -> #crate_name::Result<#crate_name::serde_json::Value> {
|
||||
let params = match params {
|
||||
#crate_name::Value::Object(params) => params,
|
||||
_ => return Err(#crate_name::QueryError::EntityNotFound.into_error(pos)),
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "async-graphql-warp"
|
||||
version = "1.0.0"
|
||||
authors = ["sunli <scott_s829@163.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
async-graphql = { path = "..", version = "1.9.0" }
|
||||
warp = "0.2.2"
|
||||
futures = "0.3.0"
|
||||
bytes = "0.5.4"
|
|
@ -0,0 +1,169 @@
|
|||
//! Async-graphql integration with Wrap
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use async_graphql::http::StreamBody;
|
||||
use async_graphql::{
|
||||
IntoQueryBuilder, ObjectType, QueryBuilder, Schema, SubscriptionType, WebSocketTransport,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures::select;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use warp::filters::ws::Message;
|
||||
use warp::filters::BoxedFilter;
|
||||
use warp::reject::Reject;
|
||||
use warp::{Filter, Rejection, Reply};
|
||||
|
||||
/// Bad request error
|
||||
///
|
||||
/// It's a wrapper of `async_graphql::ParseRequestError`.
|
||||
pub struct BadRequest(pub async_graphql::ParseRequestError);
|
||||
|
||||
impl std::fmt::Debug for BadRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Reject for BadRequest {}
|
||||
|
||||
/// GraphQL request filter
|
||||
///
|
||||
/// It outputs a tuple containing the `Schema` and `QuertBuilder`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
///
|
||||
/// use async_graphql::*;
|
||||
/// use warp::Filter;
|
||||
/// use std::convert::Infallible;
|
||||
///
|
||||
/// struct QueryRoot;
|
||||
///
|
||||
/// #[Object]
|
||||
/// impl QueryRoot {
|
||||
/// #[field]
|
||||
/// async fn value(&self, ctx: &Context<'_>) -> i32 {
|
||||
/// unimplemented!()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
|
||||
/// let filter = async_graphql_warp::graphql(schema).and_then(|schema, builder| async move {
|
||||
/// let resp = builder.execute(&schema).await;
|
||||
/// Ok::<_, Infallible>(warp::reply::json(&GQLResponse(resp)).into_response())
|
||||
/// });
|
||||
/// warp::serve(filter).run(([0, 0, 0, 0], 8000)).await;
|
||||
/// ```
|
||||
pub fn graphql<Query, Mutation, Subscription>(
|
||||
schema: Schema<Query, Mutation, Subscription>,
|
||||
) -> BoxedFilter<((Schema<Query, Mutation, Subscription>, QueryBuilder),)>
|
||||
where
|
||||
Query: ObjectType + Send + Sync + 'static,
|
||||
Mutation: ObjectType + Send + Sync + 'static,
|
||||
Subscription: SubscriptionType + Send + Sync + 'static,
|
||||
{
|
||||
warp::any()
|
||||
.and(warp::post())
|
||||
.and(warp::header::optional::<String>("content-type"))
|
||||
.and(warp::body::stream())
|
||||
.and(warp::any().map(move || schema.clone()))
|
||||
.and_then(|content_type, body, schema| async move {
|
||||
let builder = (content_type, StreamBody::new(body))
|
||||
.into_query_builder()
|
||||
.await
|
||||
.map_err(|err| warp::reject::custom(BadRequest(err)))?;
|
||||
Ok::<_, Rejection>((schema, builder))
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
/// GraphQL subscription filter
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use async_graphql::*;
|
||||
/// use warp::Filter;
|
||||
/// use futures::{Stream, StreamExt};
|
||||
/// use tokio::time::Duration;
|
||||
///
|
||||
/// struct QueryRoot;
|
||||
///
|
||||
/// #[Object]
|
||||
/// impl QueryRoot {}
|
||||
///
|
||||
/// struct SubscriptionRoot;
|
||||
///
|
||||
/// #[Subscription]
|
||||
/// impl SubscriptionRoot {
|
||||
/// #[field]
|
||||
/// async fn tick(&self) -> impl Stream<String> {
|
||||
/// tokio::time::interval(Duration::from_secs(1)).map(|n| format!("{}", n.elapsed().as_secs_f32()))
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let schema = Schema::new(QueryRoot, EmptyMutation, SubscriptionRoot);
|
||||
/// let filter = async_graphql_warp::graphql_subscription(schema);
|
||||
/// warp::serve(filter).run(([0, 0, 0, 0], 8000)).await;
|
||||
/// ```
|
||||
pub fn graphql_subscription<Query, Mutation, Subscription>(
|
||||
schema: Schema<Query, Mutation, Subscription>,
|
||||
) -> BoxedFilter<(impl Reply,)>
|
||||
where
|
||||
Query: ObjectType + Sync + Send + 'static,
|
||||
Mutation: ObjectType + Sync + Send + 'static,
|
||||
Subscription: SubscriptionType + Send + Sync + 'static,
|
||||
{
|
||||
warp::any()
|
||||
.and(warp::ws())
|
||||
.and(warp::any().map(move || schema.clone()))
|
||||
.map(
|
||||
|ws: warp::ws::Ws, schema: Schema<Query, Mutation, Subscription>| {
|
||||
ws.on_upgrade(move |websocket| {
|
||||
let (mut tx, rx) = websocket.split();
|
||||
let (mut stx, srx) =
|
||||
schema.subscription_connection(WebSocketTransport::default());
|
||||
|
||||
let mut rx = rx.fuse();
|
||||
let mut srx = srx.fuse();
|
||||
|
||||
async move {
|
||||
loop {
|
||||
select! {
|
||||
bytes = srx.next() => {
|
||||
if let Some(bytes) = bytes {
|
||||
if tx
|
||||
.send(Message::text(unsafe {
|
||||
String::from_utf8_unchecked(bytes.to_vec())
|
||||
}))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
msg = rx.next() => {
|
||||
if let Some(Ok(msg)) = msg {
|
||||
if msg.is_text() {
|
||||
if stx.send(Bytes::copy_from_slice(msg.as_bytes())).await.is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
).map(|reply| {
|
||||
warp::reply::with_header(reply, "Sec-WebSocket-Protocol", "graphql-ws")
|
||||
})
|
||||
.boxed()
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
mod starwars;
|
||||
|
||||
use actix_web::{guard, web, App, HttpResponse, HttpServer};
|
||||
use async_graphql::http::{
|
||||
graphiql_source, playground_source, GQLRequest, GQLResponse, IntoQueryBuilder,
|
||||
};
|
||||
use async_graphql::{EmptyMutation, EmptySubscription, Schema};
|
||||
use futures::TryFutureExt;
|
||||
|
||||
type StarWarsSchema = Schema<starwars::QueryRoot, EmptyMutation, EmptySubscription>;
|
||||
|
||||
async fn index(s: web::Data<StarWarsSchema>, req: web::Json<GQLRequest>) -> web::Json<GQLResponse> {
|
||||
web::Json(GQLResponse(
|
||||
req.into_inner()
|
||||
.into_query_builder(&s)
|
||||
.and_then(|builder| builder.execute())
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
async fn gql_playgound() -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(playground_source("/", None))
|
||||
}
|
||||
|
||||
async fn gql_graphiql() -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(graphiql_source("/"))
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(
|
||||
Schema::build(starwars::QueryRoot, EmptyMutation, EmptySubscription)
|
||||
.data(starwars::StarWars::new())
|
||||
.extension(|| async_graphql::extensions::ApolloTracing::default())
|
||||
.finish(),
|
||||
)
|
||||
.service(web::resource("/").guard(guard::Post()).to(index))
|
||||
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
|
||||
.service(
|
||||
web::resource("/graphiql")
|
||||
.guard(guard::Get())
|
||||
.to(gql_graphiql),
|
||||
)
|
||||
})
|
||||
.bind("127.0.0.1:8000")?
|
||||
.run()
|
||||
.await
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
#[macro_use]
|
||||
extern crate thiserror;
|
||||
|
||||
use actix_rt;
|
||||
use actix_web::{guard, web, App, HttpResponse, HttpServer};
|
||||
use async_graphql::http::{
|
||||
graphiql_source, playground_source, GQLRequest, GQLResponse, IntoQueryBuilder,
|
||||
};
|
||||
use async_graphql::ErrorExtensions;
|
||||
use async_graphql::*;
|
||||
use futures::TryFutureExt;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MyError {
|
||||
#[error("Could not find resource")]
|
||||
NotFound,
|
||||
|
||||
#[error("ServerError")]
|
||||
ServerError(String),
|
||||
|
||||
#[error("No Extensions")]
|
||||
ErrorWithoutExtensions,
|
||||
}
|
||||
|
||||
impl ErrorExtensions for MyError {
|
||||
// lets define our base extensions
|
||||
fn extend(&self) -> FieldError {
|
||||
let extensions = match self {
|
||||
MyError::NotFound => json!({"code": "NOT_FOUND"}),
|
||||
MyError::ServerError(reason) => json!({ "reason": reason }),
|
||||
MyError::ErrorWithoutExtensions => {
|
||||
json!("This will be ignored since it does not represent an object.")
|
||||
}
|
||||
};
|
||||
|
||||
FieldError(format!("{}", self), Some(extensions))
|
||||
}
|
||||
}
|
||||
|
||||
struct QueryRoot;
|
||||
|
||||
#[Object]
|
||||
impl QueryRoot {
|
||||
// It works on foreign types without extensions as before
|
||||
#[field]
|
||||
async fn parse_without_extensions(&self) -> FieldResult<i32> {
|
||||
Ok("234a".parse()?)
|
||||
}
|
||||
|
||||
// Foreign types can be extended
|
||||
#[field]
|
||||
async fn parse_with_extensions(&self) -> FieldResult<i32> {
|
||||
Ok("234a"
|
||||
.parse()
|
||||
.map_err(|e: std::num::ParseIntError| e.extend_with(|_| json!({"code": 404})))?)
|
||||
}
|
||||
|
||||
// THIS does unfortunately NOT work because ErrorExtensions is implemented for &E and not E.
|
||||
// Which is necessary for the overwrite by the user.
|
||||
|
||||
//#[field]
|
||||
// async fn parse_with_extensions_result(&self) -> FieldResult<i32> {
|
||||
// Ok("234a".parse().extend_err(|_| json!({"code": 404}))?)
|
||||
// }
|
||||
|
||||
// Using our own types we can implement some base extensions
|
||||
#[field]
|
||||
async fn extend(&self) -> FieldResult<i32> {
|
||||
Err(MyError::NotFound.extend())?
|
||||
}
|
||||
|
||||
// Or on the result
|
||||
#[field]
|
||||
async fn extend_result(&self) -> FieldResult<i32> {
|
||||
Err(MyError::NotFound).extend()?
|
||||
}
|
||||
|
||||
// Base extensions can be further extended
|
||||
#[field]
|
||||
async fn more_extensions(&self) -> FieldResult<String> {
|
||||
// resolves to extensions: { "code": "NOT_FOUND", "reason": "my reason" }
|
||||
Err(MyError::NotFound.extend_with(|_e| json!({"reason": "my reason"})))?
|
||||
}
|
||||
|
||||
// works with results as well
|
||||
#[field]
|
||||
async fn more_extensions_on_result(&self) -> FieldResult<String> {
|
||||
// resolves to extensions: { "code": "NOT_FOUND", "reason": "my reason" }
|
||||
Err(MyError::NotFound).extend_err(|_e| json!({"reason": "my reason"}))?
|
||||
}
|
||||
|
||||
// extend_with is chainable
|
||||
#[field]
|
||||
async fn chainable_extensions(&self) -> FieldResult<String> {
|
||||
let err = MyError::NotFound
|
||||
.extend_with(|_| json!({"ext1": 1}))
|
||||
.extend_with(|_| json!({"ext2": 2}))
|
||||
.extend_with(|_| json!({"ext3": 3}));
|
||||
Err(err)?
|
||||
}
|
||||
|
||||
// extend_with overwrites keys which are already present
|
||||
#[field]
|
||||
async fn overwrite(&self) -> FieldResult<String> {
|
||||
Err(MyError::NotFound.extend_with(|_| json!({"code": "overwritten"})))?
|
||||
}
|
||||
}
|
||||
|
||||
async fn index(
|
||||
s: web::Data<Schema<QueryRoot, EmptyMutation, EmptySubscription>>,
|
||||
req: web::Json<GQLRequest>,
|
||||
) -> web::Json<GQLResponse> {
|
||||
web::Json(GQLResponse(
|
||||
req.into_inner()
|
||||
.into_query_builder(&s)
|
||||
.and_then(|builder| builder.execute())
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
async fn gql_playgound() -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(playground_source("/", None))
|
||||
}
|
||||
|
||||
async fn gql_graphiql() -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(graphiql_source("/"))
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
println!("Playground: http://localhost:8000");
|
||||
println!("Graphiql: http://localhost:8000/graphiql");
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(Schema::new(QueryRoot, EmptyMutation, EmptySubscription))
|
||||
.service(web::resource("/").guard(guard::Post()).to(index))
|
||||
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
|
||||
.service(
|
||||
web::resource("/graphiql")
|
||||
.guard(guard::Get())
|
||||
.to(gql_graphiql),
|
||||
)
|
||||
})
|
||||
.bind("127.0.0.1:8000")?
|
||||
.run()
|
||||
.await
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
use actix_web::{guard, web, App, HttpResponse, HttpServer};
|
||||
use async_graphql::http::{playground_source, GQLRequest, GQLResponse, IntoQueryBuilder};
|
||||
use async_graphql::{EmptyMutation, EmptySubscription, Object, Schema, SimpleObject, ID};
|
||||
use futures::TryFutureExt;
|
||||
|
||||
type MySchema = Schema<Query, EmptyMutation, EmptySubscription>;
|
||||
|
||||
#[SimpleObject]
|
||||
struct User {
|
||||
#[field]
|
||||
id: ID,
|
||||
|
||||
#[field]
|
||||
username: String,
|
||||
}
|
||||
|
||||
struct Query;
|
||||
|
||||
#[Object(extends)]
|
||||
impl Query {
|
||||
#[field]
|
||||
async fn me(&self) -> User {
|
||||
User {
|
||||
id: "1234".into(),
|
||||
username: "Me".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[entity]
|
||||
async fn find_user_by_id(&self, id: ID) -> User {
|
||||
let username = if id == "1234" {
|
||||
"Me".to_string()
|
||||
} else {
|
||||
format!("User {}", id)
|
||||
};
|
||||
User { id, username }
|
||||
}
|
||||
}
|
||||
|
||||
async fn index(s: web::Data<MySchema>, req: web::Json<GQLRequest>) -> web::Json<GQLResponse> {
|
||||
web::Json(GQLResponse(
|
||||
req.into_inner()
|
||||
.into_query_builder(&s)
|
||||
.and_then(|builder| builder.execute())
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
async fn gql_playgound() -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(playground_source("/", None))
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(schema.clone())
|
||||
.service(web::resource("/").guard(guard::Post()).to(index))
|
||||
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
|
||||
})
|
||||
.bind("127.0.0.1:4001")?
|
||||
.run()
|
||||
.await
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
use actix_web::{guard, web, App, HttpResponse, HttpServer};
|
||||
use async_graphql::http::{playground_source, GQLRequest, GQLResponse, IntoQueryBuilder};
|
||||
use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject};
|
||||
use futures::TryFutureExt;
|
||||
|
||||
type MySchema = Schema<Query, EmptyMutation, EmptySubscription>;
|
||||
|
||||
#[SimpleObject]
|
||||
struct Product {
|
||||
#[field]
|
||||
upc: String,
|
||||
|
||||
#[field]
|
||||
name: String,
|
||||
|
||||
#[field]
|
||||
price: i32,
|
||||
}
|
||||
|
||||
struct Query;
|
||||
|
||||
#[Object(extends)]
|
||||
impl Query {
|
||||
#[field]
|
||||
async fn top_products<'a>(&self, ctx: &'a Context<'_>) -> &'a Vec<Product> {
|
||||
ctx.data::<Vec<Product>>()
|
||||
}
|
||||
|
||||
#[entity]
|
||||
async fn find_product_by_upc<'a>(
|
||||
&self,
|
||||
ctx: &'a Context<'_>,
|
||||
upc: String,
|
||||
) -> Option<&'a Product> {
|
||||
let hats = ctx.data::<Vec<Product>>();
|
||||
hats.iter().find(|product| product.upc == upc)
|
||||
}
|
||||
}
|
||||
|
||||
async fn index(s: web::Data<MySchema>, req: web::Json<GQLRequest>) -> web::Json<GQLResponse> {
|
||||
web::Json(GQLResponse(
|
||||
req.into_inner()
|
||||
.into_query_builder(&s)
|
||||
.and_then(|builder| builder.execute())
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
async fn gql_playgound() -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(playground_source("/", None))
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let hats = vec![
|
||||
Product {
|
||||
upc: "top-1".to_string(),
|
||||
name: "Trilby".to_string(),
|
||||
price: 11,
|
||||
},
|
||||
Product {
|
||||
upc: "top-2".to_string(),
|
||||
name: "Fedora".to_string(),
|
||||
price: 22,
|
||||
},
|
||||
Product {
|
||||
upc: "top-3".to_string(),
|
||||
name: "Boater".to_string(),
|
||||
price: 33,
|
||||
},
|
||||
];
|
||||
|
||||
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
|
||||
.data(hats)
|
||||
.finish();
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(schema.clone())
|
||||
.service(web::resource("/").guard(guard::Post()).to(index))
|
||||
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
|
||||
})
|
||||
.bind("127.0.0.1:4002")?
|
||||
.run()
|
||||
.await
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
use actix_web::{guard, web, App, HttpResponse, HttpServer};
|
||||
use async_graphql::http::{playground_source, GQLRequest, GQLResponse, IntoQueryBuilder};
|
||||
use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject, ID};
|
||||
use futures::TryFutureExt;
|
||||
|
||||
type MySchema = Schema<Query, EmptyMutation, EmptySubscription>;
|
||||
|
||||
struct User {
|
||||
id: ID,
|
||||
}
|
||||
|
||||
#[Object(extends)]
|
||||
impl User {
|
||||
#[field(external)]
|
||||
async fn id(&self) -> &ID {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[field]
|
||||
async fn reviews<'a>(&self, ctx: &'a Context<'_>) -> Vec<&'a Review> {
|
||||
let reviews = ctx.data::<Vec<Review>>();
|
||||
reviews
|
||||
.iter()
|
||||
.filter(|review| review.author.id == self.id)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
struct Product {
|
||||
upc: String,
|
||||
}
|
||||
|
||||
#[Object(extends)]
|
||||
impl Product {
|
||||
#[field(external)]
|
||||
async fn upc(&self) -> &String {
|
||||
&self.upc
|
||||
}
|
||||
|
||||
#[field]
|
||||
async fn reviews<'a>(&self, ctx: &'a Context<'_>) -> Vec<&'a Review> {
|
||||
let reviews = ctx.data::<Vec<Review>>();
|
||||
reviews
|
||||
.iter()
|
||||
.filter(|review| review.product.upc == self.upc)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[SimpleObject]
|
||||
struct Review {
|
||||
#[field]
|
||||
body: String,
|
||||
|
||||
#[field(provides = "username")]
|
||||
author: User,
|
||||
|
||||
#[field]
|
||||
product: Product,
|
||||
}
|
||||
|
||||
struct Query;
|
||||
|
||||
#[Object]
|
||||
impl Query {
|
||||
#[entity]
|
||||
async fn find_user_by_id<'a>(&self, id: ID) -> User {
|
||||
User { id }
|
||||
}
|
||||
|
||||
#[entity]
|
||||
async fn find_product_by_upc<'a>(&self, upc: String) -> Product {
|
||||
Product { upc }
|
||||
}
|
||||
}
|
||||
|
||||
async fn index(s: web::Data<MySchema>, req: web::Json<GQLRequest>) -> web::Json<GQLResponse> {
|
||||
web::Json(GQLResponse(
|
||||
req.into_inner()
|
||||
.into_query_builder(&s)
|
||||
.and_then(|builder| builder.execute())
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
async fn gql_playgound() -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(playground_source("/", None))
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let reviews = vec![
|
||||
Review {
|
||||
body: "A highly effective form of birth control.".into(),
|
||||
author: User { id: "1234".into() },
|
||||
product: Product {
|
||||
upc: "top-1".to_string(),
|
||||
},
|
||||
},
|
||||
Review {
|
||||
body: "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.".into(),
|
||||
author: User { id: "1234".into() },
|
||||
product: Product {
|
||||
upc: "top-1".to_string(),
|
||||
},
|
||||
},
|
||||
Review {
|
||||
body: "This is the last straw. Hat you will wear. 11/10".into(),
|
||||
author: User { id: "7777".into() },
|
||||
product: Product {
|
||||
upc: "top-1".to_string(),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
|
||||
.data(reviews)
|
||||
.finish();
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(schema.clone())
|
||||
.service(web::resource("/").guard(guard::Post()).to(index))
|
||||
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
|
||||
})
|
||||
.bind("127.0.0.1:4003")?
|
||||
.run()
|
||||
.await
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
const { ApolloServer } = require('apollo-server');
|
||||
const { ApolloGateway } = require("@apollo/gateway");
|
||||
|
||||
const gateway = new ApolloGateway({
|
||||
serviceList: [
|
||||
{ name: 'accounts', url: 'http://localhost:4001' },
|
||||
{ name: 'products', url: 'http://localhost:4002' },
|
||||
{ name: 'reviews', url: 'http://localhost:4003' }
|
||||
],
|
||||
});
|
||||
|
||||
const server = new ApolloServer({
|
||||
gateway,
|
||||
subscriptions: false,
|
||||
});
|
||||
|
||||
server.listen().then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`);
|
||||
});
|
|
@ -1,10 +0,0 @@
|
|||
module.exports = {
|
||||
testEnvironment: "node",
|
||||
testMatch: ["<rootDir>/**/*-test.js"],
|
||||
testPathIgnorePatterns: ["<rootDir>/node_modules/"],
|
||||
moduleFileExtensions: ["js"],
|
||||
modulePaths: ["<rootDir>/node_modules"],
|
||||
// transform: {
|
||||
// '^.+\\.jsx?$': 'babel-jest',
|
||||
// },
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"name": "gateway",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@apollo/gateway": "^0.11.7",
|
||||
"apollo-server": "^2.9.16",
|
||||
"graphql": "^14.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"apollo-cache-inmemory": "^1.6.5",
|
||||
"apollo-client": "^2.6.8",
|
||||
"apollo-link-http": "^1.5.16",
|
||||
"jest": "^25.1.0",
|
||||
"node-fetch": "^2.6.0"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,25 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
function cleanup {
|
||||
kill "$ACCOUNTS_PID"
|
||||
kill "$PRODUCTS_PID"
|
||||
kill "$REVIEWS_PID"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cargo build --example federation-accounts
|
||||
cargo build --example federation-products
|
||||
cargo build --example federation-reviews
|
||||
|
||||
cargo run --example federation-accounts &
|
||||
ACCOUNTS_PID=$!
|
||||
|
||||
cargo run --example federation-products &
|
||||
PRODUCTS_PID=$!
|
||||
|
||||
cargo run --example federation-reviews &
|
||||
REVIEWS_PID=$!
|
||||
|
||||
sleep 3
|
||||
|
||||
node federation/index.js
|
|
@ -1,135 +0,0 @@
|
|||
mod model;
|
||||
|
||||
use model::Episode;
|
||||
pub use model::QueryRoot;
|
||||
use slab::Slab;
|
||||
use std::collections::HashMap;
|
||||
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -1,173 +0,0 @@
|
|||
use super::StarWars;
|
||||
use async_graphql::{Connection, Context, DataSource, EmptyEdgeFields, FieldResult};
|
||||
|
||||
#[async_graphql::Enum(desc = "One of the films in the Star Wars Trilogy")]
|
||||
pub enum Episode {
|
||||
#[item(desc = "Released in 1977.")]
|
||||
NewHope,
|
||||
|
||||
#[item(desc = "Released in 1980.")]
|
||||
Empire,
|
||||
|
||||
#[item(desc = "Released in 1983.")]
|
||||
Jedi,
|
||||
}
|
||||
|
||||
pub struct Human(usize);
|
||||
|
||||
#[async_graphql::Object(desc = "A humanoid creature in the Star Wars universe.")]
|
||||
impl Human {
|
||||
#[field(desc = "The id of the human.")]
|
||||
async fn id(&self, ctx: &Context<'_>) -> &str {
|
||||
ctx.data::<StarWars>().chars[self.0].id
|
||||
}
|
||||
|
||||
#[field(desc = "The name of the human.")]
|
||||
async fn name(&self, ctx: &Context<'_>) -> &str {
|
||||
ctx.data::<StarWars>().chars[self.0].name
|
||||
}
|
||||
|
||||
#[field(desc = "The friends of the human, or an empty list if they have none.")]
|
||||
async fn friends(&self, ctx: &Context<'_>) -> Vec<Character> {
|
||||
ctx.data::<StarWars>().chars[self.0]
|
||||
.friends
|
||||
.iter()
|
||||
.map(|id| Human(*id).into())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[field(desc = "Which movies they appear in.")]
|
||||
async fn appears_in<'a>(&self, ctx: &'a Context<'_>) -> &'a [Episode] {
|
||||
&ctx.data::<StarWars>().chars[self.0].appears_in
|
||||
}
|
||||
|
||||
#[field(desc = "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::<StarWars>().chars[self.0].home_planet
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Droid(usize);
|
||||
|
||||
#[async_graphql::Object(desc = "A mechanical creature in the Star Wars universe.")]
|
||||
impl Droid {
|
||||
#[field(desc = "The id of the droid.")]
|
||||
async fn id(&self, ctx: &Context<'_>) -> &str {
|
||||
ctx.data::<StarWars>().chars[self.0].id
|
||||
}
|
||||
|
||||
#[field(desc = "The name of the droid.")]
|
||||
async fn name(&self, ctx: &Context<'_>) -> &str {
|
||||
ctx.data::<StarWars>().chars[self.0].name
|
||||
}
|
||||
|
||||
#[field(desc = "The friends of the droid, or an empty list if they have none.")]
|
||||
async fn friends(&self, ctx: &Context<'_>) -> Vec<Character> {
|
||||
ctx.data::<StarWars>().chars[self.0]
|
||||
.friends
|
||||
.iter()
|
||||
.map(|id| Droid(*id).into())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[field(desc = "Which movies they appear in.")]
|
||||
async fn appears_in<'a>(&self, ctx: &'a Context<'_>) -> &'a [Episode] {
|
||||
&ctx.data::<StarWars>().chars[self.0].appears_in
|
||||
}
|
||||
|
||||
#[field(desc = "The primary function of the droid.")]
|
||||
async fn primary_function<'a>(&self, ctx: &'a Context<'_>) -> &'a Option<&'a str> {
|
||||
&ctx.data::<StarWars>().chars[self.0].primary_function
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QueryRoot;
|
||||
|
||||
#[async_graphql::Object]
|
||||
impl QueryRoot {
|
||||
#[field]
|
||||
async fn hero(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
#[arg(
|
||||
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::<StarWars>().luke).into()
|
||||
} else {
|
||||
Droid(ctx.data::<StarWars>().artoo).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[field]
|
||||
async fn human(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
#[arg(desc = "id of the human")] id: String,
|
||||
) -> Option<Human> {
|
||||
ctx.data::<StarWars>().human(&id).map(|id| Human(id))
|
||||
}
|
||||
|
||||
#[field]
|
||||
async fn humans(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
after: Option<String>,
|
||||
before: Option<String>,
|
||||
first: Option<i32>,
|
||||
last: Option<i32>,
|
||||
) -> FieldResult<Connection<Human, EmptyEdgeFields>> {
|
||||
let humans = ctx
|
||||
.data::<StarWars>()
|
||||
.humans()
|
||||
.iter()
|
||||
.map(|id| *id)
|
||||
.collect::<Vec<_>>();
|
||||
humans
|
||||
.as_slice()
|
||||
.query(ctx, after, before, first, last)
|
||||
.await
|
||||
.map(|connection| connection.map(|id| Human(*id)))
|
||||
}
|
||||
|
||||
#[field]
|
||||
async fn droid(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
#[arg(desc = "id of the droid")] id: String,
|
||||
) -> Option<Droid> {
|
||||
ctx.data::<StarWars>().droid(&id).map(|id| Droid(id))
|
||||
}
|
||||
|
||||
#[field]
|
||||
async fn droids(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
after: Option<String>,
|
||||
before: Option<String>,
|
||||
first: Option<i32>,
|
||||
last: Option<i32>,
|
||||
) -> FieldResult<Connection<Droid, EmptyEdgeFields>> {
|
||||
let droids = ctx
|
||||
.data::<StarWars>()
|
||||
.droids()
|
||||
.iter()
|
||||
.map(|id| *id)
|
||||
.collect::<Vec<_>>();
|
||||
droids
|
||||
.as_slice()
|
||||
.query(ctx, after, before, first, last)
|
||||
.await
|
||||
.map(|connection| connection.map(|id| Droid(*id)))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_graphql::Interface(
|
||||
field(name = "id", type = "&str", context),
|
||||
field(name = "name", type = "&str", context),
|
||||
field(name = "friends", type = "Vec<Character>", context),
|
||||
field(name = "appears_in", type = "&'ctx [Episode]", context)
|
||||
)]
|
||||
pub struct Character(Human, Droid);
|
|
@ -1,47 +0,0 @@
|
|||
mod starwars;
|
||||
|
||||
use async_graphql::http::{
|
||||
graphiql_source, playground_source, GQLRequest, GQLResponse, IntoQueryBuilder,
|
||||
};
|
||||
use async_graphql::{EmptyMutation, EmptySubscription, Schema};
|
||||
use futures::TryFutureExt;
|
||||
use mime;
|
||||
use tide::{self, Request, Response};
|
||||
|
||||
type StarWarsSchema = Schema<starwars::QueryRoot, EmptyMutation, EmptySubscription>;
|
||||
|
||||
async fn index(mut request: Request<StarWarsSchema>) -> Response {
|
||||
let gql_request: GQLRequest = request.body_json().await.unwrap();
|
||||
let schema = request.state();
|
||||
let gql_response = gql_request
|
||||
.into_query_builder(schema)
|
||||
.and_then(|builder| builder.execute())
|
||||
.await;
|
||||
Response::new(200)
|
||||
.body_json(&GQLResponse(gql_response))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn gql_playground(_request: Request<StarWarsSchema>) -> Response {
|
||||
Response::new(200)
|
||||
.body_string(playground_source("/", None))
|
||||
.set_mime(mime::TEXT_HTML_UTF_8)
|
||||
}
|
||||
async fn gql_graphiql(_request: Request<StarWarsSchema>) -> Response {
|
||||
Response::new(200)
|
||||
.body_string(graphiql_source("/"))
|
||||
.set_mime(mime::TEXT_HTML_UTF_8)
|
||||
}
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let mut app = tide::with_state(
|
||||
Schema::build(starwars::QueryRoot, EmptyMutation, EmptySubscription)
|
||||
.data(starwars::StarWars::new())
|
||||
.finish(),
|
||||
);
|
||||
app.at("/").post(index);
|
||||
app.at("/").get(gql_playground);
|
||||
app.at("/graphiql").get(gql_graphiql);
|
||||
app.listen("0.0.0.0:8000").await
|
||||
}
|
|
@ -305,7 +305,7 @@ impl From<ParseError> for Error {
|
|||
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RequestError {
|
||||
pub enum ParseRequestError {
|
||||
#[error("{0}")]
|
||||
Io(std::io::Error),
|
||||
|
||||
|
@ -353,7 +353,4 @@ pub enum Error {
|
|||
|
||||
#[error("Rule error")]
|
||||
Rule { errors: Vec<RuleError> },
|
||||
|
||||
#[error("Request error")]
|
||||
Request(#[from] RequestError),
|
||||
}
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
use crate::error::RequestError;
|
||||
use crate::http::multipart::{Multipart, PartData};
|
||||
use crate::http::{GQLRequest, IntoQueryBuilder};
|
||||
use crate::{Error, ObjectType, QueryBuilder, Result, Schema, SubscriptionType};
|
||||
use futures::{AsyncRead, AsyncReadExt};
|
||||
use mime::Mime;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Http request trait for GraphQL
|
||||
#[allow(missing_docs)]
|
||||
pub trait GQLHttpRequest {
|
||||
type Body: AsyncRead + Send + Unpin;
|
||||
|
||||
fn content_type(&self) -> Option<&str>;
|
||||
|
||||
fn into_body(self) -> Self::Body;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<R: GQLHttpRequest + Send + Sync> IntoQueryBuilder for R {
|
||||
async fn into_query_builder<Query, Mutation, Subscription>(
|
||||
mut self,
|
||||
schema: &Schema<Query, Mutation, Subscription>,
|
||||
) -> Result<QueryBuilder<Query, Mutation, Subscription>>
|
||||
where
|
||||
Query: ObjectType + Send + Sync + 'static,
|
||||
Mutation: ObjectType + Send + Sync + 'static,
|
||||
Subscription: SubscriptionType + Send + Sync + 'static,
|
||||
{
|
||||
if let Some(boundary) = self
|
||||
.content_type()
|
||||
.and_then(|value| value.parse::<Mime>().ok())
|
||||
.and_then(|ct| {
|
||||
if ct.essence_str() == mime::MULTIPART_FORM_DATA {
|
||||
ct.get_param("boundary")
|
||||
.map(|boundary| boundary.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
{
|
||||
// multipart
|
||||
let mut multipart = Multipart::parse(self.into_body(), boundary.as_str())
|
||||
.await
|
||||
.map_err(RequestError::InvalidMultipart)?;
|
||||
let gql_request: GQLRequest = {
|
||||
let part = multipart
|
||||
.remove("operations")
|
||||
.ok_or_else(|| Error::Request(RequestError::MissingOperatorsPart))?;
|
||||
let reader = part
|
||||
.create_reader()
|
||||
.map_err(|err| Error::Request(RequestError::PartData(err)))?;
|
||||
serde_json::from_reader(reader).map_err(RequestError::InvalidRequest)?
|
||||
};
|
||||
let mut map: HashMap<String, Vec<String>> = {
|
||||
let part = multipart
|
||||
.remove("map")
|
||||
.ok_or_else(|| Error::Request(RequestError::MissingMapPart))?;
|
||||
let reader = part
|
||||
.create_reader()
|
||||
.map_err(|err| Error::Request(RequestError::PartData(err)))?;
|
||||
serde_json::from_reader(reader).map_err(RequestError::InvalidFilesMap)?
|
||||
};
|
||||
|
||||
let mut builder = gql_request.into_query_builder(schema).await?;
|
||||
if !builder.is_upload() {
|
||||
return Err(RequestError::NotUpload.into());
|
||||
}
|
||||
|
||||
// read files
|
||||
for part in &multipart.parts {
|
||||
if let Some(name) = &part.name {
|
||||
if let Some(var_paths) = map.remove(name) {
|
||||
for var_path in var_paths {
|
||||
if let (Some(filename), PartData::File(path)) =
|
||||
(&part.filename, &part.data)
|
||||
{
|
||||
builder.set_upload(
|
||||
&var_path,
|
||||
&filename,
|
||||
part.content_type.as_deref(),
|
||||
path,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !map.is_empty() {
|
||||
return Err(RequestError::MissingFiles.into());
|
||||
}
|
||||
|
||||
if let Some(temp_dir) = multipart.temp_dir {
|
||||
builder.set_files_holder(temp_dir);
|
||||
}
|
||||
|
||||
Ok(builder)
|
||||
} else {
|
||||
let mut data = Vec::new();
|
||||
self.into_body()
|
||||
.read_to_end(&mut data)
|
||||
.await
|
||||
.map_err(RequestError::Io)?;
|
||||
let gql_request: GQLRequest =
|
||||
serde_json::from_slice(&data).map_err(RequestError::InvalidRequest)?;
|
||||
gql_request.into_query_builder(schema).await
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
use crate::http::multipart::{Multipart, PartData};
|
||||
use crate::http::GQLRequest;
|
||||
use crate::query::IntoQueryBuilder;
|
||||
use crate::{ParseRequestError, QueryBuilder};
|
||||
use futures::{AsyncRead, AsyncReadExt};
|
||||
use mime::Mime;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<CT, Body> IntoQueryBuilder for (Option<CT>, Body)
|
||||
where
|
||||
CT: AsRef<str> + Send,
|
||||
Body: AsyncRead + Send + Unpin,
|
||||
{
|
||||
async fn into_query_builder(mut self) -> std::result::Result<QueryBuilder, ParseRequestError> {
|
||||
if let Some(boundary) = self
|
||||
.0
|
||||
.and_then(|value| value.as_ref().parse::<Mime>().ok())
|
||||
.and_then(|ct| {
|
||||
if ct.essence_str() == mime::MULTIPART_FORM_DATA {
|
||||
ct.get_param("boundary")
|
||||
.map(|boundary| boundary.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
{
|
||||
// multipart
|
||||
let mut multipart = Multipart::parse(self.1, boundary.as_str())
|
||||
.await
|
||||
.map_err(ParseRequestError::InvalidMultipart)?;
|
||||
let gql_request: GQLRequest = {
|
||||
let part = multipart
|
||||
.remove("operations")
|
||||
.ok_or_else(|| ParseRequestError::MissingOperatorsPart)?;
|
||||
let reader = part.create_reader().map_err(ParseRequestError::PartData)?;
|
||||
serde_json::from_reader(reader).map_err(ParseRequestError::InvalidRequest)?
|
||||
};
|
||||
let mut map: HashMap<String, Vec<String>> = {
|
||||
let part = multipart
|
||||
.remove("map")
|
||||
.ok_or_else(|| ParseRequestError::MissingMapPart)?;
|
||||
let reader = part.create_reader().map_err(ParseRequestError::PartData)?;
|
||||
serde_json::from_reader(reader).map_err(ParseRequestError::InvalidFilesMap)?
|
||||
};
|
||||
|
||||
let mut builder = gql_request.into_query_builder().await?;
|
||||
|
||||
// read files
|
||||
for part in &multipart.parts {
|
||||
if let Some(name) = &part.name {
|
||||
if let Some(var_paths) = map.remove(name) {
|
||||
for var_path in var_paths {
|
||||
if let (Some(filename), PartData::File(path)) =
|
||||
(&part.filename, &part.data)
|
||||
{
|
||||
builder.set_upload(
|
||||
&var_path,
|
||||
&filename,
|
||||
part.content_type.as_deref(),
|
||||
path,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !map.is_empty() {
|
||||
return Err(ParseRequestError::MissingFiles);
|
||||
}
|
||||
|
||||
if let Some(temp_dir) = multipart.temp_dir {
|
||||
builder.set_files_holder(temp_dir);
|
||||
}
|
||||
|
||||
Ok(builder)
|
||||
} else {
|
||||
let mut data = Vec::new();
|
||||
self.1
|
||||
.read_to_end(&mut data)
|
||||
.await
|
||||
.map_err(ParseRequestError::Io)?;
|
||||
let gql_request: GQLRequest =
|
||||
serde_json::from_slice(&data).map_err(ParseRequestError::InvalidRequest)?;
|
||||
gql_request.into_query_builder().await
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,38 +1,24 @@
|
|||
//! A helper module that supports HTTP
|
||||
|
||||
mod graphiql_source;
|
||||
mod http_request;
|
||||
mod into_query_builder;
|
||||
mod multipart;
|
||||
mod playground_source;
|
||||
mod stream_body;
|
||||
mod token_reader;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
pub use graphiql_source::graphiql_source;
|
||||
pub use http_request::GQLHttpRequest;
|
||||
pub use playground_source::playground_source;
|
||||
pub use stream_body::StreamBody;
|
||||
|
||||
use crate::{
|
||||
Error, ObjectType, QueryBuilder, QueryError, QueryResponse, Result, Schema, SubscriptionType,
|
||||
Variables,
|
||||
};
|
||||
use crate::query::IntoQueryBuilder;
|
||||
use crate::{Error, ParseRequestError, QueryBuilder, QueryError, QueryResponse, Result, Variables};
|
||||
use graphql_parser::Pos;
|
||||
use serde::ser::{SerializeMap, SerializeSeq};
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[async_trait::async_trait]
|
||||
pub trait IntoQueryBuilder {
|
||||
async fn into_query_builder<Query, Mutation, Subscription>(
|
||||
self,
|
||||
schema: &Schema<Query, Mutation, Subscription>,
|
||||
) -> Result<QueryBuilder<Query, Mutation, Subscription>>
|
||||
where
|
||||
Query: ObjectType + Send + Sync + 'static,
|
||||
Mutation: ObjectType + Send + Sync + 'static,
|
||||
Subscription: SubscriptionType + Send + Sync + 'static;
|
||||
}
|
||||
|
||||
/// Deserializable GraphQL Request object
|
||||
#[derive(Deserialize, Clone, PartialEq, Debug)]
|
||||
pub struct GQLRequest {
|
||||
|
@ -49,16 +35,8 @@ pub struct GQLRequest {
|
|||
|
||||
#[async_trait::async_trait]
|
||||
impl IntoQueryBuilder for GQLRequest {
|
||||
async fn into_query_builder<Query, Mutation, Subscription>(
|
||||
self,
|
||||
schema: &Schema<Query, Mutation, Subscription>,
|
||||
) -> Result<QueryBuilder<Query, Mutation, Subscription>>
|
||||
where
|
||||
Query: ObjectType + Send + Sync + 'static,
|
||||
Mutation: ObjectType + Send + Sync + 'static,
|
||||
Subscription: SubscriptionType + Send + Sync + 'static,
|
||||
{
|
||||
let mut builder = schema.query(&self.query)?;
|
||||
async fn into_query_builder(self) -> std::result::Result<QueryBuilder, ParseRequestError> {
|
||||
let mut builder = QueryBuilder::new(self.query);
|
||||
if let Some(operation_name) = self.operation_name {
|
||||
builder = builder.operator_name(operation_name);
|
||||
}
|
||||
|
@ -160,13 +138,6 @@ impl<'a> Serialize for GQLError<'a> {
|
|||
}
|
||||
seq.end()
|
||||
}
|
||||
Error::Request(err) => {
|
||||
let mut seq = serializer.serialize_seq(Some(1))?;
|
||||
seq.serialize_element(&serde_json::json!({
|
||||
"message": err.to_string(),
|
||||
}))?;
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -242,6 +213,7 @@ mod tests {
|
|||
let resp = GQLResponse(Ok(QueryResponse {
|
||||
data: json!({"ok": true}),
|
||||
extensions: None,
|
||||
cache_control: Default::default(),
|
||||
}));
|
||||
assert_eq!(
|
||||
serde_json::to_value(resp).unwrap(),
|
||||
|
|
|
@ -1,40 +1,30 @@
|
|||
use actix_web::web::Payload;
|
||||
use async_graphql::http::GQLHttpRequest;
|
||||
use bytes::{Buf, Bytes};
|
||||
use futures::io::{Error, ErrorKind, Result};
|
||||
use futures::task::{Context, Poll};
|
||||
use futures::{AsyncRead, StreamExt};
|
||||
use futures::{AsyncRead, Stream, StreamExt};
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
use std::pin::Pin;
|
||||
|
||||
pub struct RequestWrapper(pub Option<String>, pub Payload);
|
||||
/// An Adapter for bytes stream to `AsyncRead`
|
||||
pub struct StreamBody<S> {
|
||||
s: S,
|
||||
remain_bytes: Option<Bytes>,
|
||||
}
|
||||
|
||||
unsafe impl Send for RequestWrapper {}
|
||||
unsafe impl Sync for RequestWrapper {}
|
||||
|
||||
impl GQLHttpRequest for RequestWrapper {
|
||||
type Body = PayloadReader;
|
||||
|
||||
fn content_type(&self) -> Option<&str> {
|
||||
self.0.as_deref()
|
||||
}
|
||||
|
||||
fn into_body(self) -> Self::Body {
|
||||
PayloadReader {
|
||||
payload: self.1,
|
||||
impl<S> StreamBody<S> {
|
||||
#[allow(missing_docs)]
|
||||
pub fn new(s: S) -> Self {
|
||||
Self {
|
||||
s,
|
||||
remain_bytes: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PayloadReader {
|
||||
payload: Payload,
|
||||
remain_bytes: Option<Bytes>,
|
||||
}
|
||||
|
||||
unsafe impl Send for PayloadReader {}
|
||||
unsafe impl Sync for PayloadReader {}
|
||||
|
||||
impl AsyncRead for PayloadReader {
|
||||
impl<S, E, D> AsyncRead for StreamBody<S>
|
||||
where
|
||||
D: Buf,
|
||||
S: Stream<Item = std::result::Result<D, E>> + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
|
@ -49,9 +39,9 @@ impl AsyncRead for PayloadReader {
|
|||
}
|
||||
return Poll::Ready(Ok(data.len()));
|
||||
} else {
|
||||
match self.payload.poll_next_unpin(cx) {
|
||||
Poll::Ready(Some(Ok(bytes))) => {
|
||||
self.remain_bytes = Some(bytes);
|
||||
match self.s.poll_next_unpin(cx) {
|
||||
Poll::Ready(Some(Ok(mut bytes))) => {
|
||||
self.remain_bytes = Some(bytes.to_bytes());
|
||||
}
|
||||
Poll::Ready(Some(Err(_))) => {
|
||||
return Poll::Ready(Err(Error::from(ErrorKind::InvalidData)))
|
|
@ -106,10 +106,12 @@ pub mod http;
|
|||
|
||||
pub use base::{Scalar, Type};
|
||||
pub use context::{Context, Environment, QueryPathNode, QueryPathSegment, Variables};
|
||||
pub use error::{Error, ErrorExtensions, FieldError, FieldResult, QueryError, ResultExt};
|
||||
pub use error::{
|
||||
Error, ErrorExtensions, FieldError, FieldResult, ParseRequestError, QueryError, ResultExt,
|
||||
};
|
||||
pub use graphql_parser::query::Value;
|
||||
pub use graphql_parser::Pos;
|
||||
pub use query::{QueryBuilder, QueryResponse};
|
||||
pub use query::{IntoQueryBuilder, QueryBuilder, QueryResponse};
|
||||
pub use registry::CacheControl;
|
||||
pub use scalars::{Any, ID};
|
||||
pub use schema::Schema;
|
||||
|
|
210
src/query.rs
210
src/query.rs
|
@ -1,19 +1,27 @@
|
|||
use crate::context::Data;
|
||||
use crate::extensions::BoxExtension;
|
||||
use crate::error::ParseRequestError;
|
||||
use crate::mutation_resolver::do_mutation_resolve;
|
||||
use crate::registry::CacheControl;
|
||||
use crate::validation::{check_rules, CheckResult};
|
||||
use crate::{do_resolve, ContextBase, Error, Result, Schema};
|
||||
use crate::{ObjectType, QueryError, Variables};
|
||||
use graphql_parser::query::{
|
||||
Definition, Document, OperationDefinition, SelectionSet, VariableDefinition,
|
||||
};
|
||||
use graphql_parser::Pos;
|
||||
use graphql_parser::{parse_query, Pos};
|
||||
use itertools::Itertools;
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use tempdir::TempDir;
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[async_trait::async_trait]
|
||||
pub trait IntoQueryBuilder {
|
||||
async fn into_query_builder(self) -> std::result::Result<QueryBuilder, ParseRequestError>;
|
||||
}
|
||||
|
||||
/// Query response
|
||||
pub struct QueryResponse {
|
||||
/// Data of query result
|
||||
|
@ -21,59 +29,30 @@ pub struct QueryResponse {
|
|||
|
||||
/// Extensions result
|
||||
pub extensions: Option<serde_json::Map<String, serde_json::Value>>,
|
||||
|
||||
/// Cache control value
|
||||
pub cache_control: CacheControl,
|
||||
}
|
||||
|
||||
/// Query builder
|
||||
pub struct QueryBuilder<Query, Mutation, Subscription> {
|
||||
pub(crate) schema: Schema<Query, Mutation, Subscription>,
|
||||
pub(crate) extensions: Vec<BoxExtension>,
|
||||
pub(crate) document: Document,
|
||||
pub struct QueryBuilder {
|
||||
pub(crate) query_source: String,
|
||||
pub(crate) operation_name: Option<String>,
|
||||
pub(crate) variables: Variables,
|
||||
pub(crate) ctx_data: Option<Data>,
|
||||
pub(crate) cache_control: CacheControl,
|
||||
pub(crate) files_holder: Option<TempDir>,
|
||||
}
|
||||
|
||||
impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription> {
|
||||
fn current_operation(&self) -> Option<(&SelectionSet, &[VariableDefinition], bool)> {
|
||||
for definition in &self.document.definitions {
|
||||
match definition {
|
||||
Definition::Operation(operation_definition) => match operation_definition {
|
||||
OperationDefinition::SelectionSet(s) => {
|
||||
return Some((s, &[], true));
|
||||
}
|
||||
OperationDefinition::Query(query)
|
||||
if query.name.is_none()
|
||||
|| self.operation_name.is_none()
|
||||
|| query.name.as_deref() == self.operation_name.as_deref() =>
|
||||
{
|
||||
return Some((&query.selection_set, &query.variable_definitions, true));
|
||||
}
|
||||
OperationDefinition::Mutation(mutation)
|
||||
if mutation.name.is_none()
|
||||
|| self.operation_name.is_none()
|
||||
|| mutation.name.as_deref() == self.operation_name.as_deref() =>
|
||||
{
|
||||
return Some((
|
||||
&mutation.selection_set,
|
||||
&mutation.variable_definitions,
|
||||
false,
|
||||
));
|
||||
}
|
||||
OperationDefinition::Subscription(subscription)
|
||||
if subscription.name.is_none()
|
||||
|| self.operation_name.is_none()
|
||||
|| subscription.name.as_deref() == self.operation_name.as_deref() =>
|
||||
{
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Definition::Fragment(_) => {}
|
||||
}
|
||||
impl QueryBuilder {
|
||||
/// Create query builder with query source.
|
||||
pub fn new<T: Into<String>>(query_source: T) -> QueryBuilder {
|
||||
QueryBuilder {
|
||||
query_source: query_source.into(),
|
||||
operation_name: None,
|
||||
variables: Default::default(),
|
||||
ctx_data: None,
|
||||
files_holder: None,
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Specify the operation name.
|
||||
|
@ -101,25 +80,6 @@ impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
|
|||
self
|
||||
}
|
||||
|
||||
/// Detects whether any parameter contains the Upload type
|
||||
pub fn is_upload(&self) -> bool {
|
||||
if let Some((_, variable_definitions, _)) = self.current_operation() {
|
||||
for d in variable_definitions {
|
||||
if let Some(ty) = self
|
||||
.schema
|
||||
.0
|
||||
.registry
|
||||
.concrete_type_by_parsed_type(&d.var_type)
|
||||
{
|
||||
if ty.name() == "Upload" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Set file holder
|
||||
pub fn set_files_holder(&mut self, files_holder: TempDir) {
|
||||
self.files_holder = Some(files_holder);
|
||||
|
@ -138,21 +98,64 @@ impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
|
|||
}
|
||||
|
||||
/// Execute the query.
|
||||
pub async fn execute(self) -> Result<QueryResponse>
|
||||
pub async fn execute<Query, Mutation, Subscription>(
|
||||
self,
|
||||
schema: &Schema<Query, Mutation, Subscription>,
|
||||
) -> Result<QueryResponse>
|
||||
where
|
||||
Query: ObjectType + Send + Sync,
|
||||
Mutation: ObjectType + Send + Sync,
|
||||
{
|
||||
// create extension instances
|
||||
let extensions = schema
|
||||
.0
|
||||
.extensions
|
||||
.iter()
|
||||
.map(|factory| factory())
|
||||
.collect_vec();
|
||||
|
||||
// parse query source
|
||||
extensions
|
||||
.iter()
|
||||
.for_each(|e| e.parse_start(&self.query_source));
|
||||
let document = parse_query(&self.query_source).map_err(Into::<Error>::into)?;
|
||||
extensions.iter().for_each(|e| e.parse_end());
|
||||
|
||||
// check rules
|
||||
extensions.iter().for_each(|e| e.validation_start());
|
||||
let CheckResult {
|
||||
cache_control,
|
||||
complexity,
|
||||
depth,
|
||||
} = check_rules(&schema.0.registry, &document, schema.0.validation_mode)?;
|
||||
extensions.iter().for_each(|e| e.validation_end());
|
||||
|
||||
// check limit
|
||||
if let Some(limit_complexity) = schema.0.complexity {
|
||||
if complexity > limit_complexity {
|
||||
return Err(QueryError::TooComplex.into_error(Pos::default()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(limit_depth) = schema.0.depth {
|
||||
if depth > limit_depth {
|
||||
return Err(QueryError::TooDeep.into_error(Pos::default()));
|
||||
}
|
||||
}
|
||||
|
||||
// execute
|
||||
let resolve_id = AtomicUsize::default();
|
||||
let mut fragments = HashMap::new();
|
||||
let (selection_set, variable_definitions, is_query) =
|
||||
self.current_operation().ok_or_else(|| Error::Query {
|
||||
pos: Pos::default(),
|
||||
path: None,
|
||||
err: QueryError::MissingOperation,
|
||||
current_operation(&document, self.operation_name.as_deref()).ok_or_else(|| {
|
||||
Error::Query {
|
||||
pos: Pos::default(),
|
||||
path: None,
|
||||
err: QueryError::MissingOperation,
|
||||
}
|
||||
})?;
|
||||
|
||||
for definition in &self.document.definitions {
|
||||
for definition in &document.definitions {
|
||||
if let Definition::Fragment(fragment) = &definition {
|
||||
fragments.insert(fragment.name.clone(), fragment.clone());
|
||||
}
|
||||
|
@ -161,29 +164,29 @@ impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
|
|||
let ctx = ContextBase {
|
||||
path_node: None,
|
||||
resolve_id: &resolve_id,
|
||||
extensions: &self.extensions,
|
||||
extensions: &extensions,
|
||||
item: selection_set,
|
||||
variables: &self.variables,
|
||||
variable_definitions,
|
||||
registry: &self.schema.0.registry,
|
||||
data: &self.schema.0.data,
|
||||
registry: &schema.0.registry,
|
||||
data: &schema.0.data,
|
||||
ctx_data: self.ctx_data.as_ref(),
|
||||
fragments: &fragments,
|
||||
};
|
||||
|
||||
self.extensions.iter().for_each(|e| e.execution_start());
|
||||
extensions.iter().for_each(|e| e.execution_start());
|
||||
let data = if is_query {
|
||||
do_resolve(&ctx, &self.schema.0.query).await?
|
||||
do_resolve(&ctx, &schema.0.query).await?
|
||||
} else {
|
||||
do_mutation_resolve(&ctx, &self.schema.0.mutation).await?
|
||||
do_mutation_resolve(&ctx, &schema.0.mutation).await?
|
||||
};
|
||||
self.extensions.iter().for_each(|e| e.execution_end());
|
||||
extensions.iter().for_each(|e| e.execution_end());
|
||||
|
||||
let res = QueryResponse {
|
||||
data,
|
||||
extensions: if !self.extensions.is_empty() {
|
||||
extensions: if !extensions.is_empty() {
|
||||
Some(
|
||||
self.extensions
|
||||
extensions
|
||||
.iter()
|
||||
.map(|e| (e.name().to_string(), e.result()))
|
||||
.collect::<serde_json::Map<_, _>>(),
|
||||
|
@ -191,12 +194,51 @@ impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
|
|||
} else {
|
||||
None
|
||||
},
|
||||
cache_control,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Get cache control value
|
||||
pub fn cache_control(&self) -> CacheControl {
|
||||
self.cache_control
|
||||
}
|
||||
}
|
||||
|
||||
fn current_operation<'a>(
|
||||
document: &'a Document,
|
||||
operation_name: Option<&str>,
|
||||
) -> Option<(&'a SelectionSet, &'a [VariableDefinition], bool)> {
|
||||
for definition in &document.definitions {
|
||||
match definition {
|
||||
Definition::Operation(operation_definition) => match operation_definition {
|
||||
OperationDefinition::SelectionSet(s) => {
|
||||
return Some((s, &[], true));
|
||||
}
|
||||
OperationDefinition::Query(query)
|
||||
if query.name.is_none()
|
||||
|| operation_name.is_none()
|
||||
|| query.name.as_deref() == operation_name.as_deref() =>
|
||||
{
|
||||
return Some((&query.selection_set, &query.variable_definitions, true));
|
||||
}
|
||||
OperationDefinition::Mutation(mutation)
|
||||
if mutation.name.is_none()
|
||||
|| operation_name.is_none()
|
||||
|| mutation.name.as_deref() == operation_name.as_deref() =>
|
||||
{
|
||||
return Some((
|
||||
&mutation.selection_set,
|
||||
&mutation.variable_definitions,
|
||||
false,
|
||||
));
|
||||
}
|
||||
OperationDefinition::Subscription(subscription)
|
||||
if subscription.name.is_none()
|
||||
|| operation_name.is_none()
|
||||
|| subscription.name.as_deref() == operation_name.as_deref() =>
|
||||
{
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Definition::Fragment(_) => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
|
@ -129,21 +129,21 @@ pub struct EnumValue {
|
|||
/// impl QueryRoot {
|
||||
/// #[field(cache_control(max_age = 30))]
|
||||
/// async fn value1(&self) -> i32 {
|
||||
/// unimplemented!()
|
||||
/// 0
|
||||
/// }
|
||||
///
|
||||
/// #[field(cache_control(private))]
|
||||
/// async fn value2(&self) -> i32 {
|
||||
/// unimplemented!()
|
||||
/// 0
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[async_std::main]
|
||||
/// async fn main() {
|
||||
/// let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
|
||||
/// assert_eq!(schema.query("{ value1 }").unwrap().cache_control(), CacheControl { public: true, max_age: 30 });
|
||||
/// assert_eq!(schema.query("{ value2 }").unwrap().cache_control(), CacheControl { public: false, max_age: 60 });
|
||||
/// assert_eq!(schema.query("{ value1 value2 }").unwrap().cache_control(), CacheControl { public: false, max_age: 30 });
|
||||
/// assert_eq!(schema.execute("{ value1 }").await.unwrap().cache_control, CacheControl { public: true, max_age: 30 });
|
||||
/// assert_eq!(schema.execute("{ value2 }").await.unwrap().cache_control, CacheControl { public: false, max_age: 60 });
|
||||
/// assert_eq!(schema.execute("{ value1 value2 }").await.unwrap().cache_control, CacheControl { public: false, max_age: 30 });
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
|
|
|
@ -5,17 +5,16 @@ use crate::query::QueryBuilder;
|
|||
use crate::registry::{Directive, InputValue, Registry};
|
||||
use crate::subscription::{create_connection, create_subscription_stream, SubscriptionTransport};
|
||||
use crate::types::QueryRoot;
|
||||
use crate::validation::{check_rules, CheckResult, ValidationMode};
|
||||
use crate::validation::{check_rules, ValidationMode};
|
||||
use crate::{
|
||||
ContextSelectionSet, Error, ObjectType, Pos, QueryError, QueryResponse, Result,
|
||||
SubscriptionStream, SubscriptionType, Type, Variables,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures::channel::mpsc;
|
||||
use futures::{Stream, TryFutureExt};
|
||||
use futures::Stream;
|
||||
use graphql_parser::parse_query;
|
||||
use graphql_parser::query::{Definition, OperationDefinition};
|
||||
use itertools::Itertools;
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
|
@ -224,55 +223,9 @@ where
|
|||
&self.0.registry
|
||||
}
|
||||
|
||||
/// Start a query and return `QueryBuilder`.
|
||||
pub fn query(&self, source: &str) -> Result<QueryBuilder<Query, Mutation, Subscription>> {
|
||||
let extensions = self
|
||||
.0
|
||||
.extensions
|
||||
.iter()
|
||||
.map(|factory| factory())
|
||||
.collect_vec();
|
||||
extensions.iter().for_each(|e| e.parse_start(source));
|
||||
let document = parse_query(source).map_err(Into::<Error>::into)?;
|
||||
extensions.iter().for_each(|e| e.parse_end());
|
||||
|
||||
extensions.iter().for_each(|e| e.validation_start());
|
||||
let CheckResult {
|
||||
cache_control,
|
||||
complexity,
|
||||
depth,
|
||||
} = check_rules(&self.0.registry, &document, self.0.validation_mode)?;
|
||||
extensions.iter().for_each(|e| e.validation_end());
|
||||
|
||||
if let Some(limit_complexity) = self.0.complexity {
|
||||
if complexity > limit_complexity {
|
||||
return Err(QueryError::TooComplex.into_error(Pos::default()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(limit_depth) = self.0.depth {
|
||||
if depth > limit_depth {
|
||||
return Err(QueryError::TooDeep.into_error(Pos::default()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(QueryBuilder {
|
||||
extensions,
|
||||
schema: self.clone(),
|
||||
document,
|
||||
operation_name: None,
|
||||
variables: Default::default(),
|
||||
ctx_data: None,
|
||||
cache_control,
|
||||
files_holder: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute query without create the `QueryBuilder`.
|
||||
pub async fn execute(&self, source: &str) -> Result<QueryResponse> {
|
||||
futures::future::ready(self.query(source))
|
||||
.and_then(|builder| builder.execute())
|
||||
.await
|
||||
pub async fn execute(&self, query_source: &str) -> Result<QueryResponse> {
|
||||
QueryBuilder::new(query_source).execute(self).await
|
||||
}
|
||||
|
||||
/// Create subscription stream, typically called inside the `SubscriptionTransport::handle_request` method
|
||||
|
|
|
@ -87,7 +87,9 @@ pub struct SubscriptionStream<Query, Mutation, Subscription, T: SubscriptionTran
|
|||
streams: SubscriptionStreams,
|
||||
rx_bytes: mpsc::Receiver<Bytes>,
|
||||
handle_request_fut: Option<
|
||||
Pin<Box<dyn Future<Output = std::result::Result<Option<Bytes>, T::Error>> + 'static>>,
|
||||
Pin<
|
||||
Box<dyn Future<Output = std::result::Result<Option<Bytes>, T::Error>> + Send + 'static>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
|
|
|
@ -114,6 +114,7 @@ impl SubscriptionTransport for WebSocketTransport {
|
|||
serde_json::to_value(GQLResponse(Ok(QueryResponse {
|
||||
data: value,
|
||||
extensions: None,
|
||||
cache_control: Default::default(),
|
||||
})))
|
||||
.unwrap(),
|
||||
),
|
||||
|
|
|
@ -55,7 +55,7 @@ pub async fn test_enum_type() {
|
|||
|
||||
#[async_std::test]
|
||||
pub async fn test_enum_derive_and_item_attributes() {
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
#[async_graphql::Enum]
|
||||
#[derive(Deserialize, PartialEq, Debug)]
|
||||
|
|
|
@ -96,7 +96,7 @@ pub async fn test_input_object_default_value() {
|
|||
|
||||
#[async_std::test]
|
||||
pub async fn test_inputobject_derive_and_item_attributes() {
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
#[async_graphql::InputObject]
|
||||
#[derive(Deserialize, PartialEq, Debug)]
|
||||
|
|
Loading…
Reference in New Issue