router_bridge/
introspect.rs1use crate::js::Js;
6use crate::{error::Error, planner::QueryPlannerConfig};
7use serde::{Deserialize, Serialize};
8use std::fmt::Display;
9use thiserror::Error;
10
11#[derive(Debug, Error, Serialize, Deserialize, PartialEq, Eq, Clone)]
19pub struct IntrospectionError {
20 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#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
34pub struct IntrospectionResponse {
35 #[serde(default)]
37 data: Option<serde_json::Value>,
38 #[serde(default)]
40 errors: Option<Vec<IntrospectionError>>,
41}
42
43impl IntrospectionResponse {
47 pub fn data(&self) -> Option<&serde_json::Value> {
51 self.data.as_ref()
52 }
53
54 pub fn errors(&self) -> Option<&Vec<IntrospectionError>> {
58 self.errors.as_ref()
59 }
60
61 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
75pub type IntrospectionResult = Result<Vec<IntrospectionResponse>, IntrospectionError>;
81
82pub 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 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}