1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
//! Async-graphql integration with Tide
//!
//! Tide [does not support websockets](https://github.com/http-rs/tide/issues/67), so you can't use
//! subscriptions with it.
//!
//! # Examples
//! *[Full Example](<https://github.com/async-graphql/examples/blob/master/tide/starwars/src/main.rs>)*

#![warn(missing_docs)]
#![allow(clippy::type_complexity)]
#![allow(clippy::needless_doctest_main)]
#![forbid(unsafe_code)]

use async_graphql::http::MultipartOptions;
use async_graphql::{ObjectType, ParseRequestError, Schema, SubscriptionType};
use tide::utils::async_trait;
use tide::{
    http::{
        headers::{self, HeaderValue},
        Method,
    },
    Body, Request, Response, StatusCode,
};

/// Create a new GraphQL endpoint with the schema.
///
/// Default multipart options are used and batch operations are supported.
pub fn endpoint<Query, Mutation, Subscription>(
    schema: Schema<Query, Mutation, Subscription>,
) -> Endpoint<Query, Mutation, Subscription> {
    Endpoint {
        schema,
        opts: MultipartOptions::default(),
        batch: true,
    }
}

/// A GraphQL endpoint.
///
/// This is created with the [`endpoint`](fn.endpoint.html) function.
#[non_exhaustive]
pub struct Endpoint<Query, Mutation, Subscription> {
    /// The schema of the endpoint.
    pub schema: Schema<Query, Mutation, Subscription>,
    /// The multipart options of the endpoint.
    pub opts: MultipartOptions,
    /// Whether to support batch requests in the endpoint.
    pub batch: bool,
}

impl<Query, Mutation, Subscription> Endpoint<Query, Mutation, Subscription> {
    /// Set the multipart options of the endpoint.
    #[must_use]
    pub fn multipart_opts(self, opts: MultipartOptions) -> Self {
        Self { opts, ..self }
    }
    /// Set whether batch requests are supported in the endpoint.
    #[must_use]
    pub fn batch(self, batch: bool) -> Self {
        Self { batch, ..self }
    }
}

// Manual impl to remove bounds on generics
impl<Query, Mutation, Subscription> Clone for Endpoint<Query, Mutation, Subscription> {
    fn clone(&self) -> Self {
        Self {
            schema: self.schema.clone(),
            opts: self.opts,
            batch: self.batch,
        }
    }
}

#[async_trait]
impl<Query, Mutation, Subscription, TideState> tide::Endpoint<TideState>
    for Endpoint<Query, Mutation, Subscription>
where
    Query: ObjectType + Send + Sync + 'static,
    Mutation: ObjectType + Send + Sync + 'static,
    Subscription: SubscriptionType + Send + Sync + 'static,
    TideState: Clone + Send + Sync + 'static,
{
    async fn call(&self, request: Request<TideState>) -> tide::Result {
        respond(
            self.schema
                .execute_batch(if self.batch {
                    receive_batch_request_opts(request, self.opts).await
                } else {
                    receive_request_opts(request, self.opts)
                        .await
                        .map(Into::into)
                }?)
                .await,
        )
    }
}

/// Convert a Tide request to a GraphQL request.
pub async fn receive_request<State: Clone + Send + Sync + 'static>(
    request: Request<State>,
) -> tide::Result<async_graphql::Request> {
    receive_request_opts(request, Default::default()).await
}

/// Convert a Tide request to a GraphQL request with options on how to receive multipart.
pub async fn receive_request_opts<State: Clone + Send + Sync + 'static>(
    request: Request<State>,
    opts: MultipartOptions,
) -> tide::Result<async_graphql::Request> {
    receive_batch_request_opts(request, opts)
        .await?
        .into_single()
        .map_err(|e| tide::Error::new(StatusCode::BadRequest, e))
}

/// Convert a Tide request to a GraphQL batch request.
pub async fn receive_batch_request<State: Clone + Send + Sync + 'static>(
    request: Request<State>,
) -> tide::Result<async_graphql::BatchRequest> {
    receive_batch_request_opts(request, Default::default()).await
}

/// Convert a Tide request to a GraphQL batch request with options on how to receive multipart.
pub async fn receive_batch_request_opts<State: Clone + Send + Sync + 'static>(
    mut request: Request<State>,
    opts: MultipartOptions,
) -> tide::Result<async_graphql::BatchRequest> {
    if request.method() == Method::Get {
        request.query::<async_graphql::Request>().map(Into::into)
    } else if request.method() == Method::Post {
        let body = request.take_body();
        let content_type = request
            .header(headers::CONTENT_TYPE)
            .and_then(|values| values.get(0))
            .map(HeaderValue::as_str);

        async_graphql::http::receive_batch_body(content_type, body, opts)
            .await
            .map_err(|e| {
                tide::Error::new(
                    match &e {
                        ParseRequestError::PayloadTooLarge => StatusCode::PayloadTooLarge,
                        _ => StatusCode::BadRequest,
                    },
                    e,
                )
            })
    } else {
        Err(tide::Error::from_str(
            StatusCode::MethodNotAllowed,
            "GraphQL only supports GET and POST requests",
        ))
    }
}

/// Convert a GraphQL response to a Tide response.
pub fn respond(gql: impl Into<async_graphql::BatchResponse>) -> tide::Result {
    let gql = gql.into();

    let mut response = Response::new(StatusCode::Ok);
    if gql.is_ok() {
        if let Some(cache_control) = gql.cache_control().value() {
            response.insert_header(headers::CACHE_CONTROL, cache_control);
        }
    }
    response.set_body(Body::from_json(&gql)?);
    Ok(response)
}