From 5e4d118f93789607c1350873cb369b65fbcbfcb8 Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Thu, 24 Apr 2025 16:38:29 -0700 Subject: [PATCH 1/8] ported "federation 1 schema" tests --- .../subgraph/subgraph_validation_tests.rs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/apollo-federation/tests/subgraph/subgraph_validation_tests.rs b/apollo-federation/tests/subgraph/subgraph_validation_tests.rs index 7be43fe271..be711b165d 100644 --- a/apollo-federation/tests/subgraph/subgraph_validation_tests.rs +++ b/apollo-federation/tests/subgraph/subgraph_validation_tests.rs @@ -918,3 +918,84 @@ mod link_handling_tests { // TODO: Test for fed1 } } + +mod federation_1_schema_tests { + use super::*; + + #[test] + fn accepts_federation_directive_definitions_without_arguments() { + let doc = r#" + type Query { + a: Int + } + + directive @key on OBJECT | INTERFACE + directive @requires on FIELD_DEFINITION + "#; + build_and_validate(doc); + } + + #[test] + fn accepts_federation_directive_definitions_with_nullable_arguments() { + let doc = r#" + type Query { + a: Int + } + + type T @key(fields: "id") { + id: ID! @requires(fields: "x") + x: Int @external + } + + # Tests with the _FieldSet argument non-nullable + scalar _FieldSet + directive @key(fields: _FieldSet) on OBJECT | INTERFACE + + # Tests with the argument as String and non-nullable + directive @requires(fields: String) on FIELD_DEFINITION + "#; + build_and_validate(doc); + } + + #[test] + fn accepts_federation_directive_definitions_with_fieldset_type_instead_of_underscore_fieldset() + { + // accepts federation directive definitions with "FieldSet" type instead of "_FieldSet" + let doc = r#" + type Query { + a: Int + } + + type T @key(fields: "id") { + id: ID! + } + + scalar FieldSet + directive @key(fields: FieldSet) on OBJECT | INTERFACE + "#; + build_and_validate(doc); + } + + #[test] + fn rejects_federation_directive_definition_with_unknown_arguments() { + let doc = r#" + type Query { + a: Int + } + + type T @key(fields: "id", unknown: 42) { + id: ID! + } + + scalar _FieldSet + directive @key(fields: _FieldSet!, unknown: Int) on OBJECT | INTERFACE + "#; + assert_errors!( + build_for_errors(doc), + [( + "DIRECTIVE_DEFINITION_INVALID", + r#"[S] Invalid definition for directive "@key": unknown/unsupported argument "unknown""# + )] + ); + } +} From 6587615488a39d240e334d09ab95b120dbedb799 Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Thu, 24 Apr 2025 17:48:28 -0700 Subject: [PATCH 2/8] ported @shareable tests --- .../subgraph/subgraph_validation_tests.rs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/apollo-federation/tests/subgraph/subgraph_validation_tests.rs b/apollo-federation/tests/subgraph/subgraph_validation_tests.rs index be711b165d..db07c8a921 100644 --- a/apollo-federation/tests/subgraph/subgraph_validation_tests.rs +++ b/apollo-federation/tests/subgraph/subgraph_validation_tests.rs @@ -999,3 +999,81 @@ mod federation_1_schema_tests { ); } } + +mod shareable_tests { + use super::*; + + #[test] + #[should_panic(expected = r#"Mismatched errors:"#)] + fn can_only_be_applied_to_fields_of_object_types() { + let doc = r#" + interface I { + a: Int @shareable + } + "#; + assert_errors!( + build_for_errors(doc), + [( + "INVALID_SHAREABLE_USAGE", + r#"[S] Invalid use of @shareable on field "I.a": only object type fields can be marked with @shareable"# + )] + ); + } + + #[test] + #[should_panic(expected = r#"Mismatched errors:"#)] + fn rejects_duplicate_shareable_on_the_same_definition_declaration() { + let doc = r#" + type E @shareable @key(fields: "id") @shareable { + id: ID! + a: Int + } + "#; + assert_errors!( + build_for_errors(doc), + [( + "INVALID_SHAREABLE_USAGE", + r#"[S] Invalid duplicate application of @shareable on the same type declaration of "E": @shareable is only repeatable on types so it can be used simultaneously on a type definition and its extensions, but it should not be duplicated on the same definition/extension declaration"# + )] + ); + } + + #[test] + #[should_panic(expected = r#"Mismatched errors:"#)] + fn rejects_duplicate_shareable_on_the_same_extension_declaration() { + let doc = r#" + type E @shareable { + id: ID! + a: Int + } + + extend type E @shareable @shareable { + b: Int + } + "#; + assert_errors!( + build_for_errors(doc), + [( + "INVALID_SHAREABLE_USAGE", + r#"[S] Invalid duplicate application of @shareable on the same type declaration of "E": @shareable is only repeatable on types so it can be used simultaneously on a type definition and its extensions, but it should not be duplicated on the same definition/extension declaration"# + )] + ); + } + + #[test] + #[should_panic(expected = r#"Mismatched errors:"#)] + fn rejects_duplicate_shareable_on_a_field() { + let doc = r#" + type E { + a: Int @shareable @shareable + } + "#; + assert_errors!( + build_for_errors(doc), + [( + "INVALID_SHAREABLE_USAGE", + r#"[S] Invalid duplicate application of @shareable on field "E.a": @shareable is only repeatable on types so it can be used simultaneously on a type definition and its extensions, but it should not be duplicated on the same definition/extension declaration"# + )] + ); + } +} From 4a910ed3b82698776db0abe6b5883461437b3de3 Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Thu, 24 Apr 2025 18:01:27 -0700 Subject: [PATCH 3/8] ported "@interfaceObject/@key on interfaces validation" tests --- .../subgraph/subgraph_validation_tests.rs | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/apollo-federation/tests/subgraph/subgraph_validation_tests.rs b/apollo-federation/tests/subgraph/subgraph_validation_tests.rs index db07c8a921..15092a570a 100644 --- a/apollo-federation/tests/subgraph/subgraph_validation_tests.rs +++ b/apollo-federation/tests/subgraph/subgraph_validation_tests.rs @@ -1077,3 +1077,138 @@ mod shareable_tests { ); } } + +mod interface_object_and_key_on_interfaces_validation_tests { + use super::*; + + #[test] + #[should_panic(expected = r#"subgraph error was expected:"#)] + fn key_on_interfaces_require_key_on_all_implementations() { + let doc = r#" + interface I @key(fields: "id1") @key(fields: "id2") { + id1: ID! + id2: ID! + } + + type A implements I @key(fields: "id2") { + id1: ID! + id2: ID! + a: Int + } + + type B implements I @key(fields: "id1") @key(fields: "id2") { + id1: ID! + id2: ID! + b: Int + } + + type C implements I @key(fields: "id2") { + id1: ID! + id2: ID! + c: Int + } + "#; + assert_errors!( + build_for_errors(doc), + [( + "INTERFACE_KEY_NOT_ON_IMPLEMENTATION", + r#"[S] Key @key(fields: "id1") on interface type "I" is missing on implementation types "A" and "C"."# + )] + ); + } + + #[test] + #[should_panic(expected = r#"subgraph error was expected:"#)] + fn key_on_interfaces_with_key_on_some_implementation_non_resolvable() { + let doc = r#" + interface I @key(fields: "id1") { + id1: ID! + } + + type A implements I @key(fields: "id1") { + id1: ID! + a: Int + } + + type B implements I @key(fields: "id1") { + id1: ID! + b: Int + } + + type C implements I @key(fields: "id1", resolvable: false) { + id1: ID! + c: Int + } + "#; + + assert_errors!( + build_for_errors(doc), + [( + "INTERFACE_KEY_NOT_ON_IMPLEMENTATION", + r#"[S] Key @key(fields: "id1") on interface type "I" should be resolvable on all implementation types, but is declared with argument "@key(resolvable:)" set to false in type "C"."# + )] + ); + } + + #[test] + fn ensures_order_of_fields_in_key_does_not_matter() { + let doc = r#" + interface I @key(fields: "a b c") { + a: Int + b: Int + c: Int + } + + type A implements I @key(fields: "c b a") { + a: Int + b: Int + c: Int + } + + type B implements I @key(fields: "a c b") { + a: Int + b: Int + c: Int + } + + type C implements I @key(fields: "a b c") { + a: Int + b: Int + c: Int + } + "#; + + // Ensure no errors are returned + build_and_validate(doc); + } + + #[test] + #[should_panic(expected = r#"Mismatched errors:"#)] + fn only_allow_interface_object_on_entity_types() { + // There is no meaningful way to make @interfaceObject work on a value type at the moment, + // because if you have an @interfaceObject, some other subgraph needs to be able to resolve + // the concrete type, and that imply that you have key to go to that other subgraph. To be + // clear, the @key on the @interfaceObject technically don't need to be "resolvable", and + // the difference between no key and a non-resolvable key is arguably more of a convention + // than a genuine mechanical difference at the moment, but still a good idea to rely on + // that convention to help catching obvious mistakes early. + let doc = r#" + # This one shouldn't raise an error + type A @key(fields: "id", resolvable: false) @interfaceObject { + id: ID! + } + + # This one should + type B @interfaceObject { + x: Int + } + "#; + assert_errors!( + build_for_errors(doc), + [( + "INTERFACE_OBJECT_USAGE_ERROR", + r#"[S] The @interfaceObject directive can only be applied to entity types but type "B" has no @key in this subgraph."# + )] + ); + } +} From 75c0687d1fe41747fd9cbef213c8a29f75593337 Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Thu, 24 Apr 2025 18:19:55 -0700 Subject: [PATCH 4/8] ported `@cost` and `@listSize` tests --- .../subgraph/subgraph_validation_tests.rs | 258 ++++++++++++++++++ 1 file changed, 258 insertions(+) diff --git a/apollo-federation/tests/subgraph/subgraph_validation_tests.rs b/apollo-federation/tests/subgraph/subgraph_validation_tests.rs index 15092a570a..b8edf6fe62 100644 --- a/apollo-federation/tests/subgraph/subgraph_validation_tests.rs +++ b/apollo-federation/tests/subgraph/subgraph_validation_tests.rs @@ -1212,3 +1212,261 @@ mod interface_object_and_key_on_interfaces_validation_tests { ); } } + +mod cost_tests { + use super::*; + + #[test] + #[should_panic( + expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# + )] + fn rejects_cost_applications_on_interfaces() { + let doc = r#" + extend schema + @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"]) + + type Query { + a: A + } + + interface A { + x: Int @cost(weight: 10) + } + "#; + + assert_errors!( + build_for_errors(doc), + [( + "COST_APPLIED_TO_INTERFACE_FIELD", + r#"[S] @cost cannot be applied to interface "A.x""# + )] + ); + } +} + +mod list_size_tests { + use super::*; + + #[test] + #[should_panic( + expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# + )] + fn rejects_applications_on_non_lists_unless_it_uses_sized_fields() { + let doc = r#" + extend schema + @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + a1: A @listSize(assumedSize: 5) + a2: A @listSize(assumedSize: 10, sizedFields: ["ints"]) + } + + type A { + ints: [Int] + } + "#; + + assert_errors!( + build_for_errors(doc), + [( + "LIST_SIZE_APPLIED_TO_NON_LIST", + r#"[S] "Query.a1" is not a list"# + )] + ); + } + + #[test] + #[should_panic( + expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# + )] + fn rejects_negative_assumed_size() { + let doc = r#" + extend schema + @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + a: [Int] @listSize(assumedSize: -5) + b: [Int] @listSize(assumedSize: 0) + } + "#; + + assert_errors!( + build_for_errors(doc), + [( + "LIST_SIZE_INVALID_ASSUMED_SIZE", + r#"[S] Assumed size of "Query.a" cannot be negative"# + )] + ); + } + + #[test] + #[should_panic( + expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# + )] + fn rejects_slicing_arguments_not_in_field_arguments() { + let doc = r#" + extend schema + @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + myField(something: Int): [String] + @listSize(slicingArguments: ["missing1", "missing2"]) + myOtherField(somethingElse: String): [Int] + @listSize(slicingArguments: ["alsoMissing"]) + } + "#; + + assert_errors!( + build_for_errors(doc), + [ + ( + "LIST_SIZE_INVALID_SLICING_ARGUMENT", + r#"[S] Slicing argument "missing1" is not an argument of "Query.myField""# + ), + ( + "LIST_SIZE_INVALID_SLICING_ARGUMENT", + r#"[S] Slicing argument "missing2" is not an argument of "Query.myField""# + ), + ( + "LIST_SIZE_INVALID_SLICING_ARGUMENT", + r#"[S] Slicing argument "alsoMissing" is not an argument of "Query.myOtherField""# + ) + ] + ); + } + + #[test] + #[should_panic( + expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# + )] + fn rejects_slicing_arguments_not_int_or_int_non_null() { + let doc = r#" + extend schema + @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + sliced( + first: String + second: Int + third: Int! + fourth: [Int] + fifth: [Int]! + ): [String] + @listSize( + slicingArguments: ["first", "second", "third", "fourth", "fifth"] + ) + } + "#; + + assert_errors!( + build_for_errors(doc), + [ + ( + "LIST_SIZE_INVALID_SLICING_ARGUMENT", + r#"[S] Slicing argument "Query.sliced(first:)" must be Int or Int!"# + ), + ( + "LIST_SIZE_INVALID_SLICING_ARGUMENT", + r#"[S] Slicing argument "Query.sliced(fourth:)" must be Int or Int!"# + ), + ( + "LIST_SIZE_INVALID_SLICING_ARGUMENT", + r#"[S] Slicing argument "Query.sliced(fifth:)" must be Int or Int!"# + ) + ] + ); + } + + #[test] + #[should_panic( + expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# + )] + fn rejects_sized_fields_when_output_type_is_not_object() { + let doc = r#" + extend schema + @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + notObject: Int @listSize(assumedSize: 1, sizedFields: ["anything"]) + a: A @listSize(assumedSize: 5, sizedFields: ["ints"]) + b: B @listSize(assumedSize: 10, sizedFields: ["ints"]) + } + + type A { + ints: [Int] + } + + interface B { + ints: [Int] + } + "#; + + assert_errors!( + build_for_errors(doc), + [( + "LIST_SIZE_INVALID_SIZED_FIELD", + r#"[S] Sized fields cannot be used because "Int" is not a composite type"# + )] + ); + } + + #[test] + #[should_panic( + expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# + )] + fn rejects_sized_fields_not_in_output_type() { + let doc = r#" + extend schema + @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + a: A @listSize(assumedSize: 5, sizedFields: ["notOnA"]) + } + + type A { + ints: [Int] + } + "#; + + assert_errors!( + build_for_errors(doc), + [( + "LIST_SIZE_INVALID_SIZED_FIELD", + r#"[S] Sized field "notOnA" is not a field on type "A""# + )] + ); + } + + #[test] + #[should_panic( + expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# + )] + fn rejects_sized_fields_not_lists() { + let doc = r#" + extend schema + @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + a: A + @listSize( + assumedSize: 5 + sizedFields: ["list", "nonNullList", "notList"] + ) + } + + type A { + list: [String] + nonNullList: [String]! + notList: String + } + "#; + + assert_errors!( + build_for_errors(doc), + [( + "LIST_SIZE_APPLIED_TO_NON_LIST", + r#"[S] Sized field "A.notList" is not a list"# + )] + ); + } +} From cefac0efeb89d2067c79ed379f43734d64fcbb8a Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Thu, 24 Apr 2025 22:30:23 -0700 Subject: [PATCH 5/8] updated build_for_errors to build as fed2 - partially ported asFed2SubgraphDocument - added a CLI command to print the parsed/expanded subgraph --- apollo-federation/cli/src/main.rs | 28 +++ .../src/link/federation_spec_definition.rs | 52 ++++++ apollo-federation/src/subgraph/mod.rs | 4 + apollo-federation/src/subgraph/typestate.rs | 16 +- .../subgraph/subgraph_validation_tests.rs | 169 ++++++++++-------- 5 files changed, 192 insertions(+), 77 deletions(-) diff --git a/apollo-federation/cli/src/main.rs b/apollo-federation/cli/src/main.rs index 805151a6b3..e6985b17b3 100644 --- a/apollo-federation/cli/src/main.rs +++ b/apollo-federation/cli/src/main.rs @@ -18,6 +18,7 @@ use apollo_federation::query_plan::query_planner::QueryPlannerConfig; use apollo_federation::sources::connect::expand::ExpansionResult; use apollo_federation::sources::connect::expand::expand_connectors; use apollo_federation::subgraph; +use apollo_federation::subgraph::typestate; use clap::Parser; use tracing_subscriber::prelude::*; @@ -92,6 +93,10 @@ enum Command { /// Path(s) to subgraph schemas. schemas: Vec, }, + Subgraph { + /// The path to the subgraph schema file, or `-` for stdin + subgraph_schema: PathBuf, + }, /// Extract subgraph schemas from a supergraph schema to stdout (or in a directory if specified) Extract { /// The path to the supergraph schema file, or `-` for stdin @@ -171,6 +176,7 @@ fn main() -> ExitCode { planner, } => cmd_plan(&query, &schemas, planner), Command::Validate { schemas } => cmd_validate(&schemas), + Command::Subgraph { subgraph_schema } => cmd_subgraph(&subgraph_schema, true), Command::Compose { schemas } => cmd_compose(&schemas), Command::Extract { supergraph_schema, @@ -320,6 +326,28 @@ fn cmd_validate(file_paths: &[PathBuf]) -> Result<(), FederationError> { Ok(()) } +fn cmd_subgraph(file_path: &Path, as_fed2: bool) -> Result<(), FederationError> { + let doc_str = read_input(file_path); + let name = file_path + .file_name() + .and_then(|name| name.to_str().map(|x| x.to_string())); + let name = name.unwrap_or("subgraph".to_string()); + let subgraph = typestate::Subgraph::parse(&name, &format!("http://{name}"), &doc_str) + .expect("valid schema"); + let subgraph = if as_fed2 { + subgraph.into_fed2_subgraph()? + } else { + subgraph + }; + let subgraph = subgraph + .expand_links() + .expect("expanded subgraph to be valid") + .validate(true) + .map_err(|e| e.into_inner())?; + println!("{}", subgraph.schema_string()); + Ok(()) +} + fn cmd_compose(file_paths: &[PathBuf]) -> Result<(), FederationError> { let supergraph = compose_files(file_paths)?; println!("{}", supergraph.schema.schema()); diff --git a/apollo-federation/src/link/federation_spec_definition.rs b/apollo-federation/src/link/federation_spec_definition.rs index e39ec4b54b..773f0c05c4 100644 --- a/apollo-federation/src/link/federation_spec_definition.rs +++ b/apollo-federation/src/link/federation_spec_definition.rs @@ -3,6 +3,7 @@ use std::sync::LazyLock; use apollo_compiler::Name; use apollo_compiler::Node; +use apollo_compiler::Schema; use apollo_compiler::ast::Argument; use apollo_compiler::ast::DirectiveLocation; use apollo_compiler::ast::Type; @@ -23,6 +24,8 @@ use crate::link::argument::directive_optional_boolean_argument; use crate::link::argument::directive_optional_string_argument; use crate::link::argument::directive_required_string_argument; use crate::link::link_spec_definition::LINK_DIRECTIVE_FEATURE_ARGUMENT_NAME; +use crate::link::link_spec_definition::LINK_DIRECTIVE_IMPORT_ARGUMENT_NAME; +use crate::link::link_spec_definition::LINK_DIRECTIVE_URL_ARGUMENT_NAME; use crate::link::spec::Identity; use crate::link::spec::Url; use crate::link::spec::Version; @@ -121,6 +124,13 @@ impl FederationSpecDefinition { Self::for_version(latest_version).unwrap() } + /// Some users rely on auto-expanding fed v1 graphs with fed v2 directives. While technically + /// we should only expand @tag directive from v2 definitions, we will continue expanding other + /// directives (up to v2.4) to ensure backwards compatibility. + pub(crate) fn auto_expanded_federation_spec() -> &'static Self { + Self::for_version(&Version { major: 2, minor: 4 }).unwrap() + } + pub(crate) fn is_fed1(&self) -> bool { self.version().satisfies(&Version { major: 1, minor: 0 }) } @@ -966,3 +976,45 @@ pub(crate) fn fed1_link_imports() -> Vec> { .map(Arc::new) .collect() } + +/// Adds a bootstrap federation (v2 or above) link directive to the schema. +/// - Similar to `add_fed1_link_to_schema`, but for federation v2 and above. +/// - Mainly for testing. +pub(crate) fn add_federation_link_to_schema( + schema: &mut Schema, + federation_version: &Version, +) -> Result<(), FederationError> { + let federation_spec = FEDERATION_VERSIONS + .find(federation_version) + .ok_or_else(|| internal_error!( + "Subgraph unexpectedly does not use a supported federation spec version. Requested version: {}", + federation_version, + ))?; + + // Insert `@link(url: "http://specs.apollo.dev/federation/vX.Y", import: ...)`. + // - auto import all directives. + let imports: Vec<_> = federation_spec + .directive_specs() + .iter() + .map(|d| format!("@{}", d.name()).into()) + .collect(); + + schema + .schema_definition + .make_mut() + .directives + .push(Component::new(Directive { + name: Identity::link_identity().name, + arguments: vec![ + Node::new(Argument { + name: LINK_DIRECTIVE_URL_ARGUMENT_NAME, + value: federation_spec.url.to_string().into(), + }), + Node::new(Argument { + name: LINK_DIRECTIVE_IMPORT_ARGUMENT_NAME, + value: Node::new(Value::List(imports)), + }), + ], + })); + Ok(()) +} diff --git a/apollo-federation/src/subgraph/mod.rs b/apollo-federation/src/subgraph/mod.rs index 9165059405..1e061db423 100644 --- a/apollo-federation/src/subgraph/mod.rs +++ b/apollo-federation/src/subgraph/mod.rs @@ -379,6 +379,10 @@ impl SubgraphError { &self.error } + pub fn into_inner(self) -> FederationError { + self.error + } + // Format subgraph errors in the same way as `Rover` does. // And return them as a vector of (error_code, error_message) tuples // - Gather associated errors from the validation error. diff --git a/apollo-federation/src/subgraph/typestate.rs b/apollo-federation/src/subgraph/typestate.rs index a4e3caa2f1..90cbdc9166 100644 --- a/apollo-federation/src/subgraph/typestate.rs +++ b/apollo-federation/src/subgraph/typestate.rs @@ -15,7 +15,9 @@ use crate::link::federation_spec_definition::FEDERATION_EXTENDS_DIRECTIVE_NAME_I use crate::link::federation_spec_definition::FEDERATION_KEY_DIRECTIVE_NAME_IN_SPEC; use crate::link::federation_spec_definition::FEDERATION_PROVIDES_DIRECTIVE_NAME_IN_SPEC; use crate::link::federation_spec_definition::FEDERATION_REQUIRES_DIRECTIVE_NAME_IN_SPEC; +use crate::link::federation_spec_definition::FederationSpecDefinition; use crate::link::federation_spec_definition::add_fed1_link_to_schema; +use crate::link::federation_spec_definition::add_federation_link_to_schema; use crate::link::spec_definition::SpecDefinition; use crate::schema::FederationSchema; use crate::schema::blueprint::FederationBlueprint; @@ -117,7 +119,7 @@ impl Subgraph { } pub fn parse( - name: &'static str, + name: &str, url: &str, schema_str: &str, ) -> Result, FederationError> { @@ -129,6 +131,18 @@ impl Subgraph { Ok(Self::new(name, url, schema)) } + /// Converts the schema to a fed2 schema. + /// - It is assumed to have no `@link` to the federation spec. + /// - Returns an equivalent subgraph with a `@link` to the auto expanded federation spec. + /// - This is mainly for testing and not optimized. + // PORT_NOTE: Corresponds to `asFed2SubgraphDocument` function in JS, but simplified. + pub fn into_fed2_subgraph(self) -> Result { + let mut schema = self.state.schema; + let federation_spec = FederationSpecDefinition::auto_expanded_federation_spec(); + add_federation_link_to_schema(&mut schema, federation_spec.version())?; + Ok(Self::new(&self.name, &self.url, schema)) + } + pub fn assume_expanded(self) -> Result, FederationError> { let schema = FederationSchema::new(self.state.schema)?; let metadata = compute_subgraph_metadata(&schema)?.ok_or_else(|| { diff --git a/apollo-federation/tests/subgraph/subgraph_validation_tests.rs b/apollo-federation/tests/subgraph/subgraph_validation_tests.rs index b8edf6fe62..31bf7d6ab0 100644 --- a/apollo-federation/tests/subgraph/subgraph_validation_tests.rs +++ b/apollo-federation/tests/subgraph/subgraph_validation_tests.rs @@ -2,25 +2,46 @@ use apollo_federation::subgraph::SubgraphError; use apollo_federation::subgraph::typestate::Subgraph; use apollo_federation::subgraph::typestate::Validated; -fn build_inner(schema_str: &str) -> Result, SubgraphError> { +enum BuildOption { + AsIs, + AsFed2, +} + +fn build_inner( + schema_str: &str, + build_option: BuildOption, +) -> Result, SubgraphError> { let name = "S"; - Subgraph::parse(name, &format!("http://{name}"), schema_str) - .expect("valid schema") + let subgraph = + Subgraph::parse(name, &format!("http://{name}"), schema_str).expect("valid schema"); + let subgraph = if matches!(build_option, BuildOption::AsFed2) { + subgraph + .into_fed2_subgraph() + .map_err(|e| SubgraphError::new(name, e))? + } else { + subgraph + }; + subgraph .expand_links() .map_err(|e| SubgraphError::new(name, e))? .validate(true) } fn build_and_validate(schema_str: &str) -> Subgraph { - build_inner(schema_str).expect("expanded subgraph to be valid") + build_inner(schema_str, BuildOption::AsIs).expect("expanded subgraph to be valid") } -fn build_for_errors(schema: &str) -> Vec<(String, String)> { - build_inner(schema) +fn build_for_errors_with_option(schema: &str, build_option: BuildOption) -> Vec<(String, String)> { + build_inner(schema, build_option) .expect_err("subgraph error was expected") .format_errors() } +/// Build subgraph expecting errors, assuming fed 2. +fn build_for_errors(schema: &str) -> Vec<(String, String)> { + build_for_errors_with_option(schema, BuildOption::AsFed2) +} + fn remove_indentation(s: &str) -> String { // count the last lines that are space-only let first_empty_lines = s.lines().take_while(|line| line.trim().is_empty()).count(); @@ -59,9 +80,17 @@ fn remove_indentation(s: &str) -> String { fn check_errors(a: &[(String, String)], b: &[(&str, &str)]) -> Result<(), String> { if a.len() != b.len() { return Err(format!( - "Mismatched error counts: {} != {}", + "Mismatched error counts: {} != {}\n\nexpected:\n{}\n\nactual:\n{}", + b.len(), a.len(), - b.len() + b.iter() + .map(|(code, msg)| { format!("- {}: {}", code, msg) }) + .collect::>() + .join("\n"), + a.iter() + .map(|(code, msg)| { format!("+ {}: {}", code, msg) }) + .collect::>() + .join("\n"), )); } @@ -105,7 +134,7 @@ mod fieldset_based_directives { use super::*; #[test] - #[should_panic(expected = r#"subgraph error was expected:"#)] + #[should_panic(expected = r#"Mismatched errors:"#)] fn rejects_field_defined_with_arguments_in_key() { let schema_str = r#" type Query { @@ -127,7 +156,7 @@ mod fieldset_based_directives { } #[test] - #[should_panic(expected = r#"subgraph error was expected:"#)] + #[should_panic(expected = r#"Mismatched errors:"#)] fn rejects_field_defined_with_arguments_in_provides() { let schema_str = r#" type Query { @@ -150,7 +179,7 @@ mod fieldset_based_directives { } #[test] - #[should_panic(expected = r#"subgraph error was expected:"#)] + #[should_panic(expected = r#"Mismatched errors:"#)] fn rejects_provides_on_non_external_fields() { let schema_str = r#" type Query { @@ -173,7 +202,7 @@ mod fieldset_based_directives { } #[test] - #[should_panic(expected = r#"subgraph error was expected:"#)] + #[should_panic(expected = r#"Mismatched errors:"#)] fn rejects_requires_on_non_external_fields() { let schema_str = r#" type Query { @@ -215,7 +244,7 @@ mod fieldset_based_directives { "#, version ); - let err = build_for_errors(&schema_str); + let err = build_for_errors_with_option(&schema_str, BuildOption::AsIs); assert_errors!( err, @@ -319,7 +348,7 @@ mod fieldset_based_directives { f: Int } "#; - let err = build_for_errors(schema_str); + let err = build_for_errors_with_option(schema_str, BuildOption::AsIs); assert_errors!( err, @@ -714,9 +743,8 @@ mod link_handling_tests { #[test] fn errors_on_invalid_known_directive_location() { - let errors = build_for_errors( - // @external is not allowed on 'schema' and likely never will. - r#" + // @external is not allowed on 'schema' and likely never will. + let doc = r#" extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) @@ -727,11 +755,9 @@ mod link_handling_tests { directive @federation__external( reason: String ) on OBJECT | FIELD_DEFINITION | SCHEMA - "#, - ); - + "#; assert_errors!( - errors, + build_for_errors_with_option(doc, BuildOption::AsIs), [( "DIRECTIVE_DEFINITION_INVALID", r#"[S] Invalid definition for directive "@federation__external": "@federation__external" should have locations OBJECT, FIELD_DEFINITION, but found (non-subset) OBJECT, FIELD_DEFINITION, SCHEMA"#, @@ -741,8 +767,7 @@ mod link_handling_tests { #[test] fn errors_on_invalid_non_repeatable_directive_marked_repeatable() { - let errors = build_for_errors( - r#" + let doc = r#" extend schema @link(url: "https://specs.apollo.dev/federation/v2.0" import: ["@key"]) @@ -751,10 +776,9 @@ mod link_handling_tests { } directive @federation__external repeatable on OBJECT | FIELD_DEFINITION - "#, - ); + "#; assert_errors!( - errors, + build_for_errors_with_option(doc, BuildOption::AsIs), [( "DIRECTIVE_DEFINITION_INVALID", r#"[S] Invalid definition for directive "@federation__external": "@federation__external" should not be repeatable"#, @@ -764,8 +788,7 @@ mod link_handling_tests { #[test] fn errors_on_unknown_argument_of_known_directive() { - let errors = build_for_errors( - r#" + let doc = r#" extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) @@ -774,10 +797,9 @@ mod link_handling_tests { } directive @federation__external(foo: Int) on OBJECT | FIELD_DEFINITION - "#, - ); + "#; assert_errors!( - errors, + build_for_errors_with_option(doc, BuildOption::AsIs), [( "DIRECTIVE_DEFINITION_INVALID", r#"[S] Invalid definition for directive "@federation__external": unknown/unsupported argument "foo""#, @@ -787,8 +809,7 @@ mod link_handling_tests { #[test] fn errors_on_invalid_type_for_a_known_argument() { - let errors = build_for_errors( - r#" + let doc = r#" extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) @@ -800,10 +821,9 @@ mod link_handling_tests { fields: String! resolvable: String ) repeatable on OBJECT | INTERFACE - "#, - ); + "#; assert_errors!( - errors, + build_for_errors_with_option(doc, BuildOption::AsIs), [( "DIRECTIVE_DEFINITION_INVALID", r#"[S] Invalid definition for directive "@key": argument "resolvable" should have type "Boolean" but found type "String""#, @@ -813,8 +833,7 @@ mod link_handling_tests { #[test] fn errors_on_a_required_argument_defined_as_optional() { - let errors = build_for_errors( - r#" + let doc = r#" extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) @@ -828,10 +847,9 @@ mod link_handling_tests { ) repeatable on OBJECT | INTERFACE scalar federation__FieldSet - "#, - ); + "#; assert_errors!( - errors, + build_for_errors_with_option(doc, BuildOption::AsIs), [( "DIRECTIVE_DEFINITION_INVALID", r#"[S] Invalid definition for directive "@key": argument "fields" should have type "federation__FieldSet!" but found type "federation__FieldSet""#, @@ -841,8 +859,7 @@ mod link_handling_tests { #[test] fn errors_on_invalid_definition_for_link_purpose() { - let errors = build_for_errors( - r#" + let doc = r#" extend schema @link(url: "https://specs.apollo.dev/federation/v2.0") type T { @@ -853,10 +870,9 @@ mod link_handling_tests { EXECUTION RANDOM } - "#, - ); + "#; assert_errors!( - errors, + build_for_errors_with_option(doc, BuildOption::AsIs), [( "TYPE_DEFINITION_INVALID", r#"[S] Invalid definition for type "Purpose": expected values [EXECUTION, SECURITY] but found [EXECUTION, RANDOM]."#, @@ -915,7 +931,24 @@ mod link_handling_tests { )] ); - // TODO: Test for fed1 + // Test for fed1 + assert_errors!( + build_for_errors_with_option(doc, BuildOption::AsIs), + [( + "INVALID_GRAPHQL", + r###" + [S] Error: non-repeatable directive key can only be used once per location + ╭─[ S:2:39 ] + │ + 2 │ type T @key(fields: "k1") @key(fields: "k2") { + │ ──┬─ ─────────┬──────── + │ ╰──────────────────────────────────── directive `@key` first called here + │ │ + │ ╰────────── directive `@key` called again here + ───╯ + "### + )] + ); } } @@ -991,7 +1024,7 @@ mod federation_1_schema_tests { directive @key(fields: _FieldSet!, unknown: Int) on OBJECT | INTERFACE "#; assert_errors!( - build_for_errors(doc), + build_for_errors_with_option(doc, BuildOption::AsIs), [( "DIRECTIVE_DEFINITION_INVALID", r#"[S] Invalid definition for directive "@key": unknown/unsupported argument "unknown""# @@ -1004,7 +1037,7 @@ mod shareable_tests { use super::*; #[test] - #[should_panic(expected = r#"Mismatched errors:"#)] + #[should_panic(expected = r#"subgraph error was expected: "#)] fn can_only_be_applied_to_fields_of_object_types() { let doc = r#" interface I { @@ -1021,7 +1054,7 @@ mod shareable_tests { } #[test] - #[should_panic(expected = r#"Mismatched errors:"#)] + #[should_panic(expected = r#"subgraph error was expected:"#)] fn rejects_duplicate_shareable_on_the_same_definition_declaration() { let doc = r#" type E @shareable @key(fields: "id") @shareable { @@ -1039,7 +1072,7 @@ mod shareable_tests { } #[test] - #[should_panic(expected = r#"Mismatched errors:"#)] + #[should_panic(expected = r#"subgraph error was expected: "#)] fn rejects_duplicate_shareable_on_the_same_extension_declaration() { let doc = r#" type E @shareable { @@ -1061,7 +1094,7 @@ mod shareable_tests { } #[test] - #[should_panic(expected = r#"Mismatched errors:"#)] + #[should_panic(expected = r#"subgraph error was expected: "#)] fn rejects_duplicate_shareable_on_a_field() { let doc = r#" type E { @@ -1217,9 +1250,7 @@ mod cost_tests { use super::*; #[test] - #[should_panic( - expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# - )] + #[should_panic(expected = r#"Mismatched errors:"#)] fn rejects_cost_applications_on_interfaces() { let doc = r#" extend schema @@ -1248,9 +1279,7 @@ mod list_size_tests { use super::*; #[test] - #[should_panic( - expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# - )] + #[should_panic(expected = r#"Mismatched errors:"#)] fn rejects_applications_on_non_lists_unless_it_uses_sized_fields() { let doc = r#" extend schema @@ -1276,9 +1305,7 @@ mod list_size_tests { } #[test] - #[should_panic( - expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# - )] + #[should_panic(expected = r#"Mismatched errors:"#)] fn rejects_negative_assumed_size() { let doc = r#" extend schema @@ -1300,9 +1327,7 @@ mod list_size_tests { } #[test] - #[should_panic( - expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# - )] + #[should_panic(expected = r#"Mismatched error counts:"#)] fn rejects_slicing_arguments_not_in_field_arguments() { let doc = r#" extend schema @@ -1336,9 +1361,7 @@ mod list_size_tests { } #[test] - #[should_panic( - expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# - )] + #[should_panic(expected = r#"Mismatched error counts:"#)] fn rejects_slicing_arguments_not_int_or_int_non_null() { let doc = r#" extend schema @@ -1378,9 +1401,7 @@ mod list_size_tests { } #[test] - #[should_panic( - expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# - )] + #[should_panic(expected = r#"Mismatched errors:"#)] fn rejects_sized_fields_when_output_type_is_not_object() { let doc = r#" extend schema @@ -1411,9 +1432,7 @@ mod list_size_tests { } #[test] - #[should_panic( - expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# - )] + #[should_panic(expected = r#"Mismatched errors:"#)] fn rejects_sized_fields_not_in_output_type() { let doc = r#" extend schema @@ -1438,9 +1457,7 @@ mod list_size_tests { } #[test] - #[should_panic( - expected = r#"The https://specs.apollo.dev/federation/v1.0 specification should have been added to the schema before this is called"# - )] + #[should_panic(expected = r#"Mismatched errors:"#)] fn rejects_sized_fields_not_lists() { let doc = r#" extend schema From 3bf045750cd635f382f614ad985120eda7c01e01 Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Fri, 25 Apr 2025 08:37:09 -0700 Subject: [PATCH 6/8] removed `as_fed2` parameter for `cmd_subgraph` function --- apollo-federation/cli/src/main.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apollo-federation/cli/src/main.rs b/apollo-federation/cli/src/main.rs index e6985b17b3..c2ab365b48 100644 --- a/apollo-federation/cli/src/main.rs +++ b/apollo-federation/cli/src/main.rs @@ -176,7 +176,7 @@ fn main() -> ExitCode { planner, } => cmd_plan(&query, &schemas, planner), Command::Validate { schemas } => cmd_validate(&schemas), - Command::Subgraph { subgraph_schema } => cmd_subgraph(&subgraph_schema, true), + Command::Subgraph { subgraph_schema } => cmd_subgraph(&subgraph_schema), Command::Compose { schemas } => cmd_compose(&schemas), Command::Extract { supergraph_schema, @@ -326,20 +326,14 @@ fn cmd_validate(file_paths: &[PathBuf]) -> Result<(), FederationError> { Ok(()) } -fn cmd_subgraph(file_path: &Path, as_fed2: bool) -> Result<(), FederationError> { +fn cmd_subgraph(file_path: &Path) -> Result<(), FederationError> { let doc_str = read_input(file_path); let name = file_path .file_name() .and_then(|name| name.to_str().map(|x| x.to_string())); let name = name.unwrap_or("subgraph".to_string()); let subgraph = typestate::Subgraph::parse(&name, &format!("http://{name}"), &doc_str) - .expect("valid schema"); - let subgraph = if as_fed2 { - subgraph.into_fed2_subgraph()? - } else { - subgraph - }; - let subgraph = subgraph + .expect("valid schema") .expand_links() .expect("expanded subgraph to be valid") .validate(true) From fda6d54b3ce4c1418d769518febc899433ac9186 Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Fri, 25 Apr 2025 08:55:42 -0700 Subject: [PATCH 7/8] added a comment on the `subgraph` CLI command --- apollo-federation/cli/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/apollo-federation/cli/src/main.rs b/apollo-federation/cli/src/main.rs index c2ab365b48..5d4ff54cbe 100644 --- a/apollo-federation/cli/src/main.rs +++ b/apollo-federation/cli/src/main.rs @@ -93,6 +93,7 @@ enum Command { /// Path(s) to subgraph schemas. schemas: Vec, }, + /// Expand and validate a subgraph schema and print the result Subgraph { /// The path to the subgraph schema file, or `-` for stdin subgraph_schema: PathBuf, From eca6cc52c7a0abca784a711683fc2827e60a9397 Mon Sep 17 00:00:00 2001 From: Duckki Oe Date: Fri, 25 Apr 2025 08:56:19 -0700 Subject: [PATCH 8/8] moved `add_federation_link_to_schema` to typestate.rs - It's only for subgraph testing for now. So, the `subgraph` module seems the right place to have it. --- .../src/link/federation_spec_definition.rs | 45 ---------------- apollo-federation/src/subgraph/typestate.rs | 51 ++++++++++++++++++- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/apollo-federation/src/link/federation_spec_definition.rs b/apollo-federation/src/link/federation_spec_definition.rs index 773f0c05c4..015dd56e1e 100644 --- a/apollo-federation/src/link/federation_spec_definition.rs +++ b/apollo-federation/src/link/federation_spec_definition.rs @@ -3,7 +3,6 @@ use std::sync::LazyLock; use apollo_compiler::Name; use apollo_compiler::Node; -use apollo_compiler::Schema; use apollo_compiler::ast::Argument; use apollo_compiler::ast::DirectiveLocation; use apollo_compiler::ast::Type; @@ -24,8 +23,6 @@ use crate::link::argument::directive_optional_boolean_argument; use crate::link::argument::directive_optional_string_argument; use crate::link::argument::directive_required_string_argument; use crate::link::link_spec_definition::LINK_DIRECTIVE_FEATURE_ARGUMENT_NAME; -use crate::link::link_spec_definition::LINK_DIRECTIVE_IMPORT_ARGUMENT_NAME; -use crate::link::link_spec_definition::LINK_DIRECTIVE_URL_ARGUMENT_NAME; use crate::link::spec::Identity; use crate::link::spec::Url; use crate::link::spec::Version; @@ -976,45 +973,3 @@ pub(crate) fn fed1_link_imports() -> Vec> { .map(Arc::new) .collect() } - -/// Adds a bootstrap federation (v2 or above) link directive to the schema. -/// - Similar to `add_fed1_link_to_schema`, but for federation v2 and above. -/// - Mainly for testing. -pub(crate) fn add_federation_link_to_schema( - schema: &mut Schema, - federation_version: &Version, -) -> Result<(), FederationError> { - let federation_spec = FEDERATION_VERSIONS - .find(federation_version) - .ok_or_else(|| internal_error!( - "Subgraph unexpectedly does not use a supported federation spec version. Requested version: {}", - federation_version, - ))?; - - // Insert `@link(url: "http://specs.apollo.dev/federation/vX.Y", import: ...)`. - // - auto import all directives. - let imports: Vec<_> = federation_spec - .directive_specs() - .iter() - .map(|d| format!("@{}", d.name()).into()) - .collect(); - - schema - .schema_definition - .make_mut() - .directives - .push(Component::new(Directive { - name: Identity::link_identity().name, - arguments: vec![ - Node::new(Argument { - name: LINK_DIRECTIVE_URL_ARGUMENT_NAME, - value: federation_spec.url.to_string().into(), - }), - Node::new(Argument { - name: LINK_DIRECTIVE_IMPORT_ARGUMENT_NAME, - value: Node::new(Value::List(imports)), - }), - ], - })); - Ok(()) -} diff --git a/apollo-federation/src/subgraph/typestate.rs b/apollo-federation/src/subgraph/typestate.rs index 90cbdc9166..60bc3ddfa9 100644 --- a/apollo-federation/src/subgraph/typestate.rs +++ b/apollo-federation/src/subgraph/typestate.rs @@ -1,9 +1,12 @@ use apollo_compiler::Name; +use apollo_compiler::Node; use apollo_compiler::Schema; +use apollo_compiler::ast; use apollo_compiler::collections::IndexSet; use apollo_compiler::name; use apollo_compiler::schema::Component; use apollo_compiler::schema::ComponentName; +use apollo_compiler::schema::Directive; use apollo_compiler::schema::Type; use crate::LinkSpecDefinition; @@ -15,9 +18,13 @@ use crate::link::federation_spec_definition::FEDERATION_EXTENDS_DIRECTIVE_NAME_I use crate::link::federation_spec_definition::FEDERATION_KEY_DIRECTIVE_NAME_IN_SPEC; use crate::link::federation_spec_definition::FEDERATION_PROVIDES_DIRECTIVE_NAME_IN_SPEC; use crate::link::federation_spec_definition::FEDERATION_REQUIRES_DIRECTIVE_NAME_IN_SPEC; +use crate::link::federation_spec_definition::FEDERATION_VERSIONS; use crate::link::federation_spec_definition::FederationSpecDefinition; use crate::link::federation_spec_definition::add_fed1_link_to_schema; -use crate::link::federation_spec_definition::add_federation_link_to_schema; +use crate::link::link_spec_definition::LINK_DIRECTIVE_IMPORT_ARGUMENT_NAME; +use crate::link::link_spec_definition::LINK_DIRECTIVE_URL_ARGUMENT_NAME; +use crate::link::spec::Identity; +use crate::link::spec::Version; use crate::link::spec_definition::SpecDefinition; use crate::schema::FederationSchema; use crate::schema::blueprint::FederationBlueprint; @@ -215,6 +222,48 @@ impl Subgraph { } } +/// Adds a federation (v2 or above) link directive to the schema. +/// - Similar to `add_fed1_link_to_schema`, but the link is added before bootstrapping. +/// - This is mainly for testing. +fn add_federation_link_to_schema( + schema: &mut Schema, + federation_version: &Version, +) -> Result<(), FederationError> { + let federation_spec = FEDERATION_VERSIONS + .find(federation_version) + .ok_or_else(|| internal_error!( + "Subgraph unexpectedly does not use a supported federation spec version. Requested version: {}", + federation_version, + ))?; + + // Insert `@link(url: "http://specs.apollo.dev/federation/vX.Y", import: ...)`. + // - auto import all directives. + let imports: Vec<_> = federation_spec + .directive_specs() + .iter() + .map(|d| format!("@{}", d.name()).into()) + .collect(); + + schema + .schema_definition + .make_mut() + .directives + .push(Component::new(Directive { + name: Identity::link_identity().name, + arguments: vec![ + Node::new(ast::Argument { + name: LINK_DIRECTIVE_URL_ARGUMENT_NAME, + value: federation_spec.url().to_string().into(), + }), + Node::new(ast::Argument { + name: LINK_DIRECTIVE_IMPORT_ARGUMENT_NAME, + value: Node::new(ast::Value::List(imports)), + }), + ], + })); + Ok(()) +} + fn add_federation_operations(schema: &mut FederationSchema) -> Result<(), FederationError> { // Add federation operation types ANY_TYPE_SPEC.check_or_add(schema, None)?;