1use apollo_compiler::Name;
2use apollo_compiler::Node;
3use apollo_compiler::Schema;
4use apollo_compiler::ast::Directive;
5use apollo_compiler::ast::Value;
6use apollo_compiler::name;
7use itertools::Itertools;
8
9use super::errors::ERRORS_ARGUMENT_NAME;
10use super::errors::ErrorsArguments;
11use super::http::HTTP_ARGUMENT_NAME;
12use super::http::PATH_ARGUMENT_NAME;
13use super::http::QUERY_PARAMS_ARGUMENT_NAME;
14use crate::connectors::ConnectSpec;
15use crate::connectors::ConnectorPosition;
16use crate::connectors::ObjectFieldDefinitionPosition;
17use crate::connectors::OriginatingDirective;
18use crate::connectors::SourceName;
19use crate::connectors::id::ObjectTypeDefinitionDirectivePosition;
20use crate::connectors::json_selection::JSONSelection;
21use crate::connectors::models::Header;
22use crate::connectors::spec::connect_spec_from_schema;
23use crate::error::FederationError;
24use crate::schema::position::InterfaceFieldDefinitionPosition;
25use crate::schema::position::ObjectOrInterfaceFieldDefinitionPosition;
26use crate::schema::position::ObjectOrInterfaceFieldDirectivePosition;
27
28pub(crate) const CONNECT_DIRECTIVE_NAME_IN_SPEC: Name = name!("connect");
29pub(crate) const CONNECT_SOURCE_ARGUMENT_NAME: Name = name!("source");
30pub(crate) const CONNECT_SELECTION_ARGUMENT_NAME: Name = name!("selection");
31pub(crate) const CONNECT_ENTITY_ARGUMENT_NAME: Name = name!("entity");
32pub(crate) const CONNECT_ID_ARGUMENT_NAME: Name = name!("id");
33pub(crate) const CONNECT_HTTP_NAME_IN_SPEC: Name = name!("ConnectHTTP");
34pub(crate) const CONNECT_BATCH_NAME_IN_SPEC: Name = name!("ConnectBatch");
35pub(crate) const CONNECT_BODY_ARGUMENT_NAME: Name = name!("body");
36pub(crate) const BATCH_ARGUMENT_NAME: Name = name!("batch");
37pub(crate) const IS_SUCCESS_ARGUMENT_NAME: Name = name!("isSuccess");
38pub(super) const DEFAULT_CONNECT_SPEC: ConnectSpec = ConnectSpec::V0_3;
39
40pub(crate) fn extract_connect_directive_arguments(
41 schema: &Schema,
42 name: &Name,
43) -> Result<Vec<ConnectDirectiveArguments>, FederationError> {
44 schema
46 .types
47 .iter()
48 .filter_map(|(name, ty)| match ty {
49 apollo_compiler::schema::ExtendedType::Object(node) => {
50 Some((name, &node.fields, false))
51 }
52 apollo_compiler::schema::ExtendedType::Interface(node) => {
53 Some((name, &node.fields, true))
54 }
55 _ => None,
56 })
57 .flat_map(|(type_name, fields, is_interface)| {
58 fields.iter().flat_map(move |(field_name, field_def)| {
59 field_def
60 .directives
61 .iter()
62 .filter(|directive| directive.name == *name)
63 .enumerate()
64 .map(move |(i, directive)| {
65 let field_pos = if is_interface {
66 ObjectOrInterfaceFieldDefinitionPosition::Interface(
67 InterfaceFieldDefinitionPosition {
68 type_name: type_name.clone(),
69 field_name: field_name.clone(),
70 },
71 )
72 } else {
73 ObjectOrInterfaceFieldDefinitionPosition::Object(
74 ObjectFieldDefinitionPosition {
75 type_name: type_name.clone(),
76 field_name: field_name.clone(),
77 },
78 )
79 };
80
81 let position =
82 ConnectorPosition::Field(ObjectOrInterfaceFieldDirectivePosition {
83 field: field_pos,
84 directive_name: directive.name.clone(),
85 directive_index: i,
86 });
87
88 let connect_spec =
89 connect_spec_from_schema(schema).unwrap_or(DEFAULT_CONNECT_SPEC);
90
91 ConnectDirectiveArguments::from_position_and_directive(
92 position,
93 directive,
94 connect_spec,
95 )
96 })
97 })
98 })
99 .chain(
100 schema
102 .types
103 .iter()
104 .filter_map(|(_, ty)| ty.as_object())
105 .flat_map(|ty| {
106 ty.directives
107 .iter()
108 .filter(|directive| directive.name == *name)
109 .enumerate()
110 .map(move |(i, directive)| {
111 let position =
112 ConnectorPosition::Type(ObjectTypeDefinitionDirectivePosition {
113 type_name: ty.name.clone(),
114 directive_name: directive.name.clone(),
115 directive_index: i,
116 });
117
118 let connect_spec =
119 connect_spec_from_schema(schema).unwrap_or(DEFAULT_CONNECT_SPEC);
120
121 ConnectDirectiveArguments::from_position_and_directive(
122 position,
123 directive,
124 connect_spec,
125 )
126 })
127 }),
128 )
129 .collect()
130}
131
132#[cfg_attr(test, derive(Debug))]
136pub(crate) struct ConnectDirectiveArguments {
137 pub(crate) position: ConnectorPosition,
138
139 pub(crate) source: Option<SourceName>,
143
144 pub(crate) http: Option<ConnectHTTPArguments>,
149
150 pub(crate) selection: JSONSelection,
155
156 pub(crate) connector_id: Option<Name>,
158
159 pub(crate) entity: bool,
165
166 pub(crate) batch: Option<ConnectBatchArguments>,
168
169 pub(crate) errors: Option<ErrorsArguments>,
171
172 pub(crate) is_success: Option<JSONSelection>,
177}
178
179impl ConnectDirectiveArguments {
180 fn from_position_and_directive(
181 position: ConnectorPosition,
182 value: &Node<Directive>,
183 connect_spec: ConnectSpec,
184 ) -> Result<Self, FederationError> {
185 let args = &value.arguments;
186 let directive_name = &value.name;
187
188 let source = SourceName::from_connect(value);
190 let mut http = None;
191 let mut selection = None;
192 let mut entity = None;
193 let mut connector_id = None;
194 let mut batch = None;
195 let mut errors = None;
196 let mut is_success = None;
197 for arg in args {
198 let arg_name = arg.name.as_str();
199
200 if arg_name == HTTP_ARGUMENT_NAME.as_str() {
201 let http_value = arg.value.as_object().ok_or_else(|| {
202 FederationError::internal(format!(
203 "`http` field in `@{directive_name}` directive is not an object"
204 ))
205 })?;
206
207 http = Some(ConnectHTTPArguments::try_from((
208 http_value,
209 directive_name,
210 connect_spec,
211 ))?);
212 } else if arg_name == BATCH_ARGUMENT_NAME.as_str() {
213 let http_value = arg.value.as_object().ok_or_else(|| {
214 FederationError::internal(format!(
215 "`http` field in `@{directive_name}` directive is not an object"
216 ))
217 })?;
218
219 batch = Some(ConnectBatchArguments::try_from((
220 http_value,
221 directive_name,
222 ))?);
223 } else if arg_name == ERRORS_ARGUMENT_NAME.as_str() {
224 let http_value = arg.value.as_object().ok_or_else(|| {
225 FederationError::internal(format!(
226 "`errors` field in `@{directive_name}` directive is not an object"
227 ))
228 })?;
229
230 let errors_value =
231 ErrorsArguments::try_from((http_value, directive_name, connect_spec))?;
232
233 errors = Some(errors_value);
234 } else if arg_name == CONNECT_SELECTION_ARGUMENT_NAME.as_str() {
235 let selection_value = arg.value.as_str().ok_or_else(|| {
236 FederationError::internal(format!(
237 "`selection` field in `@{directive_name}` directive is not a string"
238 ))
239 })?;
240 selection = Some(
241 JSONSelection::parse_with_spec(selection_value, connect_spec)
242 .map_err(|e| FederationError::internal(e.message))?,
243 );
244 } else if arg_name == CONNECT_ID_ARGUMENT_NAME.as_str() {
245 let id = arg.value.as_str().ok_or_else(|| {
246 FederationError::internal(format!(
247 "`id` field in `@{directive_name}` directive is not a string"
248 ))
249 })?;
250
251 connector_id = Some(Name::new(id)?);
252 } else if arg_name == CONNECT_ENTITY_ARGUMENT_NAME.as_str() {
253 let entity_value = arg.value.to_bool().ok_or_else(|| {
254 FederationError::internal(format!(
255 "`entity` field in `@{directive_name}` directive is not a boolean"
256 ))
257 })?;
258
259 entity = Some(entity_value);
260 } else if arg_name == IS_SUCCESS_ARGUMENT_NAME.as_str() {
261 let selection_value = arg.value.as_str().ok_or_else(|| {
262 FederationError::internal(format!(
263 "`is_success` field in `@{directive_name}` directive is not a string"
264 ))
265 })?;
266 is_success = Some(
267 JSONSelection::parse_with_spec(selection_value, connect_spec)
268 .map_err(|e| FederationError::internal(e.message))?,
269 );
270 }
271 }
272
273 Ok(Self {
274 position,
275 source,
276 http,
277 connector_id,
278 selection: selection.ok_or_else(|| {
279 FederationError::internal(format!(
280 "`@{directive_name}` directive is missing a selection"
281 ))
282 })?,
283 entity: entity.unwrap_or_default(),
284 batch,
285 errors,
286 is_success,
287 })
288 }
289}
290
291#[cfg_attr(test, derive(Debug))]
293pub struct ConnectHTTPArguments {
294 pub(crate) get: Option<String>,
295 pub(crate) post: Option<String>,
296 pub(crate) patch: Option<String>,
297 pub(crate) put: Option<String>,
298 pub(crate) delete: Option<String>,
299
300 pub(crate) body: Option<JSONSelection>,
306
307 pub(crate) headers: Vec<Header>,
311
312 pub(crate) path: Option<JSONSelection>,
314 pub(crate) query_params: Option<JSONSelection>,
316}
317
318impl TryFrom<(&ObjectNode, &Name, ConnectSpec)> for ConnectHTTPArguments {
319 type Error = FederationError;
320
321 fn try_from(
322 (values, directive_name, connect_spec): (&ObjectNode, &Name, ConnectSpec),
323 ) -> Result<Self, FederationError> {
324 let mut get = None;
325 let mut post = None;
326 let mut patch = None;
327 let mut put = None;
328 let mut delete = None;
329 let mut body = None;
330 let headers: Vec<Header> =
331 Header::from_http_arg(values, OriginatingDirective::Connect, connect_spec)
332 .into_iter()
333 .try_collect()
334 .map_err(|err| FederationError::internal(err.to_string()))?;
335 let mut path = None;
336 let mut query_params = None;
337 for (name, value) in values {
338 let name = name.as_str();
339
340 if name == CONNECT_BODY_ARGUMENT_NAME.as_str() {
341 let body_value = value.as_str().ok_or_else(|| {
342 FederationError::internal(format!("`body` field in `@{directive_name}` directive's `http` field is not a string"))
343 })?;
344 body = Some(
345 JSONSelection::parse_with_spec(body_value, connect_spec)
346 .map_err(|e| FederationError::internal(e.message))?,
347 );
348 } else if name == "GET" {
349 get = Some(value.as_str().ok_or_else(|| FederationError::internal(format!(
350 "supplied HTTP template URL in `@{directive_name}` directive's `http` field is not a string"
351 )))?.to_string());
352 } else if name == "POST" {
353 post = Some(value.as_str().ok_or_else(|| FederationError::internal(format!(
354 "supplied HTTP template URL in `@{directive_name}` directive's `http` field is not a string"
355 )))?.to_string());
356 } else if name == "PATCH" {
357 patch = Some(value.as_str().ok_or_else(|| FederationError::internal(format!(
358 "supplied HTTP template URL in `@{directive_name}` directive's `http` field is not a string"
359 )))?.to_string());
360 } else if name == "PUT" {
361 put = Some(value.as_str().ok_or_else(|| FederationError::internal(format!(
362 "supplied HTTP template URL in `@{directive_name}` directive's `http` field is not a string"
363 )))?.to_string());
364 } else if name == "DELETE" {
365 delete = Some(value.as_str().ok_or_else(|| FederationError::internal(format!(
366 "supplied HTTP template URL in `@{directive_name}` directive's `http` field is not a string"
367 )))?.to_string());
368 } else if name == PATH_ARGUMENT_NAME.as_str() {
369 let value = value.as_str().ok_or_else(|| {
370 FederationError::internal(format!(
371 "`{PATH_ARGUMENT_NAME}` field in `@{directive_name}` directive's `http` field is not a string"
372 ))
373 })?;
374 path = Some(
375 JSONSelection::parse_with_spec(value, connect_spec)
376 .map_err(|e| FederationError::internal(e.message))?,
377 );
378 } else if name == QUERY_PARAMS_ARGUMENT_NAME.as_str() {
379 let value = value.as_str().ok_or_else(|| {
380 FederationError::internal(format!(
381 "`{QUERY_PARAMS_ARGUMENT_NAME}` field in `@{directive_name}` directive's `http` field is not a string"
382 ))
383 })?;
384 query_params = Some(
385 JSONSelection::parse_with_spec(value, connect_spec)
386 .map_err(|e| FederationError::internal(e.message))?,
387 );
388 }
389 }
390
391 Ok(Self {
392 get,
393 post,
394 patch,
395 put,
396 delete,
397 body,
398 headers,
399 path,
400 query_params,
401 })
402 }
403}
404
405#[derive(Clone, Copy, Debug)]
407pub struct ConnectBatchArguments {
408 pub max_size: Option<usize>,
412}
413
414type ObjectNode = [(Name, Node<Value>)];
416
417impl TryFrom<(&ObjectNode, &Name)> for ConnectBatchArguments {
418 type Error = FederationError;
419
420 fn try_from((values, directive_name): (&ObjectNode, &Name)) -> Result<Self, FederationError> {
421 let mut max_size = None;
422 for (name, value) in values {
423 let name = name.as_str();
424
425 if name == "maxSize" {
426 let max_size_int = Some(value.to_i32().ok_or_else(|| FederationError::internal(format!(
427 "supplied 'max_size' field in `@{directive_name}` directive's `batch` field is not a positive integer"
428 )))?);
429 max_size = max_size_int.map(|i| usize::try_from(i).map_err(|_| FederationError::internal(format!(
432 "supplied 'max_size' field in `@{directive_name}` directive's `batch` field is not a positive integer"
433 )))).transpose()?;
434 }
435 }
436
437 Ok(Self { max_size })
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use apollo_compiler::Schema;
444 use apollo_compiler::name;
445
446 use super::*;
447 use crate::ValidFederationSubgraphs;
448 use crate::schema::FederationSchema;
449 use crate::supergraph::extract_subgraphs_from_supergraph;
450
451 static SIMPLE_SUPERGRAPH: &str = include_str!("../tests/schemas/simple.graphql");
452 static IS_SUCCESS_SUPERGRAPH: &str = include_str!("../tests/schemas/is-success.graphql");
453
454 fn get_subgraphs(supergraph_sdl: &str) -> ValidFederationSubgraphs {
455 let schema = Schema::parse(supergraph_sdl, "supergraph.graphql").unwrap();
456 let supergraph_schema = FederationSchema::new(schema).unwrap();
457 extract_subgraphs_from_supergraph(&supergraph_schema, Some(true)).unwrap()
458 }
459
460 #[test]
461 fn test_expected_connect_spec_latest() {
462 assert_eq!(DEFAULT_CONNECT_SPEC, ConnectSpec::latest());
467 }
468
469 #[test]
470 fn it_parses_at_connect() {
471 let subgraphs = get_subgraphs(SIMPLE_SUPERGRAPH);
472 let subgraph = subgraphs.get("connectors").unwrap();
473 let schema = &subgraph.schema;
474
475 let actual_definition = schema
476 .get_directive_definition(&CONNECT_DIRECTIVE_NAME_IN_SPEC)
477 .unwrap()
478 .get(schema.schema())
479 .unwrap();
480
481 insta::assert_snapshot!(
482 actual_definition.to_string(),
483 @"directive @connect(source: String, id: String, http: connect__ConnectHTTP, batch: connect__ConnectBatch, errors: connect__ConnectorErrors, selection: connect__JSONSelection!, entity: Boolean = false, isSuccess: connect__JSONSelection) repeatable on FIELD_DEFINITION | OBJECT"
484 );
485
486 let fields = schema
487 .referencers()
488 .get_directive(CONNECT_DIRECTIVE_NAME_IN_SPEC.as_str())
489 .object_fields
490 .iter()
491 .map(|f| f.get(schema.schema()).unwrap().to_string())
492 .collect::<Vec<_>>()
493 .join("\n");
494
495 insta::assert_snapshot!(
496 fields,
497 @r###"
498 users: [User] @connect(source: "json", http: {GET: "/users"}, selection: "id name")
499 posts: [Post] @connect(source: "json", http: {GET: "/posts"}, selection: "id title body")
500 "###
501 );
502 }
503
504 #[test]
505 fn it_extracts_at_connect() {
506 let subgraphs = get_subgraphs(SIMPLE_SUPERGRAPH);
507 let subgraph = subgraphs.get("connectors").unwrap();
508 let schema = &subgraph.schema;
509
510 let connects = extract_connect_directive_arguments(schema.schema(), &name!(connect));
512
513 insta::assert_debug_snapshot!(
514 connects.unwrap(),
515 @r#"
516 [
517 ConnectDirectiveArguments {
518 position: Field(
519 ObjectOrInterfaceFieldDirectivePosition {
520 field: Object(Query.users),
521 directive_name: "connect",
522 directive_index: 0,
523 },
524 ),
525 source: Some(
526 "json",
527 ),
528 http: Some(
529 ConnectHTTPArguments {
530 get: Some(
531 "/users",
532 ),
533 post: None,
534 patch: None,
535 put: None,
536 delete: None,
537 body: None,
538 headers: [],
539 path: None,
540 query_params: None,
541 },
542 ),
543 selection: JSONSelection {
544 inner: Named(
545 SubSelection {
546 selections: [
547 NamedSelection {
548 prefix: None,
549 path: WithRange {
550 node: Path(
551 PathSelection {
552 path: WithRange {
553 node: Key(
554 WithRange {
555 node: Field(
556 "id",
557 ),
558 range: Some(
559 0..2,
560 ),
561 },
562 WithRange {
563 node: Empty,
564 range: Some(
565 2..2,
566 ),
567 },
568 ),
569 range: Some(
570 0..2,
571 ),
572 },
573 },
574 ),
575 range: Some(
576 0..2,
577 ),
578 },
579 },
580 NamedSelection {
581 prefix: None,
582 path: WithRange {
583 node: Path(
584 PathSelection {
585 path: WithRange {
586 node: Key(
587 WithRange {
588 node: Field(
589 "name",
590 ),
591 range: Some(
592 3..7,
593 ),
594 },
595 WithRange {
596 node: Empty,
597 range: Some(
598 7..7,
599 ),
600 },
601 ),
602 range: Some(
603 3..7,
604 ),
605 },
606 },
607 ),
608 range: Some(
609 3..7,
610 ),
611 },
612 },
613 ],
614 range: Some(
615 0..7,
616 ),
617 },
618 ),
619 spec: V0_1,
620 },
621 connector_id: None,
622 entity: false,
623 batch: None,
624 errors: None,
625 is_success: None,
626 },
627 ConnectDirectiveArguments {
628 position: Field(
629 ObjectOrInterfaceFieldDirectivePosition {
630 field: Object(Query.posts),
631 directive_name: "connect",
632 directive_index: 0,
633 },
634 ),
635 source: Some(
636 "json",
637 ),
638 http: Some(
639 ConnectHTTPArguments {
640 get: Some(
641 "/posts",
642 ),
643 post: None,
644 patch: None,
645 put: None,
646 delete: None,
647 body: None,
648 headers: [],
649 path: None,
650 query_params: None,
651 },
652 ),
653 selection: JSONSelection {
654 inner: Named(
655 SubSelection {
656 selections: [
657 NamedSelection {
658 prefix: None,
659 path: WithRange {
660 node: Path(
661 PathSelection {
662 path: WithRange {
663 node: Key(
664 WithRange {
665 node: Field(
666 "id",
667 ),
668 range: Some(
669 0..2,
670 ),
671 },
672 WithRange {
673 node: Empty,
674 range: Some(
675 2..2,
676 ),
677 },
678 ),
679 range: Some(
680 0..2,
681 ),
682 },
683 },
684 ),
685 range: Some(
686 0..2,
687 ),
688 },
689 },
690 NamedSelection {
691 prefix: None,
692 path: WithRange {
693 node: Path(
694 PathSelection {
695 path: WithRange {
696 node: Key(
697 WithRange {
698 node: Field(
699 "title",
700 ),
701 range: Some(
702 3..8,
703 ),
704 },
705 WithRange {
706 node: Empty,
707 range: Some(
708 8..8,
709 ),
710 },
711 ),
712 range: Some(
713 3..8,
714 ),
715 },
716 },
717 ),
718 range: Some(
719 3..8,
720 ),
721 },
722 },
723 NamedSelection {
724 prefix: None,
725 path: WithRange {
726 node: Path(
727 PathSelection {
728 path: WithRange {
729 node: Key(
730 WithRange {
731 node: Field(
732 "body",
733 ),
734 range: Some(
735 9..13,
736 ),
737 },
738 WithRange {
739 node: Empty,
740 range: Some(
741 13..13,
742 ),
743 },
744 ),
745 range: Some(
746 9..13,
747 ),
748 },
749 },
750 ),
751 range: Some(
752 9..13,
753 ),
754 },
755 },
756 ],
757 range: Some(
758 0..13,
759 ),
760 },
761 ),
762 spec: V0_1,
763 },
764 connector_id: None,
765 entity: false,
766 batch: None,
767 errors: None,
768 is_success: None,
769 },
770 ]
771 "#
772 );
773 }
774
775 #[test]
776 fn it_supports_is_success_in_connect() {
777 let subgraphs = get_subgraphs(IS_SUCCESS_SUPERGRAPH);
778 let subgraph = subgraphs.get("connectors").unwrap();
779 let schema = &subgraph.schema;
780
781 let connects =
783 extract_connect_directive_arguments(schema.schema(), &name!(connect)).unwrap();
784 for connect in connects {
785 connect.is_success.unwrap();
787 }
788 }
789}