Skip to main content

hive_router_plan_executor/plugins/hooks/
on_graphql_params.rs

1use core::fmt;
2
3use std::collections::HashMap;
4
5use ntex::util::Bytes;
6use serde::Serialize;
7use serde::{de, Deserialize, Deserializer};
8use sonic_rs::Value;
9
10use crate::plugin_context::PluginContext;
11use crate::plugin_context::RouterHttpRequest;
12use crate::plugin_trait::EndHookPayload;
13use crate::plugin_trait::EndHookResult;
14use crate::plugin_trait::StartHookPayload;
15use crate::plugin_trait::StartHookResult;
16use ntex::http::Response;
17
18#[derive(Debug, Default, Serialize)]
19/// The GraphQL parameters parsed from the HTTP request body by the router.
20/// This includes the `query`, `operationName`, `variables`, and `extensions`
21/// [Learn more about GraphQL-over-HTTP params](https://graphql.org/learn/serving-over-http/#request-format)
22pub struct GraphQLParams {
23    #[serde(skip_serializing_if = "Option::is_none")]
24    /// The GraphQL query string parsed from the HTTP request body by the router
25    /// This contains the source text of a GraphQL query, mutation, or subscription sent by the client in the request body.
26    /// It can be `None` if the client did not send a query string in the request body.
27    pub query: Option<String>,
28    #[serde(rename = "operationName", skip_serializing_if = "Option::is_none")]
29    /// The operation name parsed from the HTTP request body by the router
30    /// This is the name of the operation that the client wants to execute, sent in the request body.
31    /// It is optional and can be `None` if the client did not specify an operation
32    pub operation_name: Option<String>,
33    #[serde(skip_serializing_if = "HashMap::is_empty")]
34    /// The variables map parsed from the HTTP request body by the router
35    /// This is a map of variable names to their values sent by the client in the request
36    /// [Learn more about GraphQL variables](https://graphql.org/learn/queries/#variables)
37    pub variables: HashMap<String, Value>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub extensions: Option<HashMap<String, Value>>,
40}
41
42// Workaround for https://github.com/cloudwego/sonic-rs/issues/114
43
44impl<'de> Deserialize<'de> for GraphQLParams {
45    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
46    where
47        D: Deserializer<'de>,
48    {
49        struct GraphQLParamsVisitor;
50
51        impl<'de> de::Visitor<'de> for GraphQLParamsVisitor {
52            type Value = GraphQLParams;
53
54            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
55                formatter.write_str("a map for GraphQLParams")
56            }
57
58            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
59            where
60                A: de::MapAccess<'de>,
61            {
62                let mut query = None;
63                let mut operation_name = None;
64                let mut variables: Option<HashMap<String, Value>> = None;
65                let mut extensions: Option<HashMap<String, Value>> = None;
66                let mut extra_params = HashMap::new();
67
68                while let Some(key) = map.next_key::<String>()? {
69                    match key.as_str() {
70                        "query" => {
71                            if query.is_some() {
72                                return Err(de::Error::duplicate_field("query"));
73                            }
74                            query = map.next_value::<Option<String>>()?;
75                        }
76                        "operationName" => {
77                            if operation_name.is_some() {
78                                return Err(de::Error::duplicate_field("operationName"));
79                            }
80                            operation_name = map.next_value::<Option<String>>()?;
81                        }
82                        "variables" => {
83                            if variables.is_some() {
84                                return Err(de::Error::duplicate_field("variables"));
85                            }
86                            variables = map.next_value::<Option<HashMap<String, Value>>>()?;
87                        }
88                        "extensions" => {
89                            if extensions.is_some() {
90                                return Err(de::Error::duplicate_field("extensions"));
91                            }
92                            extensions = map.next_value::<Option<HashMap<String, Value>>>()?;
93                        }
94                        other => {
95                            let value: Value = map.next_value()?;
96                            extra_params.insert(other.to_string(), value);
97                        }
98                    }
99                }
100
101                Ok(GraphQLParams {
102                    query,
103                    operation_name,
104                    variables: variables.unwrap_or_default(),
105                    extensions,
106                })
107            }
108        }
109
110        deserializer.deserialize_map(GraphQLParamsVisitor)
111    }
112}
113
114pub struct OnGraphQLParamsStartHookPayload<'exec> {
115    /// The incoming HTTP request to the router for which the GraphQL execution is happening.
116    /// It includes all the details of the request such as headers, body, etc.
117    ///
118    /// Example:
119    /// ```
120    ///  let my_header = payload.router_http_request.headers.get("my-header");
121    ///  // do something with the header...
122    ///  payload.proceed()
123    /// ```
124    pub router_http_request: &'exec RouterHttpRequest<'exec>,
125    /// The context object that can be used to share data across different plugin hooks for the same request.
126    /// It is unique per request and is dropped after the response is sent.
127    ///
128    /// [Learn more about the context data sharing in the docs](https://the-guild.dev/graphql/hive/docs/router/extensibility/plugin_system#context-data-sharing)
129    pub context: &'exec PluginContext,
130    /// The raw body of the incoming HTTP request.
131    /// This is useful for plugins that want to parse the body in a custom way,
132    /// or want to access the raw body for logging or other purposes.
133    pub body: Bytes,
134    /// The overriden GraphQL parameters to be used in the execution instead of the ones parsed from the HTTP request.
135    /// If this is `None`, the router will use the GraphQL parameters parsed from the HTTP request.
136    /// This is useful for plugins that want to parse the GraphQL parameters in a custom way,
137    /// or want to override the GraphQL parameters for testing or other purposes.
138    ///
139    /// [Learn more about overriding the default behavior](https://the-guild.dev/graphql/hive/docs/router/extensibility/plugin_system#overriding-default-behavior)
140    pub graphql_params: Option<GraphQLParams>,
141}
142
143impl<'exec> OnGraphQLParamsStartHookPayload<'exec> {
144    /// Overrides GraphQL parameters to be used in the execution instead of the ones parsed from the HTTP request.
145    /// If this is `None`, the router will use the GraphQL parameters parsed from the HTTP request.
146    /// This is useful for plugins that want to parse the GraphQL parameters in a custom way,
147    /// or want to override the GraphQL parameters for testing or other purposes.
148    ///
149    /// [Learn more about overriding the default behavior](https://the-guild.dev/graphql/hive/docs/router/extensibility/plugin_system#overriding-default-behavior)
150    pub fn with_graphql_params(mut self, graphql_params: GraphQLParams) -> Self {
151        self.graphql_params = Some(graphql_params);
152        self
153    }
154}
155
156impl<'exec> StartHookPayload<OnGraphQLParamsEndHookPayload<'exec>, Response>
157    for OnGraphQLParamsStartHookPayload<'exec>
158{
159}
160
161pub type OnGraphQLParamsStartHookResult<'exec> = StartHookResult<
162    'exec,
163    OnGraphQLParamsStartHookPayload<'exec>,
164    OnGraphQLParamsEndHookPayload<'exec>,
165    Response,
166>;
167
168pub struct OnGraphQLParamsEndHookPayload<'exec> {
169    /// Parsed GraphQL parameters to be used in the execution.
170    /// This is either the result of parsing the HTTP request body by the router,
171    /// or the overridden GraphQL parameters set by the plugin in the `OnGraphQLParamsStartHookPayload`.
172    ///
173    /// [Learn more about overriding the default behavior](https://the-guild.dev/graphql/hive/docs/router/extensibility/plugin_system#overriding-default-behavior)
174    pub graphql_params: GraphQLParams,
175    /// The context object that can be used to share data across different plugin hooks for the same request.
176    /// It is unique per request and is dropped after the response is sent.
177    ///
178    /// [Learn more about the context data sharing in the docs](https://the-guild.dev/graphql/hive/docs/router/extensibility/plugin_system#context-data-sharing)
179    pub context: &'exec PluginContext,
180}
181
182impl<'exec> EndHookPayload<Response> for OnGraphQLParamsEndHookPayload<'exec> {}
183
184pub type OnGraphQLParamsEndHookResult<'exec> =
185    EndHookResult<OnGraphQLParamsEndHookPayload<'exec>, Response>;
186
187#[cfg(test)]
188use ntex::web::test;
189
190#[cfg(test)]
191impl Into<test::TestRequest> for GraphQLParams {
192    fn into(self) -> test::TestRequest {
193        let body = self;
194        test::TestRequest::post().uri("/graphql").set_json(&body)
195    }
196}