Add DgsGraphQlClient

Closes gh-846
This commit is contained in:
rstoyanchev
2024-02-19 19:41:11 +00:00
parent b727854fa5
commit c5acccf770
5 changed files with 323 additions and 38 deletions

View File

@@ -33,6 +33,7 @@ dependencies {
api("jakarta.persistence:jakarta.persistence-api:3.1.0")
api("com.apollographql.federation:federation-graphql-java-support:4.4.0")
api("com.netflix.graphql.dgs.codegen:graphql-dgs-codegen-shared-core:6.1.4")
api("com.google.code.findbugs:jsr305:3.0.2")

View File

@@ -25,6 +25,12 @@ with options applicable to all transports.
Once `GraphQlClient` is built you can begin to make xref:client.adoc#client.requests[requests].
Typically, the GraphQL operation for a request is provided as text. Alternatively, you
can use https://github.com/Netflix/dgs-codegen[DGS Codegen] client API classes through
xref:client.adoc#client.dgsgraphqlclient[DgsGraphQlClient], which can wrap any of the
above `GraphQlClient` extensions.
[[client.httpsyncgraphqlclient]]
=== HTTP Sync
@@ -247,6 +253,7 @@ builders of all extensions. Currently, it has lets you configure:
[[client.requests]]
== Requests
@@ -591,3 +598,47 @@ Once the interceptor is created, register it through the client builder. For exa
.build();
----
[[client.dgsgraphqlclient]]
== DGS Codegen
As an alternative to providing the operation such as a mutation, query, or subscription as
text, you can use the https://github.com/Netflix/dgs-codegen[DGS Codegen] library to
generate client API classes that let you use a fluent API to define the request.
Spring for GraphQL provides xref:client.adoc#client.dgsgraphqlclient[DgsGraphQlClient]
that wraps any `GraphQlClient` and helps to prepare the request with generated client
API classes.
For example, given the following schema:
[source,graphql,indent=0,subs="verbatim,quotes"]
----
type Query {
books: [Book]
}
type Book {
id: ID
name: String
}
----
You can perform a request as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
HttpGraphQlClient client = ... ;
DgsGraphQlClient dgsClient = DgsGraphQlClient.create(client); // <1>
List<Book> books = dgsClient.request(new BooksGraphQLQuery()) // <2>
.projection(new BooksProjectionRoot<>().id().name()) // <3>
.retrieveSync()
.toEntityList(Book.class);
----
<1> - Create `DgsGraphQlClient` by wrapping any `GraphQlClient`.
<2> - Specify the operation for the request.
<3> - Define the selection set.

View File

@@ -2,10 +2,10 @@
= Code Generation
You can use tools such as
https://netflix.github.io/dgs/generating-code-from-schema/[DGS Code Generation] to generate
https://netflix.github.io/dgs/generating-code-from-schema/[DGS Codegen] to generate
Java types from the GraphQL schema. The following can be generated:
1. Client types for requests (e.g. queries, mutations) input types, and response selection types.
1. Client types for requests (e.g. query, mutation) input types, and response selection types.
2. Data types corresponding to GraphQL schema types.
Code generation may not be ideal for your own application's data types especially if you
@@ -13,41 +13,10 @@ want to add logic to them. Code generation, however, is a good fit for client ty
those define the request, and don't need to have other logic. As a client, you may also
choose to generate the data types for the response.
Client generated types can be used with Spring's `GraphQlClient`. Start by following the
Client generated types can be used with Spring's
xref:client.adoc#client.dgsgraphqlclient[DgsGraphQlClient]. Start by following the
instructions for the DGS code generation plugin to generate client API types. Then, given
a schema like this:
[source,graphql,indent=0,subs="verbatim,quotes"]
----
type Query {
books: [Book]
}
type Book {
id: ID
name: String
}
----
DGS Codegen generates `BooksGraphQLQuery` and `BooksProjectionRoot` that you can use with
`GraphQlClient` over HTTP (or any supported transport) as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
HttpGraphQlClient client =
HttpGraphQlClient.create(WebClient.create("http://localhost:8080/graphql"));
BooksGraphQLQuery query = new BooksGraphQLQuery();
String document = new GraphQLQueryRequest(query, new BooksProjectionRoot<>().id().name()).serialize();
List<Book> books = client.document(document)
.retrieve(query.getOperationName())
.toEntityList(Book.class) // possibly also generated or imported if available
.block();
----
TIP: We intend to further simplify the above code in
https://github.com/spring-projects/spring-graphql/issues/846[spring-graphql#846].
You can use Spring Initializer at https://start.spring.io to create a Spring project with
the DGS Code Generation Gradle or Maven plugin.
TIP: Spring Initializer at https://start.spring.io can create a Spring project with
the DGS Codegen Gradle or Maven plugin.

View File

@@ -32,7 +32,11 @@ dependencies {
compileOnly 'com.fasterxml.jackson.core:jackson-databind'
compileOnly 'com.apollographql.federation:federation-graphql-java-support'
compileOnly('com.apollographql.federation:federation-graphql-java-support')
compileOnly('com.netflix.graphql.dgs.codegen:graphql-dgs-codegen-shared-core') {
exclude group: "com.apollographql.federation", module: "federation-graphql-java-support"
exclude group: "com.graphql-java", module: "graphql-java"
}
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.assertj:assertj-core'

View File

@@ -0,0 +1,260 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.client;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import com.netflix.graphql.dgs.client.codegen.BaseProjectionNode;
import com.netflix.graphql.dgs.client.codegen.GraphQLQuery;
import com.netflix.graphql.dgs.client.codegen.GraphQLQueryRequest;
import graphql.schema.Coercing;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Simple wrapper around a {@link GraphQlClient} that prepares the request
* from classes generated with the
* <a href="https://github.com/Netflix/dgs-codegen">DGS Code Generation</a> library.
*
* <pre class="code">
* GraphQlClient client = ... ;
* DgsGraphQlClient dgsClient = DgsGraphQlClient.create(client);
*
* List<Book> books = dgsClient.request(new BooksGraphQLQuery())
* .projection(new BooksProjectionRoot<>().id().name())
* .retrieveSync()
* .toEntityList(Book.class);
* </pre>
*
* @author Rossen Stoyanchev
* @since 1.3
*/
public final class DgsGraphQlClient {
private final GraphQlClient graphQlClient;
private DgsGraphQlClient(GraphQlClient graphQlClient) {
this.graphQlClient = graphQlClient;
}
/**
* Return the wrapped {@link GraphQlClient} to delegate to.
*/
public GraphQlClient getGraphQlClient() {
return this.graphQlClient;
}
/**
* Start defining a GraphQL request for the given {@link GraphQLQuery}.
*/
public RequestSpec request(GraphQLQuery query) {
return new RequestSpec(query);
}
/**
* Create instance that wraps the given {@link GraphQlClient}.
* @param client the client to delegate to
*/
public static DgsGraphQlClient create(GraphQlClient client) {
return new DgsGraphQlClient(client);
}
/**
* Declare options to gather input for a GraphQL request and execute it.
*/
public final class RequestSpec {
private final GraphQLQuery query;
@Nullable
private BaseProjectionNode projectionNode;
@Nullable
private Map<Class<?>, Coercing<?, ?>> coercingMap;
@Nullable
private Map<String, Object> attributes;
private RequestSpec(GraphQLQuery query) {
Assert.notNull(query, "Expected GraphQLQuery");
this.query = query;
}
/**
* Provide a {@link BaseProjectionNode} that defines the response selection set.
* @return ths same builder instance
*/
public RequestSpec projection(BaseProjectionNode projectionNode) {
this.projectionNode = projectionNode;
return this;
}
/**
* Configure {@link Coercing} for serialization of scalar types.
* @return ths same builder instance
*/
public RequestSpec coercing(Class<?> scalarType, Coercing<?, ?> coercing) {
this.coercingMap = (this.coercingMap != null ? this.coercingMap : new LinkedHashMap<>());
this.coercingMap.put(scalarType, coercing);
return this;
}
/**
* Configure {@link Coercing} for serialization of scalar types.
* @return ths same builder instance
*/
public RequestSpec coercing(Map<Class<?>, Coercing<?, ?>> coercingMap) {
this.coercingMap = (this.coercingMap != null ? this.coercingMap : new LinkedHashMap<>());
this.coercingMap.putAll(coercingMap);
return this;
}
/**
* Set a client request attribute.
* <p>This is purely for client side request processing, i.e. available
* throughout the {@link GraphQlClientInterceptor} chain but not sent.
* @return ths same builder instance
*/
public RequestSpec attribute(String name, Object value) {
this.attributes = (this.attributes != null ? this.attributes : new HashMap<>());
this.attributes.put(name, value);
return this;
}
/**
* Manipulate the client request attributes. The map provided to the consumer
* is "live", so the consumer can inspect and modify attributes accordingly.
* @return ths same builder instance
*/
public RequestSpec attributes(Consumer<Map<String, Object>> attributesConsumer) {
this.attributes = (this.attributes != null ? this.attributes : new HashMap<>());
attributesConsumer.accept(this.attributes);
return this;
}
/**
* Create {@link GraphQLQueryRequest}, serialize it to a String document
* to send, and delegate to the wrapped {@code GraphQlClient}.
* <p>See Javadoc of delegate method
* {@link GraphQlClient.RequestSpec#retrieveSync(String)} for details.
* The path used is the operationName.
*/
public GraphQlClient.RetrieveSyncSpec retrieveSync() {
return initRequestSpec().retrieveSync(getDefaultPath());
}
/**
* Variant of {@link #executeSync()} with explicit path relative to the "data" key.
*/
public GraphQlClient.RetrieveSyncSpec retrieveSync(String path) {
return initRequestSpec().retrieveSync(path);
}
/**
* Create {@link GraphQLQueryRequest}, serialize it to a String document
* to send, and delegate to the wrapped {@code GraphQlClient}.
* <p>See Javadoc of delegate method
* {@link GraphQlClient.RequestSpec#retrieve(String)} for details.
* The path used is the operationName.
*/
public GraphQlClient.RetrieveSpec retrieve() {
return initRequestSpec().retrieve(getDefaultPath());
}
/**
* Variant of {@link #retrieve()} with explicit path relative to the "data" key.
*/
public GraphQlClient.RetrieveSpec retrieve(String path) {
return initRequestSpec().retrieve(path);
}
/**
* Create {@link GraphQLQueryRequest}, serialize it to a String document
* to send, and delegate to the wrapped {@code GraphQlClient}.
* <p>See Javadoc of delegate method
* {@link GraphQlClient.RequestSpec#retrieveSubscription(String)} for details.
* The path used is the operationName.
*/
public GraphQlClient.RetrieveSubscriptionSpec retrieveSubscription() {
return initRequestSpec().retrieveSubscription(getDefaultPath());
}
/**
* Create {@link GraphQLQueryRequest}, serialize it to a String document
* to send, and delegate to the wrapped {@code GraphQlClient}.
* <p>See Javadoc of delegate method
* {@link GraphQlClient.RequestSpec#executeSync()} for details.
*/
public ClientGraphQlResponse executeSync() {
return initRequestSpec().executeSync();
}
/**
* Create {@link GraphQLQueryRequest}, serialize it to a String document
* to send, and delegate to the wrapped {@code GraphQlClient}.
* <p>See Javadoc of delegate method
* {@link GraphQlClient.RequestSpec#execute()} for details.
*/
public Mono<ClientGraphQlResponse> execute() {
return initRequestSpec().execute();
}
/**
* Create {@link GraphQLQueryRequest}, serialize it to a String document
* to send, and delegate to the wrapped {@code GraphQlClient}.
* <p>See Javadoc of delegate method
* {@link GraphQlClient.RequestSpec#executeSubscription()} for details.
*/
public Flux<ClientGraphQlResponse> executeSubscription() {
return initRequestSpec().executeSubscription();
}
private GraphQlClient.RequestSpec initRequestSpec() {
Assert.state(this.projectionNode != null || this.coercingMap == null,
"Coercing map provided without projection");
GraphQLQueryRequest request = (this.coercingMap != null ?
new GraphQLQueryRequest(this.query, this.projectionNode, this.coercingMap) :
new GraphQLQueryRequest(this.query, this.projectionNode));
String operationName = (this.query.getName() != null ? this.query.getName() : null);
return graphQlClient.document(request.serialize())
.operationName(operationName)
.attributes(map -> {
if (this.attributes != null) {
map.putAll(this.attributes);
}
});
}
private String getDefaultPath() {
return this.query.getOperationName();
}
}
}