1use std::{collections::HashMap, sync::Arc};
7
8use super::{
9 super::{CompiledSchema, MutationDefinition, QueryDefinition, SubscriptionDefinition},
10 directive_builder::{build_custom_directives, builtin_directives},
11 field_resolver::{build_arg_input_value, type_ref},
12 type_resolver::{
13 build_enum_type, build_input_object_type, build_interface_type, build_object_type,
14 build_union_type, builtin_scalars,
15 },
16 types::{
17 IntrospectionField, IntrospectionInputValue, IntrospectionSchema, IntrospectionType,
18 IntrospectionTypeRef, TypeKind,
19 },
20};
21
22#[must_use = "call .build() to construct the final value"]
28pub struct IntrospectionBuilder;
29
30impl IntrospectionBuilder {
31 #[must_use]
33 pub fn build(schema: &CompiledSchema) -> IntrospectionSchema {
34 let mut types = Vec::new();
35
36 types.extend(builtin_scalars());
38
39 for type_def in &schema.types {
41 types.push(build_object_type(type_def));
42 }
43
44 for enum_def in &schema.enums {
46 types.push(build_enum_type(enum_def));
47 }
48
49 for input_def in &schema.input_types {
51 types.push(build_input_object_type(input_def));
52 }
53
54 for interface_def in &schema.interfaces {
56 types.push(build_interface_type(interface_def, schema));
57 }
58
59 for union_def in &schema.unions {
61 types.push(build_union_type(union_def));
62 }
63
64 types.push(build_query_type(schema));
66
67 if !schema.mutations.is_empty() {
69 types.push(build_mutation_type(schema));
70 }
71
72 if !schema.subscriptions.is_empty() {
74 types.push(build_subscription_type(schema));
75 }
76
77 let mut directives = builtin_directives();
79 directives.extend(build_custom_directives(&schema.directives));
80
81 IntrospectionSchema {
82 description: Some("FraiseQL GraphQL Schema".to_string()),
83 types,
84 query_type: IntrospectionTypeRef {
85 name: "Query".to_string(),
86 },
87 mutation_type: if schema.mutations.is_empty() {
88 None
89 } else {
90 Some(IntrospectionTypeRef {
91 name: "Mutation".to_string(),
92 })
93 },
94 subscription_type: if schema.subscriptions.is_empty() {
95 None
96 } else {
97 Some(IntrospectionTypeRef {
98 name: "Subscription".to_string(),
99 })
100 },
101 directives,
102 }
103 }
104
105 #[must_use]
107 pub fn build_type_map(schema: &IntrospectionSchema) -> HashMap<String, IntrospectionType> {
108 let mut map = HashMap::new();
109 for t in &schema.types {
110 if let Some(ref name) = t.name {
111 map.insert(name.clone(), t.clone());
112 }
113 }
114 map
115 }
116
117 #[must_use]
119 pub fn type_ref(name: &str) -> IntrospectionType {
120 type_ref(name)
121 }
122}
123
124fn build_query_type(schema: &CompiledSchema) -> IntrospectionType {
130 let mut fields: Vec<IntrospectionField> =
131 schema.queries.iter().map(|q| build_query_field(q, schema)).collect();
132
133 let has_relay_types =
135 schema.types.iter().any(|t| t.relay) || schema.interfaces.iter().any(|i| i.name == "Node");
136 if has_relay_types && !fields.iter().any(|f| f.name == "node") {
137 fields.push(build_node_query_field());
138 }
139
140 IntrospectionType {
141 kind: TypeKind::Object,
142 name: Some("Query".to_string()),
143 description: Some("Root query type".to_string()),
144 fields: Some(fields),
145 interfaces: Some(vec![]),
146 possible_types: None,
147 enum_values: None,
148 input_fields: None,
149 of_type: None,
150 specified_by_u_r_l: None,
151 }
152}
153
154fn build_mutation_type(schema: &CompiledSchema) -> IntrospectionType {
156 let fields: Vec<IntrospectionField> =
157 schema.mutations.iter().map(|m| build_mutation_field(m, schema)).collect();
158
159 IntrospectionType {
160 kind: TypeKind::Object,
161 name: Some("Mutation".to_string()),
162 description: Some("Root mutation type".to_string()),
163 fields: Some(fields),
164 interfaces: Some(vec![]),
165 possible_types: None,
166 enum_values: None,
167 input_fields: None,
168 of_type: None,
169 specified_by_u_r_l: None,
170 }
171}
172
173fn build_subscription_type(schema: &CompiledSchema) -> IntrospectionType {
175 let fields: Vec<IntrospectionField> = schema
176 .subscriptions
177 .iter()
178 .map(|s| build_subscription_field(s, schema))
179 .collect();
180
181 IntrospectionType {
182 kind: TypeKind::Object,
183 name: Some("Subscription".to_string()),
184 description: Some("Root subscription type".to_string()),
185 fields: Some(fields),
186 interfaces: Some(vec![]),
187 possible_types: None,
188 enum_values: None,
189 input_fields: None,
190 of_type: None,
191 specified_by_u_r_l: None,
192 }
193}
194
195fn build_query_field(query: &QueryDefinition, schema: &CompiledSchema) -> IntrospectionField {
201 if query.relay {
204 return build_relay_query_field(query, schema);
205 }
206
207 let return_type = type_ref(&query.return_type);
208 let return_type = if query.returns_list {
209 IntrospectionType {
210 kind: TypeKind::List,
211 name: None,
212 description: None,
213 fields: None,
214 interfaces: None,
215 possible_types: None,
216 enum_values: None,
217 input_fields: None,
218 of_type: Some(Box::new(return_type)),
219 specified_by_u_r_l: None,
220 }
221 } else {
222 return_type
223 };
224
225 let return_type = if query.nullable {
226 return_type
227 } else {
228 IntrospectionType {
229 kind: TypeKind::NonNull,
230 name: None,
231 description: None,
232 fields: None,
233 interfaces: None,
234 possible_types: None,
235 enum_values: None,
236 input_fields: None,
237 of_type: Some(Box::new(return_type)),
238 specified_by_u_r_l: None,
239 }
240 };
241
242 let args: Vec<IntrospectionInputValue> =
246 query.graphql_arguments().iter().map(build_arg_input_value).collect();
247
248 IntrospectionField {
249 name: schema.display_name(&query.name),
250 description: query.description.clone(),
251 args,
252 field_type: return_type,
253 is_deprecated: query.is_deprecated(),
254 deprecation_reason: query.deprecation_reason().map(ToString::to_string),
255 }
256}
257
258fn build_relay_query_field(query: &QueryDefinition, schema: &CompiledSchema) -> IntrospectionField {
265 let connection_type = format!("{}Connection", query.return_type);
266
267 let return_type = IntrospectionType {
269 kind: TypeKind::NonNull,
270 name: None,
271 description: None,
272 fields: None,
273 interfaces: None,
274 possible_types: None,
275 enum_values: None,
276 input_fields: None,
277 of_type: Some(Box::new(type_ref(&connection_type))),
278 specified_by_u_r_l: None,
279 };
280
281 let nullable_int = || IntrospectionType {
283 kind: TypeKind::Scalar,
284 name: Some("Int".to_string()),
285 description: None,
286 fields: None,
287 interfaces: None,
288 possible_types: None,
289 enum_values: None,
290 input_fields: None,
291 of_type: None,
292 specified_by_u_r_l: None,
293 };
294 let nullable_string = || IntrospectionType {
295 kind: TypeKind::Scalar,
296 name: Some("String".to_string()),
297 description: None,
298 fields: None,
299 interfaces: None,
300 possible_types: None,
301 enum_values: None,
302 input_fields: None,
303 of_type: None,
304 specified_by_u_r_l: None,
305 };
306 let relay_args = vec![
307 IntrospectionInputValue {
308 name: "first".to_string(),
309 description: Some("Return the first N items.".to_string()),
310 input_type: nullable_int(),
311 default_value: None,
312 is_deprecated: false,
313 deprecation_reason: None,
314 validation_rules: vec![],
315 },
316 IntrospectionInputValue {
317 name: "after".to_string(),
318 description: Some("Cursor: return items after this position.".to_string()),
319 input_type: nullable_string(),
320 default_value: None,
321 is_deprecated: false,
322 deprecation_reason: None,
323 validation_rules: vec![],
324 },
325 IntrospectionInputValue {
326 name: "last".to_string(),
327 description: Some("Return the last N items.".to_string()),
328 input_type: nullable_int(),
329 default_value: None,
330 is_deprecated: false,
331 deprecation_reason: None,
332 validation_rules: vec![],
333 },
334 IntrospectionInputValue {
335 name: "before".to_string(),
336 description: Some("Cursor: return items before this position.".to_string()),
337 input_type: nullable_string(),
338 default_value: None,
339 is_deprecated: false,
340 deprecation_reason: None,
341 validation_rules: vec![],
342 },
343 ];
344
345 IntrospectionField {
346 name: schema.display_name(&query.name),
347 description: query.description.clone(),
348 args: relay_args,
349 field_type: return_type,
350 is_deprecated: query.is_deprecated(),
351 deprecation_reason: query.deprecation_reason().map(ToString::to_string),
352 }
353}
354
355fn build_node_query_field() -> IntrospectionField {
359 let return_type = IntrospectionType {
363 kind: TypeKind::Interface,
364 name: Some("Node".to_string()),
365 description: None,
366 fields: None,
367 interfaces: None,
368 possible_types: None,
369 enum_values: None,
370 input_fields: None,
371 of_type: None,
372 specified_by_u_r_l: None,
373 };
374
375 let id_arg = IntrospectionInputValue {
377 name: "id".to_string(),
378 description: Some("Globally unique opaque identifier.".to_string()),
379 input_type: IntrospectionType {
380 kind: TypeKind::NonNull,
381 name: None,
382 description: None,
383 fields: None,
384 interfaces: None,
385 possible_types: None,
386 enum_values: None,
387 input_fields: None,
388 of_type: Some(Box::new(type_ref("ID"))),
389 specified_by_u_r_l: None,
390 },
391 default_value: None,
392 is_deprecated: false,
393 deprecation_reason: None,
394 validation_rules: vec![],
395 };
396
397 IntrospectionField {
398 name: "node".to_string(),
399 description: Some(
400 "Fetch any object that implements the Node interface by its global ID.".to_string(),
401 ),
402 args: vec![id_arg],
403 field_type: return_type,
404 is_deprecated: false,
405 deprecation_reason: None,
406 }
407}
408
409fn build_mutation_field(
411 mutation: &MutationDefinition,
412 schema: &CompiledSchema,
413) -> IntrospectionField {
414 let return_type = type_ref(&mutation.return_type);
416
417 let args: Vec<IntrospectionInputValue> =
419 mutation.arguments.iter().map(build_arg_input_value).collect();
420
421 IntrospectionField {
422 name: schema.display_name(&mutation.name),
423 description: mutation.description.clone(),
424 args,
425 field_type: return_type,
426 is_deprecated: mutation.is_deprecated(),
427 deprecation_reason: mutation.deprecation_reason().map(ToString::to_string),
428 }
429}
430
431fn build_subscription_field(
433 subscription: &SubscriptionDefinition,
434 schema: &CompiledSchema,
435) -> IntrospectionField {
436 let return_type = type_ref(&subscription.return_type);
438
439 let args: Vec<IntrospectionInputValue> =
441 subscription.arguments.iter().map(build_arg_input_value).collect();
442
443 IntrospectionField {
444 name: schema.display_name(&subscription.name),
445 description: subscription.description.clone(),
446 args,
447 field_type: return_type,
448 is_deprecated: subscription.is_deprecated(),
449 deprecation_reason: subscription.deprecation_reason().map(ToString::to_string),
450 }
451}
452
453#[derive(Debug, Clone)]
462pub struct IntrospectionResponses {
463 pub schema_response: Arc<serde_json::Value>,
465 pub type_responses: HashMap<String, Arc<serde_json::Value>>,
467}
468
469impl IntrospectionResponses {
470 #[must_use]
474 pub fn build(schema: &CompiledSchema) -> Self {
475 let introspection = IntrospectionBuilder::build(schema);
476 let type_map = IntrospectionBuilder::build_type_map(&introspection);
477
478 let schema_response = Arc::new(serde_json::json!({
480 "data": {
481 "__schema": introspection
482 }
483 }));
484
485 let mut type_responses = HashMap::new();
487 for (name, t) in type_map {
488 let response = Arc::new(serde_json::json!({
489 "data": {
490 "__type": t
491 }
492 }));
493 type_responses.insert(name, response);
494 }
495
496 Self {
497 schema_response,
498 type_responses,
499 }
500 }
501
502 pub fn filter_inaccessible(&mut self, inaccessible: &HashMap<String, Vec<String>>) {
511 if inaccessible.is_empty() {
512 return;
513 }
514
515 for (type_name, field_names) in inaccessible {
517 if let Some(response) = self.type_responses.get_mut(type_name) {
518 let mut val = response.as_ref().clone();
519 if let Some(fields) =
520 val.pointer_mut("/data/__type/fields").and_then(|v| v.as_array_mut())
521 {
522 fields.retain(|f| {
523 f.get("name")
524 .and_then(|n| n.as_str())
525 .is_none_or(|name| !field_names.contains(&name.to_string()))
526 });
527 }
528 *response = Arc::new(val);
529 }
530 }
531
532 let mut schema_val = self.schema_response.as_ref().clone();
534 if let Some(types) =
535 schema_val.pointer_mut("/data/__schema/types").and_then(|v| v.as_array_mut())
536 {
537 for type_val in types.iter_mut() {
538 let type_name = type_val.get("name").and_then(|n| n.as_str()).unwrap_or("");
539 if let Some(field_names) = inaccessible.get(type_name) {
540 if let Some(fields) = type_val.get_mut("fields").and_then(|v| v.as_array_mut())
541 {
542 fields.retain(|f| {
543 f.get("name")
544 .and_then(|n| n.as_str())
545 .is_none_or(|name| !field_names.contains(&name.to_string()))
546 });
547 }
548 }
549 }
550 }
551 self.schema_response = Arc::new(schema_val);
552 }
553
554 #[must_use]
556 pub fn get_type_response(&self, type_name: &str) -> serde_json::Value {
557 self.type_responses.get(type_name).map_or_else(
558 || {
559 serde_json::json!({
560 "data": {
561 "__type": null
562 }
563 })
564 },
565 |v| v.as_ref().clone(),
566 )
567 }
568}