router_bridge/
introspect.rs

1/*!
2# Run introspection against a GraphQL schema and obtain the result
3*/
4
5use crate::js::Js;
6use crate::{error::Error, planner::QueryPlannerConfig};
7use serde::{Deserialize, Serialize};
8use std::fmt::Display;
9use thiserror::Error;
10
11/// An error which occurred during JavaScript introspection.
12///
13/// The shape of this error is meant to mimick that of the error created within
14/// JavaScript, which is a [`GraphQLError`] from the [`graphql-js`] library.
15///
16/// [`graphql-js']: https://npm.im/graphql
17/// [`GraphQLError`]: https://github.com/graphql/graphql-js/blob/3869211/src/error/GraphQLError.js#L18-L75
18#[derive(Debug, Error, Serialize, Deserialize, PartialEq, Eq, Clone)]
19pub struct IntrospectionError {
20    /// A human-readable description of the error that prevented introspection.
21    pub message: Option<String>,
22}
23
24impl Display for IntrospectionError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        f.write_str(self.message.as_deref().unwrap_or("UNKNOWN"))
27    }
28}
29
30/// If `batch_introspect` succeeds, it returns a `Vec<IntrospectionResponse>`.
31///
32/// `IntrospectionResponse` contains data, and a vec of eventual errors.
33#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
34pub struct IntrospectionResponse {
35    /// The introspection response if batch_introspect succeeded
36    #[serde(default)]
37    data: Option<serde_json::Value>,
38    /// The errors raised on this specific query if any
39    #[serde(default)]
40    errors: Option<Vec<IntrospectionError>>,
41}
42
43/// In some (rare) cases, executing a GraphQL query can return both errors and data.
44///
45/// This impl allows you to turn it into either data or errors, or get a reference to both.
46impl IntrospectionResponse {
47    /// `data` returns a reference to the underlying data
48    ///
49    /// use `into_result` if you don't want to use both data and errors.
50    pub fn data(&self) -> Option<&serde_json::Value> {
51        self.data.as_ref()
52    }
53
54    /// `errors` returns a reference to the underlying errors
55    ///
56    /// use `into_result` if you don't want to use both data and errors.
57    pub fn errors(&self) -> Option<&Vec<IntrospectionError>> {
58        self.errors.as_ref()
59    }
60
61    /// `into_result` will turn an IntrospectionResponse into either Ok(data) or Err(errors)
62    pub fn into_result(self) -> Result<serde_json::Value, Vec<IntrospectionError>> {
63        match (self.data, self.errors) {
64            (Some(_), Some(errors)) if !errors.is_empty() => Err(errors),
65            (Some(data), Some(errors)) if errors.is_empty() => Ok(data),
66            (Some(data), None) => Ok(data),
67            (None, Some(errors)) => Err(errors),
68            _ => Err(vec![IntrospectionError {
69                message: Some("neither data nor errors could be found".to_string()),
70            }]),
71        }
72    }
73}
74
75/// The type returned when invoking `batch_introspect`
76///
77/// A global introspect error would be raised here, often meaning the sdl is invalid.
78/// A successful call to `batch_introspect` doesn't mean each query succeeded,
79/// refer to `IntrospectionResponse` to make sure each query ran successfully.
80pub type IntrospectionResult = Result<Vec<IntrospectionResponse>, IntrospectionError>;
81
82/// The `batch_introspect` function receives a [`string`] representing the SDL and invokes JavaScript
83/// introspection on it, with the `queries` to run against the SDL.
84///
85pub fn batch_introspect(
86    sdl: &str,
87    queries: Vec<String>,
88    config: QueryPlannerConfig,
89) -> Result<IntrospectionResult, Error> {
90    Js::new("introspect".to_string())
91        .with_parameter("sdl", sdl)?
92        .with_parameter("queries", queries)?
93        .with_parameter("config", config)?
94        .execute::<IntrospectionResult>(
95            "do_introspect",
96            include_str!("../bundled/do_introspect.js"),
97        )
98}
99
100#[cfg(test)]
101mod tests {
102    use crate::{
103        introspect::batch_introspect,
104        planner::{IncrementalDeliverySupport, QueryPlannerConfig},
105    };
106    #[test]
107    fn it_works() {
108        let raw_sdl = r#"schema
109        {
110          query: Query
111        }
112
113        type Query {
114          hello: String
115        }
116        "#;
117
118        let introspected = batch_introspect(
119            raw_sdl,
120            vec![DEFAULT_INTROSPECTION_QUERY.to_string()],
121            QueryPlannerConfig::default(),
122        )
123        .unwrap();
124        insta::assert_snapshot!(serde_json::to_string(&introspected).unwrap());
125    }
126
127    #[test]
128    fn invalid_sdl() {
129        use crate::introspect::IntrospectionError;
130        let expected_error = IntrospectionError {
131            message: Some(r#"Unknown type "Query"."#.to_string()),
132        };
133        let response = batch_introspect(
134            "schema {
135                query: Query
136            }",
137            vec![DEFAULT_INTROSPECTION_QUERY.to_string()],
138            QueryPlannerConfig::default(),
139        )
140        .expect("an uncaught deno error occured")
141        .expect("a javascript land error happened");
142
143        assert_eq!(vec![expected_error], response[0].clone().errors.unwrap());
144    }
145
146    #[test]
147    fn missing_introspection_query() {
148        use crate::introspect::IntrospectionError;
149        let expected_error = IntrospectionError {
150            message: Some(r#"Unknown type "Query"."#.to_string()),
151        };
152        let response = batch_introspect(
153            "schema {
154                query: Query
155            }",
156            vec![DEFAULT_INTROSPECTION_QUERY.to_string()],
157            QueryPlannerConfig::default(),
158        )
159        .expect("an uncaught deno error occured")
160        .expect("a javascript land error happened");
161        assert_eq!(expected_error, response[0].clone().errors.unwrap()[0]);
162    }
163    // This string is the result of calling getIntrospectionQuery() from the 'graphql' js package.
164    static DEFAULT_INTROSPECTION_QUERY: &str = r#"
165query IntrospectionQuery {
166    __schema {
167        queryType {
168            name
169        }
170        mutationType {
171            name
172        }
173        subscriptionType {
174            name
175        }
176        types {
177            ...FullType
178        }
179        directives {
180            name
181            description
182            locations
183            args {
184                ...InputValue
185            }
186        }
187    }
188}
189
190fragment FullType on __Type {
191    kind
192    name
193    description
194
195    fields(includeDeprecated: true) {
196        name
197        description
198        args {
199            ...InputValue
200        }
201        type {
202            ...TypeRef
203        }
204        isDeprecated
205        deprecationReason
206    }
207    inputFields {
208        ...InputValue
209    }
210    interfaces {
211        ...TypeRef
212    }
213    enumValues(includeDeprecated: true) {
214        name
215        description
216        isDeprecated
217        deprecationReason
218    }
219    possibleTypes {
220        ...TypeRef
221    }
222}
223
224fragment InputValue on __InputValue {
225    name
226    description
227    type {
228        ...TypeRef
229    }
230    defaultValue
231}
232
233fragment TypeRef on __Type {
234    kind
235    name
236    ofType {
237        kind
238        name
239        ofType {
240            kind
241            name
242            ofType {
243                kind
244                name
245                    ofType {
246                    kind
247                    name
248                    ofType {
249                        kind
250                        name
251                            ofType {
252                            kind
253                            name
254                            ofType {
255                                kind
256                                name
257                            }
258                        }
259                    }
260                }
261            }
262        }
263    }
264}
265"#;
266
267    #[test]
268    fn defer_in_introspection() {
269        let raw_sdl = r#"schema
270        {
271          query: Query
272        }
273
274        type Query {
275          hello: String
276        }
277        "#;
278
279        let introspected = batch_introspect(
280            raw_sdl,
281            vec![r#"query {
282                __schema {
283                  directives {
284                    name
285                    locations
286                  }
287                }
288              }"#
289            .to_string()],
290            QueryPlannerConfig::default(),
291        )
292        .unwrap();
293        insta::assert_snapshot!(serde_json::to_string(&introspected).unwrap());
294
295        let introspected = batch_introspect(
296            raw_sdl,
297            vec![r#"query {
298                __schema {
299                  directives {
300                    name
301                    locations
302                  }
303                }
304              }"#
305            .to_string()],
306            QueryPlannerConfig {
307                incremental_delivery: Some(IncrementalDeliverySupport {
308                    enable_defer: Some(true),
309                }),
310                graphql_validation: true,
311                reuse_query_fragments: None,
312                generate_query_fragments: None,
313                debug: Default::default(),
314                type_conditioned_fetching: false,
315            },
316        )
317        .unwrap();
318        insta::assert_snapshot!(serde_json::to_string(&introspected).unwrap());
319    }
320}