diff --git a/demoapp/pom.xml b/demoapp/pom.xml index 8dd3151..9d0a43f 100644 --- a/demoapp/pom.xml +++ b/demoapp/pom.xml @@ -6,11 +6,11 @@ org.rapid-graphql rapid-graphql - 0.0.3 + 0.0.4 rapid-graphql-demoapp - 0.0.3 + 0.0.4 jar demoapp @@ -27,10 +27,22 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-websocket + org.rapid-graphql rapid-graphql-starter - 0.0.3 + 0.0.4 @@ -70,7 +82,6 @@ guava 31.0.1-jre - diff --git a/demoapp/src/main/java/org/rapidgraphql/helloworld/MySubscription.java b/demoapp/src/main/java/org/rapidgraphql/helloworld/MySubscription.java new file mode 100644 index 0000000..6e34f4f --- /dev/null +++ b/demoapp/src/main/java/org/rapidgraphql/helloworld/MySubscription.java @@ -0,0 +1,26 @@ +package org.rapidgraphql.helloworld; + +import graphql.kickstart.tools.GraphQLSubscriptionResolver; +import graphql.schema.DataFetchingEnvironment; +import lombok.extern.log4j.Log4j2; +import org.reactivestreams.Publisher; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.time.Duration; + +@Service +@Log4j2 +class MySubscription implements GraphQLSubscriptionResolver { + + public Publisher hello(DataFetchingEnvironment env) { + return Flux.range(0, 100) + .delayElements(Duration.ofSeconds(1)) + .map(this::fun); + } + private Integer fun(Integer i) { + Integer result = i*10; + log.info("result={}", result); + return result; + } +} \ No newline at end of file diff --git a/demoapp/src/test/java/org/rapidgraphql/app/RapidGraphQLApplicationTests.java b/demoapp/src/test/java/org/rapidgraphql/app/RapidGraphQLApplicationTests.java index df62b0a..c921e3a 100644 --- a/demoapp/src/test/java/org/rapidgraphql/app/RapidGraphQLApplicationTests.java +++ b/demoapp/src/test/java/org/rapidgraphql/app/RapidGraphQLApplicationTests.java @@ -3,7 +3,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class RapidGraphQLApplicationTests { @Test diff --git a/pom.xml b/pom.xml index ced7d3f..492cc27 100644 --- a/pom.xml +++ b/pom.xml @@ -5,12 +5,12 @@ org.springframework.boot spring-boot-starter-parent - 2.6.3 + 2.7.15 org.rapid-graphql rapid-graphql - 0.0.3 + 0.0.4 rapid-graphql Demo graphql booster app @@ -21,11 +21,11 @@ 11 - 12.0.0 - 17.3 + 14.0.0 + 21.0 3.21.2 - 17.0 - 2.6.3 + 17.1 + 2.7.15 diff --git a/starter/pom.xml b/starter/pom.xml index f91e8ed..fbbb45a 100644 --- a/starter/pom.xml +++ b/starter/pom.xml @@ -6,11 +6,11 @@ org.rapid-graphql rapid-graphql - 0.0.3 + 0.0.4 rapid-graphql-starter - 0.0.3 + 0.0.4 jar starter @@ -41,7 +41,10 @@ org.springframework spring-context - + + org.springframework.boot + spring-boot-starter-webflux + org.projectlombok lombok @@ -71,7 +74,7 @@ com.graphql-java graphql-java-extended-scalars - 17.0 + ${graphql-scalars.version} diff --git a/starter/src/main/java/org/rapidgraphql/schemabuilder/DefinitionFactory.java b/starter/src/main/java/org/rapidgraphql/schemabuilder/DefinitionFactory.java index af51638..2925ea3 100644 --- a/starter/src/main/java/org/rapidgraphql/schemabuilder/DefinitionFactory.java +++ b/starter/src/main/java/org/rapidgraphql/schemabuilder/DefinitionFactory.java @@ -1,29 +1,13 @@ package org.rapidgraphql.schemabuilder; import graphql.VisibleForTesting; -import graphql.kickstart.tools.GraphQLMutationResolver; -import graphql.kickstart.tools.GraphQLQueryResolver; -import graphql.kickstart.tools.GraphQLResolver; -import graphql.kickstart.tools.SchemaError; -import graphql.language.Definition; -import graphql.language.DirectiveDefinition; -import graphql.language.DirectiveLocation; -import graphql.language.EnumTypeDefinition; -import graphql.language.EnumValueDefinition; -import graphql.language.FieldDefinition; -import graphql.language.InputObjectTypeDefinition; -import graphql.language.InputValueDefinition; -import graphql.language.ListType; -import graphql.language.NonNullType; -import graphql.language.ObjectTypeDefinition; -import graphql.language.ObjectTypeExtensionDefinition; +import graphql.kickstart.tools.*; import graphql.language.Type; -import graphql.language.TypeName; +import graphql.language.*; import graphql.scalars.ExtendedScalars; import graphql.schema.DataFetchingEnvironment; import graphql.schema.GraphQLScalarType; import org.checkerframework.checker.nullness.qual.NonNull; -import org.jetbrains.annotations.NotNull; import org.rapidgraphql.annotations.GraphQLIgnore; import org.rapidgraphql.annotations.GraphQLInputType; import org.rapidgraphql.directives.SecuredDirectiveWiring; @@ -31,26 +15,12 @@ import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; -import java.lang.reflect.AnnotatedParameterizedType; -import java.lang.reflect.AnnotatedType; -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.lang.reflect.ParameterizedType; +import java.lang.reflect.*; import java.math.BigDecimal; import java.math.BigInteger; import java.time.LocalDate; import java.time.OffsetDateTime; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Queue; -import java.util.Set; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.function.Function; @@ -60,11 +30,7 @@ import static java.lang.String.format; import static java.util.Map.entry; import static org.rapidgraphql.schemabuilder.MethodsFilter.*; -import static org.rapidgraphql.schemabuilder.TypeUtils.actualTypeArgument; -import static org.rapidgraphql.schemabuilder.TypeUtils.baseType; -import static org.rapidgraphql.schemabuilder.TypeUtils.castToParameterizedType; -import static org.rapidgraphql.schemabuilder.TypeUtils.isListType; -import static org.rapidgraphql.schemabuilder.TypeUtils.isNotNullable; +import static org.rapidgraphql.schemabuilder.TypeUtils.*; import static org.slf4j.LoggerFactory.getLogger; public class DefinitionFactory { @@ -72,6 +38,7 @@ public class DefinitionFactory { private static final Logger LOGGER = getLogger(DefinitionFactory.class); public static final String QUERY_TYPE = "Query"; public static final String MUTATION_TYPE = "Mutation"; + public static final String SUBSCRIPTION_TYPE = "Subscription"; private static final List scalars = List.of( ExtendedScalars.GraphQLLong, ExtendedScalars.Date, @@ -132,10 +99,14 @@ public Definition createTypeDefinition(GraphQLResolver resolver) { String name; Class sourceType = null; Class resolverType = ClassUtils.getUserClass(resolver); + boolean isSubscription = false; if (resolver instanceof GraphQLQueryResolver) { name = QUERY_TYPE; } else if(resolver instanceof GraphQLMutationResolver) { name = MUTATION_TYPE; + } else if(resolver instanceof GraphQLSubscriptionResolver) { + name = SUBSCRIPTION_TYPE; + isSubscription = true; } else { Optional discoveredClass = extractResolverType(resolver); if (discoveredClass.isEmpty()) { @@ -148,8 +119,9 @@ public Definition createTypeDefinition(GraphQLResolver resolver) { LOGGER.info("Processing {} resolver: {}", name, resolverType.getName()); final Class finalSourceType = sourceType; + boolean finalIsSubscription = isSubscription; Method[] resolverDeclaredMethods = ReflectionUtils.getUniqueDeclaredMethods(resolverType, - method -> resolverMethodFilter(finalSourceType, method)); + method -> resolverMethodFilter(finalSourceType, method, finalIsSubscription)); List typeFields = Arrays.stream(resolverDeclaredMethods) .map(method -> createFieldDefinition(method, skipFirstParameter)) .collect(Collectors.toList()); @@ -348,7 +320,10 @@ private Type convertToInputGraphQLType(AnnotatedType annotatedType) { private Type convertToGraphQLType(AnnotatedType annotatedType, TypeKind typeKind) { Optional parameterizedType = castToParameterizedType(annotatedType); Type graphqlType; - if (parameterizedType.isPresent() && isListType(parameterizedType.get())) { + if (typeKind == TypeKind.OUTPUT_TYPE && parameterizedType.isPresent() && isPublisherType(parameterizedType.get())) { + AnnotatedType typeOfParameter = actualTypeArgument(parameterizedType.get(), 0); + graphqlType = convertToGraphQLType(typeOfParameter, typeKind); + } else if (parameterizedType.isPresent() && isListType(parameterizedType.get())) { AnnotatedType typeOfParameter = actualTypeArgument(parameterizedType.get(), 0); graphqlType = new ListType(convertToGraphQLType(typeOfParameter, typeKind)); } else { diff --git a/starter/src/main/java/org/rapidgraphql/schemabuilder/GraphQLSchemaResolver.java b/starter/src/main/java/org/rapidgraphql/schemabuilder/GraphQLSchemaResolver.java index 1d5ae71..73604f2 100644 --- a/starter/src/main/java/org/rapidgraphql/schemabuilder/GraphQLSchemaResolver.java +++ b/starter/src/main/java/org/rapidgraphql/schemabuilder/GraphQLSchemaResolver.java @@ -65,7 +65,9 @@ private List> processResolvers(List> definitions.addAll(definitionFactory.getScalars().stream() .map(scalar -> ScalarTypeDefinition.newScalarTypeDefinition().name(scalar.getName()).build()) .collect(Collectors.toList())); - definitions.addAll(resolvers.stream().map(resolver -> definitionFactory.createTypeDefinition(resolver)).collect(Collectors.toList())); + definitions.addAll(resolvers.stream() + .map(definitionFactory::createTypeDefinition) + .collect(Collectors.toList())); definitions.addAll(definitionFactory.processTypesQueue()); return definitions; } diff --git a/starter/src/main/java/org/rapidgraphql/schemabuilder/MethodsFilter.java b/starter/src/main/java/org/rapidgraphql/schemabuilder/MethodsFilter.java index a03863b..51c8f75 100644 --- a/starter/src/main/java/org/rapidgraphql/schemabuilder/MethodsFilter.java +++ b/starter/src/main/java/org/rapidgraphql/schemabuilder/MethodsFilter.java @@ -16,6 +16,7 @@ import java.util.regex.Pattern; import static java.lang.Character.isUpperCase; +import static org.rapidgraphql.schemabuilder.TypeUtils.isPublisherType; import static org.slf4j.LoggerFactory.getLogger; public class MethodsFilter { @@ -52,7 +53,7 @@ private static boolean dataLoaderMethodFilter(Method method) { return method.isAnnotationPresent(DataLoaderMethod.class); } - public static boolean resolverMethodFilter(Class sourceType, Method method) { + public static boolean resolverMethodFilter(Class sourceType, Method method, boolean isSubscription) { if (!typeMethodFilter(method)) { return false; } @@ -61,6 +62,14 @@ public static boolean resolverMethodFilter(Class sourceType, Method method) { method.getDeclaringClass().getName(), method.getName(), sourceType.getName()); return false; } + if (isSubscription) { + if ( !isPublisherType(method.getReturnType()) ) { + LOGGER.warn("Skipping method {}::{} in subscription resolver because it doesn't return publisher type", + method.getDeclaringClass().getName(), method.getName()); + return false; + } + } + return true; } diff --git a/starter/src/main/java/org/rapidgraphql/schemabuilder/RapidGraphQLContextBuilder.java b/starter/src/main/java/org/rapidgraphql/schemabuilder/RapidGraphQLContextBuilder.java index 9b3ef88..2cb4add 100644 --- a/starter/src/main/java/org/rapidgraphql/schemabuilder/RapidGraphQLContextBuilder.java +++ b/starter/src/main/java/org/rapidgraphql/schemabuilder/RapidGraphQLContextBuilder.java @@ -1,40 +1,44 @@ package org.rapidgraphql.schemabuilder; -import graphql.kickstart.execution.context.DefaultGraphQLContext; -import graphql.kickstart.execution.context.GraphQLContext; -import graphql.kickstart.servlet.context.DefaultGraphQLServletContext; -import graphql.kickstart.servlet.context.DefaultGraphQLWebSocketContext; +import graphql.kickstart.execution.context.DefaultGraphQLContextBuilder; +import graphql.kickstart.execution.context.GraphQLKickstartContext; import graphql.kickstart.servlet.context.GraphQLServletContextBuilder; +//import jakarta.servlet.http.HttpServletRequest; +//import jakarta.servlet.http.HttpServletResponse; +//import jakarta.websocket.Session; +//import jakarta.websocket.server.HandshakeRequest; import org.dataloader.DataLoaderRegistry; -import org.springframework.stereotype.Component; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.websocket.Session; import javax.websocket.server.HandshakeRequest; -public class RapidGraphQLContextBuilder implements GraphQLServletContextBuilder { +import java.util.HashMap; +import java.util.Map; + +public class RapidGraphQLContextBuilder extends DefaultGraphQLContextBuilder + implements GraphQLServletContextBuilder { private final DataLoaderRegistryFactory dataLoaderRegistryFactory; public RapidGraphQLContextBuilder(DataLoaderRegistryFactory dataLoaderRegistryFactory) { this.dataLoaderRegistryFactory = dataLoaderRegistryFactory; } - @Override - public GraphQLContext build(HttpServletRequest req, HttpServletResponse response) { - return DefaultGraphQLServletContext.createServletContext(buildDataLoaderRegistry(), null).with(req).with(response) - .build(); - } @Override - public GraphQLContext build() { - return new DefaultGraphQLContext(buildDataLoaderRegistry(), null); + public GraphQLKickstartContext build(HttpServletRequest request, HttpServletResponse response) { + Map map = new HashMap<>(); + map.put(HttpServletRequest.class, request); + map.put(HttpServletResponse.class, response); + return GraphQLKickstartContext.of(buildDataLoaderRegistry(), map); } @Override - public GraphQLContext build(Session session, HandshakeRequest request) { - return DefaultGraphQLWebSocketContext.createWebSocketContext(buildDataLoaderRegistry(), null).with(session) - .with(request).build(); + public GraphQLKickstartContext build(Session session, HandshakeRequest handshakeRequest) { + Map map = new HashMap<>(); + map.put(Session.class, session); + map.put(HandshakeRequest.class, handshakeRequest); + return GraphQLKickstartContext.of(buildDataLoaderRegistry(), map); } private DataLoaderRegistry buildDataLoaderRegistry() { diff --git a/starter/src/main/java/org/rapidgraphql/schemabuilder/TypeUtils.java b/starter/src/main/java/org/rapidgraphql/schemabuilder/TypeUtils.java index e3345cd..3d52c5a 100644 --- a/starter/src/main/java/org/rapidgraphql/schemabuilder/TypeUtils.java +++ b/starter/src/main/java/org/rapidgraphql/schemabuilder/TypeUtils.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; +import org.reactivestreams.Publisher; import java.lang.reflect.AnnotatedParameterizedType; import java.lang.reflect.AnnotatedType; @@ -60,7 +61,16 @@ public static boolean isListType(Class type) { return List.class.isAssignableFrom(type); } - public static Class baseType(AnnotatedParameterizedType annotatedParameterizedType) { + public static boolean isPublisherType(AnnotatedParameterizedType type) { + Class clazz = baseType(type); + return clazz.getTypeParameters().length==1 && Publisher.class.isAssignableFrom(clazz); + } + + public static boolean isPublisherType(Class type) { + return Publisher.class.isAssignableFrom(type); + } + + public static Class baseType(AnnotatedParameterizedType annotatedParameterizedType) { Type rawType = ((ParameterizedType) annotatedParameterizedType.getType()).getRawType(); if (!(rawType instanceof Class)) { throw new RuntimeException("Parameterized type " + rawType.getTypeName() + " can't be processed");