diff --git a/docs/en/src/apollo_federation.md b/docs/en/src/apollo_federation.md index ac03643a..6f8b7f7d 100644 --- a/docs/en/src/apollo_federation.md +++ b/docs/en/src/apollo_federation.md @@ -20,6 +20,26 @@ - The `override` directive is used to indicate that a field is now to be resolved by the current subgraph instead of the named subgraph. +- The `link` directive is needed in order to indicate that the subgraph is Federation v2 compatible, enabling the `shareable`, `inaccessable`, and `override` directives. + +## Enabling Federation v2 using the link directive + +async-graphql provides a configuration function `enable_apollo_fed2_link` on the schema builder to have it print out an `extend schema` element with an appropriately configured `link` directive whenever the federation schema is requested. + +```rust +Schema::build(Query, EmptyMutation, EmptySubscription) + .enable_apollo_fed2_link() + .finish() +``` + +and the following (or similar) will be attached to the schema: +``` +extend schema @link( + url: "https://specs.apollo.dev/federation/v2.0", + import: ["@key", "@tag", "@shareable", "@inaccessible", "@override", "@external", "@provides", "@requires"] +) +``` + ## Entity lookup function ```rust diff --git a/src/registry/export_sdl.rs b/src/registry/export_sdl.rs index 2adf2596..be38f43b 100644 --- a/src/registry/export_sdl.rs +++ b/src/registry/export_sdl.rs @@ -111,7 +111,14 @@ impl Registry { writeln!(sdl).ok(); } - if !options.federation { + if options.federation { + if self.enable_apollo_link { + writeln!(sdl, "extend schema @link(").ok(); + writeln!(sdl, "\turl: \"https://specs.apollo.dev/federation/v2.0\",").ok(); + writeln!(sdl, "\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\"]").ok(); + writeln!(sdl, ")").ok(); + } + } else { writeln!(sdl, "schema {{").ok(); writeln!(sdl, "\tquery: {}", self.query_type).ok(); if let Some(mutation_type) = self.mutation_type.as_deref() { diff --git a/src/registry/mod.rs b/src/registry/mod.rs index cf67b893..5a6e6f34 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -423,6 +423,7 @@ pub struct Registry { pub subscription_type: Option, pub introspection_mode: IntrospectionMode, pub enable_federation: bool, + pub enable_apollo_link: bool, pub federation_subscription: bool, pub ignore_name_conflicts: HashSet, } diff --git a/src/schema.rs b/src/schema.rs index 39769ae3..adcac107 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -173,6 +173,16 @@ impl SchemaBuilder self } + /// Enables printing the apollo federation 2 `@link` directive during + /// federation schema export; the directive is attached to an "extend + /// schema" element, and will have values set to ensure that + /// the federation schema directives and types are named properly. + #[must_use] + pub fn enable_apollo_fed2_link(mut self) -> Self { + self.registry.enable_apollo_link = true; + self + } + /// Make the Federation SDL include subscriptions. /// /// Note: Not included by default, in order to be compatible with Apollo @@ -379,6 +389,7 @@ where }, introspection_mode: IntrospectionMode::Enabled, enable_federation: false, + enable_apollo_link: false, federation_subscription: false, ignore_name_conflicts, }; diff --git a/src/types/connection/mod.rs b/src/types/connection/mod.rs index ecb56466..04cf6a82 100644 --- a/src/types/connection/mod.rs +++ b/src/types/connection/mod.rs @@ -222,7 +222,7 @@ where /// # Examples /// /// ```rust -/// +/// /// use async_graphql::*; /// use async_graphql::types::connection::*; /// diff --git a/tests/federation.rs b/tests/federation.rs index 1f644290..26775617 100644 --- a/tests/federation.rs +++ b/tests/federation.rs @@ -538,6 +538,86 @@ pub async fn test_entity_inaccessible() { } } +#[tokio::test] +pub async fn test_link_directive() { + struct User { + id: ID, + } + + #[Object(extends)] + impl User { + #[graphql(external)] + async fn id(&self) -> &ID { + &self.id + } + + async fn reviews(&self) -> Vec { + todo!() + } + } + + struct Review; + + #[Object] + impl Review { + async fn body(&self) -> String { + todo!() + } + + async fn author(&self) -> User { + todo!() + } + + async fn product(&self) -> Product { + todo!() + } + } + + struct Product { + upc: String, + } + + #[Object(extends)] + impl Product { + #[graphql(external)] + async fn upc(&self) -> &str { + &self.upc + } + + async fn reviews(&self) -> Vec { + todo!() + } + } + + struct Query; + + #[Object] + impl Query { + #[graphql(entity)] + async fn find_user_by_id(&self, id: ID) -> User { + User { id } + } + + #[graphql(entity)] + async fn find_product_by_upc(&self, upc: String) -> Product { + Product { upc } + } + } + + let schema_sdl = Schema::build(Query, EmptyMutation, EmptySubscription) + .enable_apollo_fed2_link() + .finish() + .sdl_with_options(SDLExportOptions::new().federation()); + + let path = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("tests/schemas/test_fed2_link.schema.graphqls"); + let expected_schema = std::fs::read_to_string(&path).unwrap(); + if schema_sdl != expected_schema { + std::fs::write(path, schema_sdl).unwrap(); + panic!("schema was not up-to-date. verify changes and re-run if correct.") + } +} + #[tokio::test] pub async fn test_entity_tag() { struct MyCustomObjTagged; diff --git a/tests/schemas/test_fed2_link.schema.graphqls b/tests/schemas/test_fed2_link.schema.graphqls new file mode 100644 index 00000000..dfb4a959 --- /dev/null +++ b/tests/schemas/test_fed2_link.schema.graphqls @@ -0,0 +1,26 @@ + + + + +extend type Product @key(fields: "upc") { + upc: String! @external + reviews: [Review!]! +} + + +type Review { + body: String! + author: User! + product: Product! +} + + +extend type User @key(fields: "id") { + id: ID! @external + reviews: [Review!]! +} + +extend schema @link( + url: "https://specs.apollo.dev/federation/v2.0", + import: ["@key", "@tag", "@shareable", "@inaccessible", "@override", "@external", "@provides", "@requires"] +)