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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use crate::utils::MapAccessSerdeExt;
use ntex::util::Bytes;
use serde::de;
use serde::de::IgnoredAny;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use sonic_rs::Value;
use crate::executor::plugin_context::PluginContext;
use crate::executor::plugin_context::RouterHttpRequest;
use crate::executor::plugin_trait::EndHookPayload;
use crate::executor::plugin_trait::EndHookResult;
use crate::executor::plugin_trait::StartHookPayload;
use crate::executor::plugin_trait::StartHookResult;
use crate::executor::request_context::RequestContextPluginApi;
use ntex::http::Response;
type RequestContextApi = RequestContextPluginApi<super::OnGraphqlParams>;
#[derive(Debug, Default, Serialize)]
/// The GraphQL parameters parsed from the HTTP request body by the router.
/// This includes the `query`, `operationName`, `variables`, and `extensions`
/// [Learn more about GraphQL-over-HTTP params](https://graphql.org/learn/serving-over-http/#request-format)
pub struct GraphQLParams {
#[serde(skip_serializing_if = "Option::is_none")]
/// The GraphQL query string parsed from the HTTP request body by the router
/// This contains the source text of a GraphQL query, mutation, or subscription sent by the client in the request body.
/// It can be `None` if the client did not send a query string in the request body.
pub query: Option<String>,
#[serde(rename = "operationName", skip_serializing_if = "Option::is_none")]
/// The operation name parsed from the HTTP request body by the router
/// This is the name of the operation that the client wants to execute, sent in the request body.
/// It is optional and can be `None` if the client did not specify an operation
pub operation_name: Option<String>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
/// The variables map parsed from the HTTP request body by the router
/// This is a map of variable names to their values sent by the client in the request
/// [Learn more about GraphQL variables](https://graphql.org/learn/queries/#variables)
pub variables: HashMap<String, Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<HashMap<String, Value>>,
}
// Workaround for https://github.com/cloudwego/sonic-rs/issues/114
impl<'de> Deserialize<'de> for GraphQLParams {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct GraphQLParamsVisitor;
impl<'de> de::Visitor<'de> for GraphQLParamsVisitor {
type Value = GraphQLParams;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a map for GraphQLParams")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut query = None;
let mut operation_name = None;
let mut variables: Option<HashMap<String, Value>> = None;
let mut extensions: Option<HashMap<String, Value>> = None;
while let Some(key) = map.next_key::<Cow<'de, str>>()? {
match key.as_ref() {
"query" => map.deserialize_once_into_option(&mut query, "query")?,
"operationName" => {
map.deserialize_once_into_option(&mut operation_name, "operationName")?
}
"variables" => {
map.deserialize_once_into_option(&mut variables, "variables")?
}
"extensions" => {
map.deserialize_once_into_option(&mut extensions, "extensions")?
}
_ => {
let _ = map.next_value::<IgnoredAny>()?;
}
}
}
Ok(GraphQLParams {
query,
operation_name,
variables: variables.unwrap_or_default(),
extensions,
})
}
}
deserializer.deserialize_map(GraphQLParamsVisitor)
}
}
pub struct OnGraphQLParamsStartHookPayload<'exec> {
/// The incoming HTTP request to the router for which the GraphQL execution is happening.
/// It includes all the details of the request such as headers, body, etc.
///
/// Example:
/// ```
/// let my_header = payload.router_http_request.headers.get("my-header");
/// // do something with the header...
/// payload.proceed()
/// ```
pub router_http_request: &'exec RouterHttpRequest<'exec>,
/// The context object that can be used to share data across different plugin hooks for the same request.
/// It is unique per request and is dropped after the response is sent.
///
/// [Learn more about the context data sharing in the docs](https://the-guild.dev/graphql/hive/docs/router/extensibility/plugin_system#context-data-sharing)
pub context: &'exec PluginContext,
pub request_context: RequestContextApi,
/// The raw body of the incoming HTTP request.
/// This is useful for plugins that want to parse the body in a custom way,
/// or want to access the raw body for logging or other purposes.
pub body: Bytes,
/// The overriden GraphQL parameters to be used in the execution instead of the ones parsed from the HTTP request.
/// If this is `None`, the router will use the GraphQL parameters parsed from the HTTP request.
/// This is useful for plugins that want to parse the GraphQL parameters in a custom way,
/// or want to override the GraphQL parameters for testing or other purposes.
///
/// [Learn more about overriding the default behavior](https://the-guild.dev/graphql/hive/docs/router/extensibility/plugin_system#overriding-default-behavior)
pub graphql_params: Option<GraphQLParams>,
}
impl<'exec> OnGraphQLParamsStartHookPayload<'exec> {
/// Overrides GraphQL parameters to be used in the execution instead of the ones parsed from the HTTP request.
/// If this is `None`, the router will use the GraphQL parameters parsed from the HTTP request.
/// This is useful for plugins that want to parse the GraphQL parameters in a custom way,
/// or want to override the GraphQL parameters for testing or other purposes.
///
/// [Learn more about overriding the default behavior](https://the-guild.dev/graphql/hive/docs/router/extensibility/plugin_system#overriding-default-behavior)
pub fn with_graphql_params(mut self, graphql_params: GraphQLParams) -> Self {
self.graphql_params = Some(graphql_params);
self
}
/// Adds an extra partition component to the router's inbound (query/subscription) dedupe
/// fingerprint: requests with different partitions never share an in-flight response.
pub fn add_inbound_dedupe_partition(&self, partition: u64) {
self.context.add_inbound_dedupe_partition(partition);
}
}
impl<'exec> StartHookPayload<OnGraphQLParamsEndHookPayload<'exec>, Response>
for OnGraphQLParamsStartHookPayload<'exec>
{
}
pub type OnGraphQLParamsStartHookResult<'exec> = StartHookResult<
'exec,
OnGraphQLParamsStartHookPayload<'exec>,
OnGraphQLParamsEndHookPayload<'exec>,
Response,
>;
pub struct OnGraphQLParamsEndHookPayload<'exec> {
/// Parsed GraphQL parameters to be used in the execution.
/// This is either the result of parsing the HTTP request body by the router,
/// or the overridden GraphQL parameters set by the plugin in the `OnGraphQLParamsStartHookPayload`.
///
/// [Learn more about overriding the default behavior](https://the-guild.dev/graphql/hive/docs/router/extensibility/plugin_system#overriding-default-behavior)
pub graphql_params: GraphQLParams,
/// The context object that can be used to share data across different plugin hooks for the same request.
/// It is unique per request and is dropped after the response is sent.
///
/// [Learn more about the context data sharing in the docs](https://the-guild.dev/graphql/hive/docs/router/extensibility/plugin_system#context-data-sharing)
pub context: &'exec PluginContext,
pub request_context: RequestContextApi,
}
impl<'exec> EndHookPayload<Response> for OnGraphQLParamsEndHookPayload<'exec> {}
pub type OnGraphQLParamsEndHookResult<'exec> =
EndHookResult<OnGraphQLParamsEndHookPayload<'exec>, Response>;
#[cfg(test)]
use ntex::web::test;
#[cfg(test)]
impl Into<test::TestRequest> for GraphQLParams {
fn into(self) -> test::TestRequest {
let body = self;
test::TestRequest::post().uri("/graphql").set_json(&body)
}
}