apollo_router/graphql/
response.rs

1#![allow(missing_docs)] // FIXME
2use std::time::Instant;
3
4use apollo_compiler::response::ExecutionResponse;
5use bytes::Bytes;
6use displaydoc::Display;
7use serde::Deserialize;
8use serde::Serialize;
9use serde_json_bytes::ByteString;
10use serde_json_bytes::Map;
11
12use crate::error::Error;
13use crate::graphql::IntoGraphQLErrors;
14use crate::json_ext::Object;
15use crate::json_ext::Path;
16use crate::json_ext::Value;
17
18#[derive(thiserror::Error, Display, Debug, Eq, PartialEq)]
19#[error("GraphQL response was malformed: {reason}")]
20pub(crate) struct MalformedResponseError {
21    /// The reason the deserialization failed.
22    pub(crate) reason: String,
23}
24
25/// A graphql primary response.
26/// Used for federated and subgraph queries.
27#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)]
28#[serde(rename_all = "camelCase")]
29#[non_exhaustive]
30pub struct Response {
31    /// The label that was passed to the defer or stream directive for this patch.
32    #[serde(skip_serializing_if = "Option::is_none", default)]
33    pub label: Option<String>,
34
35    /// The response data.
36    #[serde(skip_serializing_if = "Option::is_none", default)]
37    pub data: Option<Value>,
38
39    /// The path that the data should be merged at.
40    #[serde(skip_serializing_if = "Option::is_none", default)]
41    pub path: Option<Path>,
42
43    /// The optional graphql errors encountered.
44    #[serde(skip_serializing_if = "Vec::is_empty", default)]
45    pub errors: Vec<Error>,
46
47    /// The optional graphql extensions.
48    #[serde(skip_serializing_if = "Object::is_empty", default)]
49    pub extensions: Object,
50
51    #[serde(skip_serializing_if = "Option::is_none", default)]
52    pub has_next: Option<bool>,
53
54    #[serde(skip, default)]
55    pub subscribed: Option<bool>,
56
57    /// Used for subscription event to compute the duration of a subscription event
58    #[serde(skip, default)]
59    pub created_at: Option<Instant>,
60
61    #[serde(skip_serializing_if = "Vec::is_empty", default)]
62    pub incremental: Vec<IncrementalResponse>,
63}
64
65#[buildstructor::buildstructor]
66impl Response {
67    /// Constructor
68    #[builder(visibility = "pub")]
69    fn new(
70        label: Option<String>,
71        data: Option<Value>,
72        path: Option<Path>,
73        errors: Vec<Error>,
74        extensions: Map<ByteString, Value>,
75        _subselection: Option<String>,
76        has_next: Option<bool>,
77        subscribed: Option<bool>,
78        incremental: Vec<IncrementalResponse>,
79        created_at: Option<Instant>,
80    ) -> Self {
81        Self {
82            label,
83            data,
84            path,
85            errors,
86            extensions,
87            has_next,
88            subscribed,
89            incremental,
90            created_at,
91        }
92    }
93
94    /// If path is None, this is a primary response.
95    pub fn is_primary(&self) -> bool {
96        self.path.is_none()
97    }
98
99    /// append_errors default the errors `path` with the one provided.
100    pub fn append_errors(&mut self, errors: &mut Vec<Error>) {
101        self.errors.append(errors)
102    }
103
104    /// Create a [`Response`] from the supplied [`Bytes`].
105    ///
106    /// This will return an error (identifying the faulty service) if the input is invalid.
107    pub(crate) fn from_bytes(b: Bytes) -> Result<Response, MalformedResponseError> {
108        let value = Value::from_bytes(b).map_err(|error| MalformedResponseError {
109            reason: error.to_string(),
110        })?;
111        Response::from_value(value)
112    }
113
114    pub(crate) fn from_value(value: Value) -> Result<Response, MalformedResponseError> {
115        let mut object = ensure_object!(value).map_err(|error| MalformedResponseError {
116            reason: error.to_string(),
117        })?;
118        let data = object.remove("data");
119        let errors = extract_key_value_from_object!(object, "errors", Value::Array(v) => v)
120            .map_err(|err| MalformedResponseError {
121                reason: err.to_string(),
122            })?
123            .into_iter()
124            .flatten()
125            .map(Error::from_value)
126            .collect::<Result<Vec<Error>, MalformedResponseError>>()?;
127        let extensions =
128            extract_key_value_from_object!(object, "extensions", Value::Object(o) => o)
129                .map_err(|err| MalformedResponseError {
130                    reason: err.to_string(),
131                })?
132                .unwrap_or_default();
133        let label = extract_key_value_from_object!(object, "label", Value::String(s) => s)
134            .map_err(|err| MalformedResponseError {
135                reason: err.to_string(),
136            })?
137            .map(|s| s.as_str().to_string());
138        let path = extract_key_value_from_object!(object, "path")
139            .map(serde_json_bytes::from_value)
140            .transpose()
141            .map_err(|err| MalformedResponseError {
142                reason: err.to_string(),
143            })?;
144        let has_next = extract_key_value_from_object!(object, "hasNext", Value::Bool(b) => b)
145            .map_err(|err| MalformedResponseError {
146                reason: err.to_string(),
147            })?;
148        let incremental =
149            extract_key_value_from_object!(object, "incremental", Value::Array(a) => a).map_err(
150                |err| MalformedResponseError {
151                    reason: err.to_string(),
152                },
153            )?;
154        let incremental: Vec<IncrementalResponse> = match incremental {
155            Some(v) => v
156                .into_iter()
157                .map(serde_json_bytes::from_value)
158                .collect::<Result<Vec<IncrementalResponse>, _>>()
159                .map_err(|err| MalformedResponseError {
160                    reason: err.to_string(),
161                })?,
162            None => vec![],
163        };
164        // Graphql spec says:
165        // If the data entry in the response is not present, the errors entry in the response must not be empty.
166        // It must contain at least one error. The errors it contains should indicate why no data was able to be returned.
167        if data.is_none() && errors.is_empty() {
168            return Err(MalformedResponseError {
169                reason: "graphql response without data must contain at least one error".to_string(),
170            });
171        }
172
173        Ok(Response {
174            label,
175            data,
176            path,
177            errors,
178            extensions,
179            has_next,
180            subscribed: None,
181            incremental,
182            created_at: None,
183        })
184    }
185}
186
187/// A graphql incremental response.
188/// Used with `@defer`
189#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)]
190#[serde(rename_all = "camelCase")]
191#[non_exhaustive]
192pub struct IncrementalResponse {
193    /// The label that was passed to the defer or stream directive for this patch.
194    #[serde(skip_serializing_if = "Option::is_none", default)]
195    pub label: Option<String>,
196
197    /// The response data.
198    #[serde(skip_serializing_if = "Option::is_none", default)]
199    pub data: Option<Value>,
200
201    /// The path that the data should be merged at.
202    #[serde(skip_serializing_if = "Option::is_none", default)]
203    pub path: Option<Path>,
204
205    /// The optional graphql errors encountered.
206    #[serde(skip_serializing_if = "Vec::is_empty", default)]
207    pub errors: Vec<Error>,
208
209    /// The optional graphql extensions.
210    #[serde(skip_serializing_if = "Object::is_empty", default)]
211    pub extensions: Object,
212}
213
214#[buildstructor::buildstructor]
215impl IncrementalResponse {
216    /// Constructor
217    #[builder(visibility = "pub")]
218    fn new(
219        label: Option<String>,
220        data: Option<Value>,
221        path: Option<Path>,
222        errors: Vec<Error>,
223        extensions: Map<ByteString, Value>,
224    ) -> Self {
225        Self {
226            label,
227            data,
228            path,
229            errors,
230            extensions,
231        }
232    }
233
234    /// append_errors default the errors `path` with the one provided.
235    pub fn append_errors(&mut self, errors: &mut Vec<Error>) {
236        self.errors.append(errors)
237    }
238}
239
240impl From<ExecutionResponse> for Response {
241    fn from(response: ExecutionResponse) -> Response {
242        let ExecutionResponse { errors, data } = response;
243        Self {
244            errors: errors.into_graphql_errors().unwrap(),
245            data: data.map(serde_json_bytes::Value::Object),
246            extensions: Default::default(),
247            label: None,
248            path: None,
249            has_next: None,
250            subscribed: None,
251            created_at: None,
252            incremental: Vec::new(),
253        }
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use serde_json::json;
260    use serde_json_bytes::json as bjson;
261    use uuid::Uuid;
262
263    use super::*;
264    use crate::assert_response_eq_ignoring_error_id;
265    use crate::graphql;
266    use crate::graphql::Error;
267    use crate::graphql::Location;
268    use crate::graphql::Response;
269
270    #[test]
271    fn test_append_errors_path_fallback_and_override() {
272        let uuid1 = Uuid::new_v4();
273        let uuid2 = Uuid::new_v4();
274        let expected_errors = vec![
275            Error::builder()
276                .message("Something terrible happened!")
277                .path(Path::from("here"))
278                .apollo_id(uuid1)
279                .build(),
280            Error::builder()
281                .message("I mean for real")
282                .apollo_id(uuid2)
283                .build(),
284        ];
285
286        let mut errors_to_append = vec![
287            Error::builder()
288                .message("Something terrible happened!")
289                .path(Path::from("here"))
290                .apollo_id(uuid1)
291                .build(),
292            Error::builder()
293                .message("I mean for real")
294                .apollo_id(uuid2)
295                .build(),
296        ];
297
298        let mut response = Response::builder().build();
299        response.append_errors(&mut errors_to_append);
300        assert_eq!(response.errors, expected_errors);
301    }
302
303    #[test]
304    fn test_response() {
305        let result = serde_json::from_str::<Response>(
306            json!(
307            {
308              "errors": [
309                {
310                  "message": "Name for character with ID 1002 could not be fetched.",
311                  "locations": [{ "line": 6, "column": 7 }],
312                  "path": ["hero", "heroFriends", 1, "name"],
313                  "extensions": {
314                    "error-extension": 5,
315                  }
316                }
317              ],
318              "data": {
319                "hero": {
320                  "name": "R2-D2",
321                  "heroFriends": [
322                    {
323                      "id": "1000",
324                      "name": "Luke Skywalker"
325                    },
326                    {
327                      "id": "1002",
328                      "name": null
329                    },
330                    {
331                      "id": "1003",
332                      "name": "Leia Organa"
333                    }
334                  ]
335                }
336              },
337              "extensions": {
338                "response-extension": 3,
339              }
340            })
341            .to_string()
342            .as_str(),
343        );
344        let response = result.unwrap();
345        assert_response_eq_ignoring_error_id!(
346            response,
347            Response::builder()
348                .data(json!({
349                  "hero": {
350                    "name": "R2-D2",
351                    "heroFriends": [
352                      {
353                        "id": "1000",
354                        "name": "Luke Skywalker"
355                      },
356                      {
357                        "id": "1002",
358                        "name": null
359                      },
360                      {
361                        "id": "1003",
362                        "name": "Leia Organa"
363                      }
364                    ]
365                  }
366                }))
367                .errors(vec![
368                    Error::builder()
369                        .message("Name for character with ID 1002 could not be fetched.")
370                        .locations(vec!(Location { line: 6, column: 7 }))
371                        .path(Path::from("hero/heroFriends/1/name"))
372                        .extensions(
373                            bjson!({ "error-extension": 5, })
374                                .as_object()
375                                .cloned()
376                                .unwrap()
377                        )
378                        .build()
379                ])
380                .extensions(
381                    bjson!({
382                        "response-extension": 3,
383                    })
384                    .as_object()
385                    .cloned()
386                    .unwrap()
387                )
388                .build()
389        );
390    }
391
392    #[test]
393    fn test_patch_response() {
394        let result = serde_json::from_str::<Response>(
395            json!(
396            {
397              "label": "part",
398              "hasNext": true,
399              "path": ["hero", "heroFriends", 1, "name"],
400              "errors": [
401                {
402                  "message": "Name for character with ID 1002 could not be fetched.",
403                  "locations": [{ "line": 6, "column": 7 }],
404                  "path": ["hero", "heroFriends", 1, "name"],
405                  "extensions": {
406                    "error-extension": 5,
407                  }
408                }
409              ],
410              "data": {
411                "hero": {
412                  "name": "R2-D2",
413                  "heroFriends": [
414                    {
415                      "id": "1000",
416                      "name": "Luke Skywalker"
417                    },
418                    {
419                      "id": "1002",
420                      "name": null
421                    },
422                    {
423                      "id": "1003",
424                      "name": "Leia Organa"
425                    }
426                  ]
427                }
428              },
429              "extensions": {
430                "response-extension": 3,
431              }
432            })
433            .to_string()
434            .as_str(),
435        );
436        let response = result.unwrap();
437        assert_response_eq_ignoring_error_id!(
438            response,
439            Response::builder()
440                .label("part".to_owned())
441                .data(json!({
442                  "hero": {
443                    "name": "R2-D2",
444                    "heroFriends": [
445                      {
446                        "id": "1000",
447                        "name": "Luke Skywalker"
448                      },
449                      {
450                        "id": "1002",
451                        "name": null
452                      },
453                      {
454                        "id": "1003",
455                        "name": "Leia Organa"
456                      }
457                    ]
458                  }
459                }))
460                .path(Path::from("hero/heroFriends/1/name"))
461                .errors(vec![
462                    Error::builder()
463                        .message("Name for character with ID 1002 could not be fetched.")
464                        .locations(vec!(Location { line: 6, column: 7 }))
465                        .path(Path::from("hero/heroFriends/1/name"))
466                        .extensions(
467                            bjson!({ "error-extension": 5, })
468                                .as_object()
469                                .cloned()
470                                .unwrap()
471                        )
472                        .build()
473                ])
474                .extensions(
475                    bjson!({
476                        "response-extension": 3,
477                    })
478                    .as_object()
479                    .cloned()
480                    .unwrap()
481                )
482                .has_next(true)
483                .build()
484        );
485    }
486
487    #[test]
488    fn test_no_data_and_no_errors() {
489        let response = Response::from_bytes("{\"errors\":null}".into());
490        assert_eq!(
491            response.expect_err("no data and no errors"),
492            MalformedResponseError {
493                reason: "graphql response without data must contain at least one error".to_string(),
494            }
495        );
496    }
497
498    #[test]
499    fn test_data_null() {
500        let response = Response::from_bytes("{\"data\":null}".into()).unwrap();
501        assert_eq!(
502            response,
503            Response::builder().data(Some(Value::Null)).build(),
504        );
505    }
506}