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