diff --git a/graphql-kickstart-spring-boot-autoconfigure-rsocket/build.gradle b/graphql-kickstart-spring-boot-autoconfigure-rsocket/build.gradle new file mode 100644 index 00000000..a889b166 --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-rsocket/build.gradle @@ -0,0 +1,11 @@ +dependencies { + api(project(':graphql-kickstart-spring-rsocket')) + compileOnly(project(":graphql-kickstart-spring-boot-starter-tools")) + + implementation "org.springframework.boot:spring-boot-autoconfigure" + implementation "com.graphql-java-kickstart:graphql-java-kickstart:$LIB_GRAPHQL_SERVLET_VER" + implementation "org.springframework.boot:spring-boot-starter-rsocket" + + testImplementation "org.springframework.boot:spring-boot-starter-test" + testImplementation(project(":graphql-kickstart-spring-boot-starter-tools")) +} diff --git a/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/main/java/graphql.kickstart.spring.rsocket.boot/GraphQlSpringRSocketAutoConfiguration.java b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/main/java/graphql.kickstart.spring.rsocket.boot/GraphQlSpringRSocketAutoConfiguration.java new file mode 100644 index 00000000..6235aa46 --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/main/java/graphql.kickstart.spring.rsocket.boot/GraphQlSpringRSocketAutoConfiguration.java @@ -0,0 +1,117 @@ +package graphql.kickstart.spring.rsocket.boot; + +import static graphql.kickstart.execution.GraphQLObjectMapper.newBuilder; + +import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationOptions; +import graphql.kickstart.execution.BatchedDataLoaderGraphQLBuilder; +import graphql.kickstart.execution.GraphQLInvoker; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.config.DefaultGraphQLSchemaProvider; +import graphql.kickstart.execution.config.GraphQLBuilder; +import graphql.kickstart.execution.config.GraphQLSchemaProvider; +import graphql.kickstart.execution.config.ObjectMapperProvider; +import graphql.kickstart.spring.error.ErrorHandlerSupplier; +import graphql.kickstart.spring.error.GraphQLErrorStartupListener; +import graphql.kickstart.spring.rsocket.DefaultGraphQLSpringRSocketRootObjectBuilder; +import graphql.kickstart.spring.rsocket.DefaultGraphQlSpringRSocketContextBuilder; +import graphql.kickstart.spring.rsocket.DefaultGraphQlSpringRSocketInvocationInputFactory; +import graphql.kickstart.spring.rsocket.GraphQLSpringRSocketRootObjectBuilder; +import graphql.kickstart.spring.rsocket.GraphQlMessageHandler; +import graphql.kickstart.spring.rsocket.GraphQlSpringRSocketContextBuilder; +import graphql.kickstart.spring.rsocket.GraphQlSpringRSocketInvocationInputFactory; +import graphql.kickstart.tools.boot.GraphQLJavaToolsAutoConfiguration; +import graphql.schema.GraphQLSchema; +import java.util.function.Supplier; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.PropertySource; + +@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") +@Configuration +@ConditionalOnBean(GraphQLSchema.class) +@AutoConfigureAfter(GraphQLJavaToolsAutoConfiguration.class) +@AutoConfigureBefore(RSocketServerAutoConfiguration.class) +@Import(GraphQlMessageHandler.class) +@PropertySource("classpath:graphql.properties") +public class GraphQlSpringRSocketAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public ErrorHandlerSupplier errorHandlerSupplier() { + return new ErrorHandlerSupplier(null); + } + + @Bean + public GraphQLErrorStartupListener graphQLErrorStartupListener( + ErrorHandlerSupplier errorHandlerSupplier) { + return new GraphQLErrorStartupListener(errorHandlerSupplier, true); + } + + @Bean + @ConditionalOnMissingBean + public GraphQLObjectMapper graphQLObjectMapper( + ObjectProvider provider, ErrorHandlerSupplier errorHandlerSupplier) { + GraphQLObjectMapper.Builder builder = newBuilder(); + builder.withGraphQLErrorHandler(errorHandlerSupplier); + provider.ifAvailable(builder::withObjectMapperProvider); + return builder.build(); + } + + @Bean + @ConditionalOnMissingBean + public GraphQlSpringRSocketContextBuilder graphQLSpringWebfluxContextBuilder() { + return new DefaultGraphQlSpringRSocketContextBuilder(); + } + + @Bean + @ConditionalOnMissingBean + public GraphQLSpringRSocketRootObjectBuilder graphQLSpringWebfluxRootObjectBuilder() { + return new DefaultGraphQLSpringRSocketRootObjectBuilder(); + } + + @Bean + @ConditionalOnMissingBean + public GraphQLSchemaProvider graphQLSchemaProvider(GraphQLSchema schema) { + return new DefaultGraphQLSchemaProvider(schema); + } + + @Bean + @ConditionalOnMissingBean + public GraphQlSpringRSocketInvocationInputFactory graphQLSpringInvocationInputFactory( + GraphQLSchemaProvider graphQLSchemaProvider, + @Autowired(required = false) GraphQlSpringRSocketContextBuilder contextBuilder, + @Autowired(required = false) GraphQLSpringRSocketRootObjectBuilder rootObjectBuilder) { + return new DefaultGraphQlSpringRSocketInvocationInputFactory( + graphQLSchemaProvider, contextBuilder, rootObjectBuilder); + } + + @Bean + @ConditionalOnMissingBean + public GraphQLBuilder graphQLBuilder() { + return new GraphQLBuilder(); + } + + @Bean + @ConditionalOnMissingBean + public BatchedDataLoaderGraphQLBuilder batchedDataLoaderGraphQLBuilder( + @Autowired(required = false) + Supplier optionsSupplier) { + return new BatchedDataLoaderGraphQLBuilder(optionsSupplier); + } + + @Bean + @ConditionalOnMissingBean + public GraphQLInvoker graphQLInvoker( + GraphQLBuilder graphQLBuilder, + BatchedDataLoaderGraphQLBuilder batchedDataLoaderGraphQLBuilder) { + return new GraphQLInvoker(graphQLBuilder, batchedDataLoaderGraphQLBuilder); + } +} diff --git a/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/main/java/graphql.kickstart.spring.rsocket.boot/MonoAutoConfiguration.java b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/main/java/graphql.kickstart.spring.rsocket.boot/MonoAutoConfiguration.java new file mode 100644 index 00000000..0dcddfa3 --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/main/java/graphql.kickstart.spring.rsocket.boot/MonoAutoConfiguration.java @@ -0,0 +1,31 @@ +package graphql.kickstart.spring.rsocket.boot; + +import graphql.kickstart.tools.SchemaParser; +import graphql.kickstart.tools.SchemaParserOptions.GenericWrapper; +import graphql.kickstart.tools.boot.GraphQLJavaToolsAutoConfiguration; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import reactor.core.publisher.Mono; + +@Configuration +@ConditionalOnClass(SchemaParser.class) +@AutoConfigureBefore(GraphQLJavaToolsAutoConfiguration.class) +public class MonoAutoConfiguration { + + @Bean + GenericWrapper monoWrapper(@Autowired(required = false) List genericWrappers) { + if (notWrapsMono(genericWrappers)) { + return GenericWrapper.withTransformer(Mono.class, 0, Mono::toFuture, t -> t); + } + return null; + } + + private boolean notWrapsMono(List genericWrappers) { + return genericWrappers == null + || genericWrappers.stream().noneMatch(it -> it.getType().isAssignableFrom(Mono.class)); + } +} diff --git a/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/main/resources/META-INF/spring.factories b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..c2004672 --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + graphql.kickstart.spring.rsocket.boot.GraphQlSpringRSocketAutoConfiguration,\ + graphql.kickstart.spring.rsocket.boot.MonoAutoConfiguration diff --git a/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/main/resources/graphql.properties b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/main/resources/graphql.properties new file mode 100644 index 00000000..d0c009a4 --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/main/resources/graphql.properties @@ -0,0 +1,2 @@ +spring.rsocket.server.transport=websocket +spring.rsocket.server.port=7000 diff --git a/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/java/graphql/kickstart/spring/rsocket/boot/QueryResolver.java b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/java/graphql/kickstart/spring/rsocket/boot/QueryResolver.java new file mode 100644 index 00000000..353f4d9b --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/java/graphql/kickstart/spring/rsocket/boot/QueryResolver.java @@ -0,0 +1,13 @@ +package graphql.kickstart.spring.rsocket.boot; + +import graphql.kickstart.tools.GraphQLQueryResolver; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +@Service +class QueryResolver implements GraphQLQueryResolver { + + public Mono hello() { + return Mono.just("Hello world"); + } +} diff --git a/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/java/graphql/kickstart/spring/rsocket/boot/RSocketApplication.java b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/java/graphql/kickstart/spring/rsocket/boot/RSocketApplication.java new file mode 100644 index 00000000..27a1de62 --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/java/graphql/kickstart/spring/rsocket/boot/RSocketApplication.java @@ -0,0 +1,12 @@ +package graphql.kickstart.spring.rsocket.boot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class RSocketApplication { + + public static void main(String[] args) { + SpringApplication.run(RSocketApplication.class, args); + } +} diff --git a/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/java/graphql/kickstart/spring/rsocket/boot/RsocketGraphQLTest.java b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/java/graphql/kickstart/spring/rsocket/boot/RsocketGraphQLTest.java new file mode 100644 index 00000000..78b6c26d --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/java/graphql/kickstart/spring/rsocket/boot/RsocketGraphQLTest.java @@ -0,0 +1,68 @@ +package graphql.kickstart.spring.rsocket.boot; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import java.net.URI; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.val; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.MimeTypeUtils; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class RSocketGraphQLTest { + + private final RSocketRequester rSocketRequester = + RSocketRequester.builder() + .dataMimeType(MimeTypeUtils.APPLICATION_JSON) + .websocket(URI.create("ws://localhost:7000")); + + @Test + void query() throws JSONException { + val result = + rSocketRequester + .route("graphql") + .data("{ \"query\": \"query { hello } \"}") + .retrieveMono(String.class); + + val response = result.block(); + val json = new JSONObject(response); + assertThat(json.getJSONObject("data").get("hello")).isEqualTo("Hello world"); + } + + @Test + void subscription() { + val result = + rSocketRequester + .route("subscriptions") + .data("{ \"query\": \"subscription { hello } \"}") + .retrieveFlux(String.class); + + AtomicInteger integer = new AtomicInteger(0); + int counter = 3; + + result + .take(counter) + .doOnNext( + data -> { + try { + System.out.println(data); + val json = new JSONObject(data); + assertThat(json.getJSONObject("data").get("hello")) + .isEqualTo(integer.getAndIncrement()); + } catch (Exception e) { + fail("Exception in assertion", e); + } + }) + .blockLast(); + + assertThat(integer.get()).isEqualTo(counter); + } +} diff --git a/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/java/graphql/kickstart/spring/rsocket/boot/SubscriptionResolver.java b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/java/graphql/kickstart/spring/rsocket/boot/SubscriptionResolver.java new file mode 100644 index 00000000..c7cf9c53 --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/java/graphql/kickstart/spring/rsocket/boot/SubscriptionResolver.java @@ -0,0 +1,16 @@ +package graphql.kickstart.spring.rsocket.boot; + +import graphql.kickstart.tools.GraphQLSubscriptionResolver; +import graphql.schema.DataFetchingEnvironment; +import java.time.Duration; +import org.reactivestreams.Publisher; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +@Service +class SubscriptionResolver implements GraphQLSubscriptionResolver { + + Publisher hello(DataFetchingEnvironment env) { + return Flux.range(0, 100).delayElements(Duration.ofSeconds(1)); + } +} diff --git a/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/resources/graphql.properties b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/resources/graphql.properties new file mode 100644 index 00000000..d0c009a4 --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/resources/graphql.properties @@ -0,0 +1,2 @@ +spring.rsocket.server.transport=websocket +spring.rsocket.server.port=7000 diff --git a/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/resources/query-hello-world.graphql b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/resources/query-hello-world.graphql new file mode 100644 index 00000000..04d764aa --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/resources/query-hello-world.graphql @@ -0,0 +1,7 @@ +query { + hello +} + +subscription { + hello +} diff --git a/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/resources/schema.graphqls b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/resources/schema.graphqls new file mode 100644 index 00000000..0d1f2a75 --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-rsocket/src/test/resources/schema.graphqls @@ -0,0 +1,7 @@ +type Query { + hello: String +} + +type Subscription { + hello: Int +} diff --git a/graphql-kickstart-spring-boot-starter-rsocket/build.gradle b/graphql-kickstart-spring-boot-starter-rsocket/build.gradle new file mode 100644 index 00000000..f04fa8ae --- /dev/null +++ b/graphql-kickstart-spring-boot-starter-rsocket/build.gradle @@ -0,0 +1,3 @@ +dependencies { + api(project(':graphql-kickstart-spring-boot-autoconfigure-rsocket')) +} diff --git a/graphql-kickstart-spring-rsocket/build.gradle b/graphql-kickstart-spring-rsocket/build.gradle new file mode 100644 index 00000000..c1e2a075 --- /dev/null +++ b/graphql-kickstart-spring-rsocket/build.gradle @@ -0,0 +1,6 @@ +dependencies { + api(project(':graphql-kickstart-spring-support')) + + api "com.graphql-java-kickstart:graphql-java-kickstart:$LIB_GRAPHQL_SERVLET_VER" + api "org.springframework.boot:spring-boot-starter-rsocket" +} diff --git a/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/DefaultGraphQLSpringRSocketRootObjectBuilder.java b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/DefaultGraphQLSpringRSocketRootObjectBuilder.java new file mode 100644 index 00000000..cb7a9581 --- /dev/null +++ b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/DefaultGraphQLSpringRSocketRootObjectBuilder.java @@ -0,0 +1,12 @@ +package graphql.kickstart.spring.rsocket; + +import java.util.Map; + +public class DefaultGraphQLSpringRSocketRootObjectBuilder + implements GraphQLSpringRSocketRootObjectBuilder { + + @Override + public Object build(Map headers) { + return new Object(); + } +} diff --git a/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/DefaultGraphQlSpringRSocketContextBuilder.java b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/DefaultGraphQlSpringRSocketContextBuilder.java new file mode 100644 index 00000000..50ada3fd --- /dev/null +++ b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/DefaultGraphQlSpringRSocketContextBuilder.java @@ -0,0 +1,14 @@ +package graphql.kickstart.spring.rsocket; + +import graphql.kickstart.execution.context.DefaultGraphQLContextBuilder; +import graphql.kickstart.execution.context.GraphQLContext; +import java.util.Map; + +public class DefaultGraphQlSpringRSocketContextBuilder + implements GraphQlSpringRSocketContextBuilder { + + @Override + public GraphQLContext build(Map headers) { + return new DefaultGraphQLContextBuilder().build(); + } +} diff --git a/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/DefaultGraphQlSpringRSocketInvocationInputFactory.java b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/DefaultGraphQlSpringRSocketInvocationInputFactory.java new file mode 100644 index 00000000..bddcd153 --- /dev/null +++ b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/DefaultGraphQlSpringRSocketInvocationInputFactory.java @@ -0,0 +1,40 @@ +package graphql.kickstart.spring.rsocket; + +import graphql.kickstart.execution.GraphQLRequest; +import graphql.kickstart.execution.config.GraphQLSchemaProvider; +import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +public class DefaultGraphQlSpringRSocketInvocationInputFactory + implements GraphQlSpringRSocketInvocationInputFactory { + + private final Supplier schemaProviderSupplier; + private Supplier contextBuilderSupplier; + private Supplier rootObjectBuilderSupplier; + + public DefaultGraphQlSpringRSocketInvocationInputFactory( + GraphQLSchemaProvider schemaProvider, + GraphQlSpringRSocketContextBuilder contextBuilder, + GraphQLSpringRSocketRootObjectBuilder rootObjectBuilder) { + + Objects.requireNonNull(schemaProvider, "GraphQLSchemaProvider is required"); + this.schemaProviderSupplier = () -> schemaProvider; + if (contextBuilder != null) { + contextBuilderSupplier = () -> contextBuilder; + } + if (rootObjectBuilder != null) { + rootObjectBuilderSupplier = () -> rootObjectBuilder; + } + } + + @Override + public GraphQLSingleInvocationInput create(GraphQLRequest request, Map headers) { + return new GraphQLSingleInvocationInput( + request, + schemaProviderSupplier.get().getSchema(), + contextBuilderSupplier.get().build(headers), + rootObjectBuilderSupplier.get().build(headers)); + } +} diff --git a/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/GraphQLSpringRSocketRootObjectBuilder.java b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/GraphQLSpringRSocketRootObjectBuilder.java new file mode 100644 index 00000000..169b67f0 --- /dev/null +++ b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/GraphQLSpringRSocketRootObjectBuilder.java @@ -0,0 +1,7 @@ +package graphql.kickstart.spring.rsocket; + +import java.util.Map; + +public interface GraphQLSpringRSocketRootObjectBuilder { + Object build(Map headers); +} diff --git a/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/GraphQlMessageHandler.java b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/GraphQlMessageHandler.java new file mode 100644 index 00000000..535f3455 --- /dev/null +++ b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/GraphQlMessageHandler.java @@ -0,0 +1,124 @@ +package graphql.kickstart.spring.rsocket; + +import static java.util.Collections.singletonList; + +import graphql.ExecutionResult; +import graphql.GraphqlErrorBuilder; +import graphql.execution.NonNullableFieldWasNullException; +import graphql.kickstart.execution.GraphQLInvoker; +import graphql.kickstart.execution.GraphQLObjectMapper; +import graphql.kickstart.execution.GraphQLRequest; +import graphql.kickstart.execution.error.GenericGraphQLError; +import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.reactivestreams.Publisher; +import org.springframework.http.HttpStatus; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.stereotype.Controller; +import org.springframework.util.MimeTypeUtils; +import org.springframework.web.server.ResponseStatusException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Slf4j +@RequiredArgsConstructor +@Controller +public class GraphQlMessageHandler { + private final GraphQLObjectMapper objectMapper; + private final GraphQlSpringRSocketInvocationInputFactory invocationInputFactory; + private final GraphQLInvoker graphQLInvoker; + + @MessageMapping("graphql") + Mono request(@Payload String payload, @Headers Map headers) { + return processQuery(payload, headers).map(objectMapper::createResultFromExecutionResult); + } + + @MessageMapping("subscriptions") + Flux> subscription( + @Payload String payload, @Headers Map headers) { + return processQuery(payload, headers) + .flatMapMany(executionResult -> { + Publisher publisher; + if (executionResult.getData() instanceof Publisher) { + publisher = executionResult.getData(); + } else { + if (executionResult.getData() == null) { + publisher = Flux.empty(); + } else { + publisher = Flux.just(executionResult.getData()); + } + } + return publisher; + }) + .map( + executionResult -> { + Map result = new HashMap<>(); + result.put("data", executionResult.getData()); + return result; + }) + .onErrorResume( + t -> { + log.error("Subscription error", t); + Map output = new HashMap<>(); + if (t.getCause() instanceof NonNullableFieldWasNullException) { + NonNullableFieldWasNullException e = + (NonNullableFieldWasNullException) t.getCause(); + output.put( + "errors", + singletonList( + GraphqlErrorBuilder.newError() + .message(e.getMessage()) + .path(e.getPath()) + .build())); + } else { + output.put("errors", singletonList(new GenericGraphQLError(t.getMessage()))); + } + return Flux.just(output); + }); + } + + private Mono processQuery(String payload, Map headers) { + String contentType = String.valueOf(headers.get(MessageHeaders.CONTENT_TYPE)); + + if (MimeTypeUtils.APPLICATION_JSON_VALUE.equals(contentType)) { + return Mono.fromCallable(() -> objectMapper.readGraphQLRequest(payload)) + .flatMap( + request -> { + if (request.getQuery() == null) { + request.setQuery(""); + } + return executeRequest( + request.getQuery(), + request.getOperationName(), + request.getVariables(), + headers); + }); + } + + if ("application/graphql".equals(contentType) + || "application/graphql; charset=utf-8".equals(contentType)) { + return executeRequest(payload, null, Collections.emptyMap(), headers); + } + + throw new ResponseStatusException( + HttpStatus.UNPROCESSABLE_ENTITY, "Could not process GraphQL request"); + } + + private Mono executeRequest( + String query, + String operationName, + Map variables, + Map headers) { + + GraphQLSingleInvocationInput invocationInput = + invocationInputFactory.create(new GraphQLRequest(query, variables, operationName), headers); + return Mono.fromCompletionStage(graphQLInvoker.executeAsync(invocationInput)); + } +} diff --git a/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/GraphQlSpringRSocketContextBuilder.java b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/GraphQlSpringRSocketContextBuilder.java new file mode 100644 index 00000000..7ee424ef --- /dev/null +++ b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/GraphQlSpringRSocketContextBuilder.java @@ -0,0 +1,8 @@ +package graphql.kickstart.spring.rsocket; + +import graphql.kickstart.execution.context.GraphQLContext; +import java.util.Map; + +public interface GraphQlSpringRSocketContextBuilder { + GraphQLContext build(Map headers); +} diff --git a/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/GraphQlSpringRSocketInvocationInputFactory.java b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/GraphQlSpringRSocketInvocationInputFactory.java new file mode 100644 index 00000000..95c8d22f --- /dev/null +++ b/graphql-kickstart-spring-rsocket/src/main/java/graphql.kickstart.spring.rsocket/GraphQlSpringRSocketInvocationInputFactory.java @@ -0,0 +1,9 @@ +package graphql.kickstart.spring.rsocket; + +import graphql.kickstart.execution.GraphQLRequest; +import graphql.kickstart.execution.input.GraphQLSingleInvocationInput; +import java.util.Map; + +public interface GraphQlSpringRSocketInvocationInputFactory { + GraphQLSingleInvocationInput create(GraphQLRequest request, Map message); +} diff --git a/settings.gradle b/settings.gradle index 03f5209b..a83c9300 100644 --- a/settings.gradle +++ b/settings.gradle @@ -46,8 +46,11 @@ include ':graphql-kickstart-spring-boot-autoconfigure-tools' include ':graphql-kickstart-spring-boot-starter-tools' include ':graphql-kickstart-spring-support' include ':graphql-kickstart-spring-webflux' +include ':graphql-kickstart-spring-rsocket' include ':graphql-kickstart-spring-boot-autoconfigure-webflux' +include ':graphql-kickstart-spring-boot-autoconfigure-rsocket' include ':graphql-kickstart-spring-boot-starter-webflux' +include ':graphql-kickstart-spring-boot-starter-rsocket' include ":graphql-spring-boot-starter-test"