Reactor DataFetcher support
Closes gh-47
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import org.springframework.boot.gradle.plugin.SpringBootPlugin
|
||||
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '2.4.4' apply false
|
||||
id 'org.springframework.boot' version '2.4.5' apply false
|
||||
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
|
||||
id 'java-library'
|
||||
id 'maven'
|
||||
@@ -24,6 +24,7 @@ dependencyManagement {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api project(':spring-graphql-core')
|
||||
api project(':spring-graphql-web')
|
||||
api 'com.graphql-java:graphql-java:16.2'
|
||||
api 'io.projectreactor:reactor-core'
|
||||
|
||||
@@ -23,6 +23,7 @@ import graphql.GraphQL;
|
||||
import graphql.execution.instrumentation.ChainedInstrumentation;
|
||||
import graphql.execution.instrumentation.Instrumentation;
|
||||
import graphql.schema.GraphQLSchema;
|
||||
import graphql.schema.SchemaTransformer;
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import graphql.schema.idl.SchemaGenerator;
|
||||
import graphql.schema.idl.SchemaParser;
|
||||
@@ -36,6 +37,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.graphql.core.ReactorDataFetcherAdapter;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnClass(GraphQL.class)
|
||||
@@ -71,8 +73,11 @@ public class GraphQLAutoConfiguration {
|
||||
public GraphQL.Builder graphQLBuilder(GraphQLProperties properties, RuntimeWiring runtimeWiring,
|
||||
ResourceLoader resourceLoader,
|
||||
ObjectProvider<Instrumentation> instrumentationsProvider) {
|
||||
|
||||
Resource schemaResource = resourceLoader.getResource(properties.getSchemaLocation());
|
||||
GraphQLSchema schema = buildSchema(schemaResource, runtimeWiring);
|
||||
schema = SchemaTransformer.transformSchema(schema, ReactorDataFetcherAdapter.TYPE_VISITOR);
|
||||
|
||||
GraphQL.Builder builder = GraphQL.newGraphQL(schema);
|
||||
List<Instrumentation> instrumentations = instrumentationsProvider.orderedStream().collect(Collectors.toList());
|
||||
if (!instrumentations.isEmpty()) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '2.4.4'
|
||||
id 'org.springframework.boot' version '2.4.5'
|
||||
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
|
||||
id 'java'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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 io.spring.sample.graphql;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Repository with data fetcher methods.
|
||||
*/
|
||||
@Repository
|
||||
public class DataRepository {
|
||||
|
||||
public String getBasic(DataFetchingEnvironment environment) {
|
||||
return "Hello world!";
|
||||
}
|
||||
|
||||
public Mono<String> getGreeting(DataFetchingEnvironment environment) {
|
||||
return Mono.deferContextual(context -> {
|
||||
Object name = context.get("name");
|
||||
return Mono.delay(Duration.ofMillis(50)).map(aLong -> "Hello " + name);
|
||||
});
|
||||
}
|
||||
|
||||
public Flux<String> getGreetings(DataFetchingEnvironment environment) {
|
||||
return Mono.delay(Duration.ofMillis(50)).flatMapMany(aLong ->
|
||||
Flux.deferContextual(context -> {
|
||||
String name = context.get("name");
|
||||
return Flux.just("Hi", "Bonjour", "Hola", "Ciao", "Zdravo").map(s -> s + " " + name);
|
||||
}));
|
||||
}
|
||||
|
||||
public Flux<String> getGreetingsStream(DataFetchingEnvironment environment) {
|
||||
return Mono.delay(Duration.ofMillis(50)).flatMapMany(aLong ->
|
||||
Flux.deferContextual(context -> {
|
||||
String name = context.get("name");
|
||||
return Flux.just("Hi", "Bonjour", "Hola", "Ciao", "Zdravo").map(s -> s + " " + name);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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 io.spring.sample.graphql;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
|
||||
/**
|
||||
* WebFilter that inserts a key-value pair into the Reactor context which is
|
||||
* transferred to and accessible to Reactor-based data fetchers.
|
||||
*/
|
||||
public class ReactorContextWebFilter implements WebFilter {
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return chain.filter(exchange).contextWrite(context -> context.put("name", "007"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -18,6 +18,7 @@ package io.spring.sample.graphql;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SampleApplication {
|
||||
@@ -25,4 +26,10 @@ public class SampleApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SampleApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactorContextWebFilter reactorContextWebFilter() {
|
||||
return new ReactorContextWebFilter();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,23 +1,51 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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 io.spring.sample.graphql;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.graphql.boot.RuntimeWiringCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SampleWiring implements RuntimeWiringCustomizer {
|
||||
|
||||
private final DataRepository dataRepository;
|
||||
|
||||
|
||||
public SampleWiring(@Autowired DataRepository dataRepository) {
|
||||
this.dataRepository = dataRepository;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void customize(RuntimeWiring.Builder builder) {
|
||||
builder.type("Query", wiringBuilder -> wiringBuilder.dataFetcher("hello",
|
||||
env -> "Hello world!"));
|
||||
builder.type("Subscription", wiringBuilder -> wiringBuilder.dataFetcher("greetings",
|
||||
env -> Flux.just("Hi", "Bonjour", "Hola", "Ciao", "Zdravo")
|
||||
.delayElements(Duration.ofMillis(500))));
|
||||
|
||||
builder.type("Query", typeBuilder ->
|
||||
typeBuilder.dataFetcher("greeting", this.dataRepository::getBasic));
|
||||
|
||||
builder.type("Query", typeBuilder ->
|
||||
typeBuilder.dataFetcher("greetingMono", this.dataRepository::getGreeting));
|
||||
|
||||
builder.type("Query", typeBuilder ->
|
||||
typeBuilder.dataFetcher("greetingsFlux", this.dataRepository::getGreetings));
|
||||
|
||||
builder.type("Subscription", typeBuilder ->
|
||||
typeBuilder.dataFetcher("greetings", this.dataRepository::getGreetingsStream));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
type Query {
|
||||
hello: String
|
||||
greeting: String
|
||||
greetingMono : String
|
||||
greetingsFlux : [String]
|
||||
}
|
||||
type Subscription {
|
||||
greetings: String
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
let result;
|
||||
client.subscribe(
|
||||
{
|
||||
query: '{ hello }',
|
||||
query: '{ greeting }',
|
||||
},
|
||||
{
|
||||
next: (data) => (result = data),
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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 io.spring.sample.graphql;
|
||||
|
||||
import graphql.GraphQL;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.graphql.WebGraphQLService;
|
||||
import org.springframework.graphql.test.query.GraphQLTester;
|
||||
|
||||
/**
|
||||
* GraphQL query tests directly via {@link GraphQL}.
|
||||
*/
|
||||
@SpringBootTest
|
||||
public class QueryTests {
|
||||
|
||||
private GraphQLTester graphQLTester;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setUp(@Autowired WebGraphQLService service) {
|
||||
this.graphQLTester = GraphQLTester.create(webInput ->
|
||||
service.execute(webInput).contextWrite(context -> context.put("name", "James")));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void greetingMono() {
|
||||
this.graphQLTester.query("{greetingMono}")
|
||||
.execute()
|
||||
.path("greetingMono")
|
||||
.entity(String.class)
|
||||
.isEqualTo("Hello James");
|
||||
}
|
||||
|
||||
@Test
|
||||
void greetingsFlux() {
|
||||
this.graphQLTester.query("{greetingsFlux}")
|
||||
.execute()
|
||||
.path("greetingsFlux")
|
||||
.entityList(String.class)
|
||||
.containsExactly("Hi James", "Bonjour James", "Hola James", "Ciao James", "Zdravo James");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,14 +30,15 @@ import org.springframework.graphql.test.query.GraphQLTester;
|
||||
* GraphQL subscription tests directly via {@link GraphQL}.
|
||||
*/
|
||||
@SpringBootTest
|
||||
public class SubscriptionGraphQLTests {
|
||||
public class SubscriptionTests {
|
||||
|
||||
private GraphQLTester graphQLTester;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setUp(@Autowired WebGraphQLService service) {
|
||||
this.graphQLTester = GraphQLTester.create(service);
|
||||
this.graphQLTester = GraphQLTester.create(webInput ->
|
||||
service.execute(webInput).contextWrite(context -> context.put("name", "James")));
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +51,11 @@ public class SubscriptionGraphQLTests {
|
||||
.toFlux("greetings", String.class);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNext("Hi", "Bonjour", "Hola", "Ciao", "Zdravo")
|
||||
.expectNext("Hi James")
|
||||
.expectNext("Bonjour James")
|
||||
.expectNext("Hola James")
|
||||
.expectNext("Ciao James")
|
||||
.expectNext("Zdravo James")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@@ -64,8 +69,8 @@ public class SubscriptionGraphQLTests {
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(spec -> spec.path("greetings").valueExists())
|
||||
.consumeNextWith(spec -> spec.path("greetings").matchesJson("\"Bonjour\""))
|
||||
.consumeNextWith(spec -> spec.path("greetings").matchesJson("\"Hola\""))
|
||||
.consumeNextWith(spec -> spec.path("greetings").matchesJson("\"Bonjour James\""))
|
||||
.consumeNextWith(spec -> spec.path("greetings").matchesJson("\"Hola James\""))
|
||||
.expectNextCount(2)
|
||||
.verifyComplete();
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '2.4.4'
|
||||
id 'org.springframework.boot' version '2.4.5'
|
||||
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
|
||||
id 'java'
|
||||
}
|
||||
|
||||
@@ -15,4 +15,9 @@ pluginManagement {
|
||||
}
|
||||
|
||||
rootProject.name = 'spring-graphql'
|
||||
include 'spring-graphql-web', 'spring-graphql-test', 'graphql-spring-boot-starter', 'samples:webmvc-http', 'samples:webflux-websocket'
|
||||
include 'spring-graphql-core',
|
||||
'spring-graphql-web',
|
||||
'spring-graphql-test',
|
||||
'graphql-spring-boot-starter',
|
||||
'samples:webmvc-http',
|
||||
'samples:webflux-websocket'
|
||||
|
||||
48
spring-graphql-core/build.gradle
Normal file
48
spring-graphql-core/build.gradle
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
plugins {
|
||||
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
|
||||
id 'java-library'
|
||||
id 'maven'
|
||||
}
|
||||
|
||||
description = "Basic Support and Utilities for Spring GraphQL Applications"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.6"
|
||||
mavenBom "org.springframework:spring-framework-bom:5.3.6"
|
||||
}
|
||||
generatedPomCustomization {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'com.graphql-java:graphql-java:16.2'
|
||||
api 'io.projectreactor:reactor-core'
|
||||
api 'org.springframework:spring-context'
|
||||
|
||||
compileOnly "javax.annotation:javax.annotation-api:1.3.2"
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
|
||||
testImplementation 'org.assertj:assertj-core:3.19.0'
|
||||
testImplementation 'org.mockito:mockito-core:3.8.0'
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
|
||||
testRuntime 'org.apache.logging.log4j:log4j-core:2.14.1'
|
||||
testRuntime 'org.apache.logging.log4j:log4j-slf4j-impl:2.14.1'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed"
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/gradle/publishing.gradle"
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.core;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import graphql.ExecutionInput;
|
||||
import graphql.GraphQLContext;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import graphql.schema.GraphQLCodeRegistry;
|
||||
import graphql.schema.GraphQLFieldDefinition;
|
||||
import graphql.schema.GraphQLFieldsContainer;
|
||||
import graphql.schema.GraphQLSchemaElement;
|
||||
import graphql.schema.GraphQLTypeVisitor;
|
||||
import graphql.schema.GraphQLTypeVisitorStub;
|
||||
import graphql.util.TraversalControl;
|
||||
import graphql.util.TraverserContext;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.context.ContextView;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Adapter to wrap a registered {@link DataFetcher} and enable it to return
|
||||
* {@link Flux} or {@link Mono}, also adding Reactor Context passed through
|
||||
* the {@link ExecutionInput} via {@link #addReactorContext(ExecutionInput, ContextView)}.
|
||||
* Use {@link #TYPE_VISITOR} to transform the
|
||||
* {@link graphql.schema.GraphQLSchema} and apply the adapter.
|
||||
*/
|
||||
public class ReactorDataFetcherAdapter implements DataFetcher<Object> {
|
||||
|
||||
private static final String REACTOR_CONTEXT_KEY =
|
||||
ReactorDataFetcherAdapter.class.getName() + ".REACTOR_CONTEXT";
|
||||
|
||||
|
||||
private final DataFetcher<?> delegate;
|
||||
|
||||
private final boolean subscription;
|
||||
|
||||
|
||||
private ReactorDataFetcherAdapter(DataFetcher<?> delegate, boolean subscription) {
|
||||
Assert.notNull(delegate, "'delegate' DataFetcher is required");
|
||||
this.delegate = delegate;
|
||||
this.subscription = subscription;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object get(DataFetchingEnvironment environment) throws Exception {
|
||||
Object value = this.delegate.get(environment);
|
||||
|
||||
if (this.subscription) {
|
||||
ContextView context = getReactorContext(environment);
|
||||
return (context != null ? Flux.from((Publisher<?>) value).contextWrite(context) : value);
|
||||
}
|
||||
|
||||
if (value instanceof Flux) {
|
||||
value = ((Flux<?>) value).collectList();
|
||||
}
|
||||
|
||||
if (value instanceof Mono) {
|
||||
Mono<?> valueMono = (Mono<?>) value;
|
||||
ContextView reactorContext = getReactorContext(environment);
|
||||
if (reactorContext != null) {
|
||||
valueMono = valueMono.contextWrite(reactorContext);
|
||||
}
|
||||
value = valueMono.toFuture();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private ContextView getReactorContext(DataFetchingEnvironment environment) {
|
||||
GraphQLContext graphQLContext = environment.getContext();
|
||||
return graphQLContext.get(REACTOR_CONTEXT_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the given Reactor Context into the {@link ExecutionInput} context
|
||||
* for later retrieval from the {@link DataFetchingEnvironment}.
|
||||
*/
|
||||
public static void addReactorContext(ExecutionInput executionInput, ContextView reactorContext) {
|
||||
GraphQLContext graphQLContext = (GraphQLContext) executionInput.getContext();
|
||||
graphQLContext.put(REACTOR_CONTEXT_KEY, reactorContext);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link GraphQLTypeVisitor} that wraps non-GraphQL data fetchers and
|
||||
* adapts them if they return {@link Flux} or {@link Mono}.
|
||||
*/
|
||||
public static GraphQLTypeVisitor TYPE_VISITOR = new GraphQLTypeVisitorStub() {
|
||||
|
||||
@Override
|
||||
public TraversalControl visitGraphQLFieldDefinition(
|
||||
GraphQLFieldDefinition fieldDefinition, TraverserContext<GraphQLSchemaElement> context) {
|
||||
|
||||
GraphQLCodeRegistry.Builder codeRegistry = context.getVarFromParents(GraphQLCodeRegistry.Builder.class);
|
||||
GraphQLFieldsContainer parent = (GraphQLFieldsContainer) context.getParentNode();
|
||||
DataFetcher<?> dataFetcher = codeRegistry.getDataFetcher(parent, fieldDefinition);
|
||||
|
||||
if (dataFetcher.getClass().getPackage().getName().startsWith("graphql.")) {
|
||||
return TraversalControl.CONTINUE;
|
||||
}
|
||||
|
||||
Method method = ClassUtils.getMethod(dataFetcher.getClass(), "get", DataFetchingEnvironment.class);
|
||||
method = ClassUtils.getMostSpecificMethod(method, dataFetcher.getClass());
|
||||
Class<?> returnType = method.getReturnType();
|
||||
System.out.println(returnType.getName());
|
||||
|
||||
dataFetcher = new ReactorDataFetcherAdapter(dataFetcher, parent.getName().equals("Subscription"));
|
||||
codeRegistry.dataFetcher(parent, fieldDefinition, dataFetcher);
|
||||
return TraversalControl.CONTINUE;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.core;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import graphql.ExecutionInput;
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.GraphQL;
|
||||
import graphql.schema.GraphQLSchema;
|
||||
import graphql.schema.SchemaTransformer;
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import graphql.schema.idl.SchemaGenerator;
|
||||
import graphql.schema.idl.SchemaParser;
|
||||
import graphql.schema.idl.TypeDefinitionRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactorDataFetcherAdapter}.
|
||||
*/
|
||||
public class ReactorDataFetcherAdapterTests {
|
||||
|
||||
@Test
|
||||
void monoDataFetcher() throws Exception {
|
||||
|
||||
GraphQL graphQL = initGraphQL("type Query { greeting: String }", builder -> {
|
||||
builder.type("Query", typeBuilder -> typeBuilder.dataFetcher("greeting",
|
||||
env -> Mono.deferContextual(context -> {
|
||||
Object name = context.get("name");
|
||||
return Mono.delay(Duration.ofMillis(50)).map(aLong -> "Hello " + name);
|
||||
})));
|
||||
});
|
||||
|
||||
ExecutionInput executionInput = initExecutionInput("{ greeting }", Context.of("name", "007"));
|
||||
Map<String, Object> data = graphQL.executeAsync(executionInput).get().getData();
|
||||
|
||||
assertThat(data).hasSize(1).containsEntry("greeting", "Hello 007");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fluxDataFetcher() throws Exception {
|
||||
|
||||
GraphQL graphQL = initGraphQL("type Query { greetings: [String] }", builder -> {
|
||||
builder.type("Query", typeBuilder -> typeBuilder.dataFetcher("greetings",
|
||||
env -> Mono.delay(Duration.ofMillis(50)).flatMapMany(aLong ->
|
||||
Flux.deferContextual(context -> {
|
||||
String name = context.get("name");
|
||||
return Flux.just("Hi", "Bonjour", "Hola").map(s -> s + " " + name);
|
||||
}))));
|
||||
});
|
||||
|
||||
ExecutionInput executionInput = initExecutionInput("{ greetings }", Context.of("name", "007"));
|
||||
|
||||
Map<String, Object> data = graphQL.executeAsync(executionInput).get().getData();
|
||||
assertThat((List<String>) data.get("greetings")).containsExactly("Hi 007", "Bonjour 007", "Hola 007");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fluxDataFetcherSubscription() throws Exception {
|
||||
|
||||
GraphQL graphQL = initGraphQL(
|
||||
"type Query { greeting: String } type Subscription { greetings: String }", builder ->
|
||||
builder.type("Subscription", typeBuilder -> typeBuilder.dataFetcher("greetings",
|
||||
env -> Mono.delay(Duration.ofMillis(50)).flatMapMany(aLong ->
|
||||
Flux.deferContextual(context -> {
|
||||
String name = context.get("name");
|
||||
return Flux.just("Hi", "Bonjour", "Hola").map(s -> s + " " + name);
|
||||
}))
|
||||
)));
|
||||
|
||||
ExecutionInput input = initExecutionInput("subscription { greetings }", Context.of("name", "007"));
|
||||
Publisher<String> publisher = graphQL.executeAsync(input).get().getData();
|
||||
|
||||
List<String> actual = Flux.from(publisher)
|
||||
.cast(ExecutionResult.class)
|
||||
.map(result -> ((Map<String, ?>) result.getData()).get("greetings"))
|
||||
.cast(String.class)
|
||||
.collectList()
|
||||
.block();
|
||||
|
||||
assertThat(actual).containsExactly("Hi 007", "Bonjour 007", "Hola 007");
|
||||
}
|
||||
|
||||
|
||||
private GraphQL initGraphQL(String schemaValue, Consumer<RuntimeWiring.Builder> consumer) throws IOException {
|
||||
Resource schemaResource = new ByteArrayResource(schemaValue.getBytes(StandardCharsets.UTF_8));
|
||||
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(schemaResource.getInputStream());
|
||||
|
||||
RuntimeWiring.Builder wiringBuilder = RuntimeWiring.newRuntimeWiring();
|
||||
consumer.accept(wiringBuilder);
|
||||
|
||||
GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(typeRegistry, wiringBuilder.build());
|
||||
schema = SchemaTransformer.transformSchema(schema, ReactorDataFetcherAdapter.TYPE_VISITOR);
|
||||
return GraphQL.newGraphQL(schema).build();
|
||||
}
|
||||
|
||||
private ExecutionInput initExecutionInput(String query, Context reactorContext) {
|
||||
ExecutionInput input = ExecutionInput.newExecutionInput().query(query).build();
|
||||
ReactorDataFetcherAdapter.addReactorContext(input, reactorContext);
|
||||
return input;
|
||||
}
|
||||
|
||||
}
|
||||
14
spring-graphql-core/src/test/resources/log4j2-test.xml
Normal file
14
spring-graphql-core/src/test/resources/log4j2-test.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="WARN">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{1.} - %msg%n" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="org.springframework" level="debug" />
|
||||
<Root level="error">
|
||||
<AppenderRef ref="Console" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
@@ -14,8 +14,8 @@ java {
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.3"
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.6"
|
||||
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.3"
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.6"
|
||||
mavenBom "org.springframework:spring-framework-bom:5.3.6"
|
||||
}
|
||||
generatedPomCustomization {
|
||||
@@ -31,7 +31,7 @@ dependencies {
|
||||
api 'org.springframework:spring-test'
|
||||
api 'com.jayway.jsonpath:json-path:2.5.0'
|
||||
|
||||
compileOnly "javax.annotation:javax.annotation-api:1.3.2"
|
||||
compileOnly "javax.annotation:javax.annotation-api:1.3.2"
|
||||
compileOnly 'org.springframework:spring-webflux'
|
||||
compileOnly 'org.springframework:spring-webmvc'
|
||||
compileOnly 'org.springframework:spring-websocket'
|
||||
|
||||
@@ -24,6 +24,7 @@ dependencyManagement {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api project(':spring-graphql-core')
|
||||
api 'com.graphql-java:graphql-java:16.2'
|
||||
api 'io.projectreactor:reactor-core'
|
||||
api 'org.springframework:spring-context'
|
||||
|
||||
@@ -23,6 +23,8 @@ import graphql.ExecutionInput;
|
||||
import graphql.ExecutionResult;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.core.ReactorDataFetcherAdapter;
|
||||
|
||||
/**
|
||||
* Base class for {@link WebGraphQLService} implementations, providing support
|
||||
* for customizations of the request through a {@link WebInterceptor} chain.
|
||||
@@ -59,7 +61,11 @@ public abstract class AbstractWebGraphQLService implements WebGraphQLService {
|
||||
}
|
||||
|
||||
private Mono<ExecutionInput> preHandle(WebInput input) {
|
||||
Mono<ExecutionInput> resultMono = Mono.just(input.toExecutionInput());
|
||||
Mono<ExecutionInput> resultMono = Mono.deferContextual(contextView -> {
|
||||
ExecutionInput executionInput = input.toExecutionInput();
|
||||
ReactorDataFetcherAdapter.addReactorContext(executionInput, contextView);
|
||||
return Mono.just(executionInput);
|
||||
});
|
||||
for (WebInterceptor interceptor : this.interceptors) {
|
||||
resultMono = resultMono.flatMap(executionInput -> interceptor.preHandle(executionInput, input));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user