Add DgsGraphQlClient
Closes gh-846
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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'
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user