From d836f6879e3989eec37001f52f78c0ce79923bfd Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 6 Feb 2024 20:46:27 +0000 Subject: [PATCH] Add documentation for federation support Closes gh-864 --- spring-graphql-docs/modules/ROOT/nav.adoc | 1 + .../modules/ROOT/pages/federation.adoc | 117 ++++++++++++++++++ .../modules/ROOT/pages/request-execution.adoc | 3 +- .../EntityMappingInvocationTests.java | 7 +- 4 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 spring-graphql-docs/modules/ROOT/pages/federation.adoc diff --git a/spring-graphql-docs/modules/ROOT/nav.adoc b/spring-graphql-docs/modules/ROOT/nav.adoc index d1007f1b..ed3f3286 100644 --- a/spring-graphql-docs/modules/ROOT/nav.adoc +++ b/spring-graphql-docs/modules/ROOT/nav.adoc @@ -6,6 +6,7 @@ * xref:security.adoc[] * xref:observability.adoc[] * xref:graalvm-native.adoc[] +* xref:federation.adoc[] * xref:client.adoc[] * xref:codegen.adoc[] * xref:graphiql.adoc[] diff --git a/spring-graphql-docs/modules/ROOT/pages/federation.adoc b/spring-graphql-docs/modules/ROOT/pages/federation.adoc new file mode 100644 index 00000000..431149c7 --- /dev/null +++ b/spring-graphql-docs/modules/ROOT/pages/federation.adoc @@ -0,0 +1,117 @@ +[[federation]] += Federation + +Spring for GraphQL provides a small integration layer for the +https://github.com/apollographql/federation-jvm[federation-jvm] library that in turn builds +on GraphQL Java, and helps to initialize the `graphql.schema.GraphQLSchema` for a GraphQL +Java application that is a sub-graph within a federated graph. For more details, see +https://www.apollographql.com/docs/federation/[Apollo Federation] and the +https://www.apollographql.com/docs/federation/subgraph-spec[Subgraph spec]. + +To use the support you can declare a `FederationSchemaFactory` bean in your config, and plug +it into `GraphQlSource.Builder`. In a Spring Boot application you can do this through a +`GraphQlSourceBuilderCustomizer` as follows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + public class FederationConfig { + + @Bean + public FederationSchemaFactory schemaFactory() { + return new FederationSchemaFactory(); + } + + @Bean + public GraphQlSourceBuilderCustomizer customizer(FederationSchemaFactory factory) { + return builder -> builder.schemaFactory(factory::createGraphQLSchema); + } + + } +---- + +Now your sub-graph schema can extend federated types: + +[source,graphql,indent=0,subs="verbatim,quotes"] +---- +type Book @key(fields: "id") @extends { + id: ID! @external + author: Author +} + +type Author { + id: ID + firstName: String + lastName: String +} +---- + +To assist with resolving federated types as part of an +https://www.apollographql.com/docs/federation/subgraph-spec/#understanding-query_entities[_entities] +query, you can use `@EntityMapping` methods side by side with `@SchemaMapping` methods +for the data that the subgraph application owns. For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + private static class BookController { + + @EntityMapping + public Book book(@Argument int id) { + // ... + } + + @SchemaMapping + public Author author(Book book) { + // ... + } + + } +---- + +The `@Argument` method parameters is resolved from the "representation"input map for the entity. +You can also inject the full `Map`. The below shows all supported arguments: + +[cols="1,2"] +|=== +| Method Argument | Description + +| `@Argument` +| For access to a named value from the "representation" input map, also converted to typed Object. + +| `Map` +| The full "representation" input map for the entity. + +| `@ContextValue` +| For access to an attribute from the main `GraphQLContext` in `DataFetchingEnvironment`. + +| `@LocalContextValue` +| For access to an attribute from the local `GraphQLContext` in `DataFetchingEnvironment`. + +| `GraphQLContext` +| For access to the context from the `DataFetchingEnvironment`. + +| `java.security.Principal` +| Obtained from the Spring Security context, if available. + +| `@AuthenticationPrincipal` +| For access to `Authentication#getPrincipal()` from the Spring Security context. + +| `DataFetchingFieldSelectionSet` +| For access to the selection set for the query through the `DataFetchingEnvironment`. + +| `Locale`, `Optional` +| For access to the `Locale` from the `DataFetchingEnvironment`. + +| `DataFetchingEnvironment` +| For direct access to the underlying `DataFetchingEnvironment`. + +|=== + +`@EntityMapping` methods can return `Mono`, `CompletableFuture`, `Callable`, or the actual entity. + +You can use `@GraphQlExceptionHandler` methods to map exceptions from `@EntityMapping` +methods to ``GraphQLError``'s. The errors will be included in the response of the +"_entities" query. Exception handler methods can be in the same controller or in an +`@ControllerAdvice` class. + diff --git a/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc b/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc index 42643ffc..3b027486 100644 --- a/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc +++ b/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc @@ -67,7 +67,8 @@ locations, e.g. across multiple modules. By default, `GraphQlSource.Builder` uses the GraphQL Java `SchemaGenerator` to create the `graphql.schema.GraphQLSchema`. This works for typical use, but if you need to use a -different generator, e.g. for federation, you can register a `schemaFactory` callback: +different generator, e.g. for xref:federation.adoc[federation], you can register a +`schemaFactory` callback: [source,java,indent=0,subs="verbatim,quotes"] ---- diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/federation/EntityMappingInvocationTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/federation/EntityMappingInvocationTests.java index b2266327..c8bf9fcd 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/federation/EntityMappingInvocationTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/federation/EntityMappingInvocationTests.java @@ -155,7 +155,12 @@ public class EntityMappingInvocationTests { @Nullable @EntityMapping - public Book book(@Argument int id) { + public Book book(@Argument int id, Map map) { + + assertThat(map).hasSize(2) + .containsEntry("__typename", Book.class.getSimpleName()) + .containsEntry("id", String.valueOf(id)); + return switch (id) { case -97 -> throw new IllegalArgumentException("handled"); case -98 -> throw new IllegalStateException("not handled");