Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No ConnectionAdapter IllegalArgumentException for existing Java Connection type #709

Closed
blaenk opened this issue May 20, 2023 · 17 comments
Closed
Assignees
Labels
type: enhancement A general enhancement
Milestone

Comments

@blaenk
Copy link

blaenk commented May 20, 2023

I had been following the development of pagination support in #103.

In my setup, I already had types like PuzzleConnection. Upon updating to the release of spring-graphql that added pagination support, I am now getting exceptions like:

java.lang.IllegalArgumentException: No ConnectionAdapter for: foo.bar.models.puzzles.connections.PuzzleConnection

I looked through the reference and even a bit of the code (the starter/autoconfigure-er) but couldn't see anything about opting out either entirely or on a case-by-case basis via graphql annotations, or even implicitly opting out such as when it detects that the Edge and Connection types already exist (which might be something worthwhile to add?).

Should I just pin to the prior version until something is implemented? Do you recommend that I do something to prevent ConnectionTypeDefinitionConfigurer from being registered?

Thanks for all your work on spring-graphql!

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label May 20, 2023
@blaenk
Copy link
Author

blaenk commented May 21, 2023

If anyone is curious, for now, the interconnectedness of the spring ecosystem made it a bit tricky to only revert this library (should be doable but don't have the time for that), so to keep things simple I ended up pinning spring boot to 3.0.7 away from 3.1.0 (including the spring-boot-parent etc.).

@rstoyanchev
Copy link
Contributor

rstoyanchev commented May 21, 2023

Do you recommend that I do something to prevent ConnectionTypeDefinitionConfigurer from being registered?

The only thing that ConnectionTypeDefinitionConfigurer does is register Connection schema types, but only if they are missing, so that shouldn't cause an issue. The exception implies that the problem probably occurs later during request processing, but it's a bit difficult to say without a stacktrace.

Generally, the idea is it shouldn't be necessary to opt out explicitly. At runtime we also check if what the DataFetcher returned is already of type graphql.relay.Connection. My guess is that PuzzleConnection is not of that type. Could you provide more details such as a bit more of the stacktrace and also what PuzzleConnection looks like? We may be able to enhance detection of such a "Connection" type.

@rstoyanchev rstoyanchev added this to the 1.2.1 milestone May 21, 2023
@rstoyanchev rstoyanchev added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels May 21, 2023
@rstoyanchev rstoyanchev self-assigned this May 21, 2023
@rstoyanchev rstoyanchev changed the title Pagination support breaks existing code/schema No ConnectionAdapter IllegalArgumentException for existing Java Connection type May 21, 2023
@blaenk
Copy link
Author

blaenk commented May 21, 2023

Sure thing, sorry for the sparse details.

So to be clear, this is code/schema that predates pagination support, where *Connection may have nothing to do with what spring-graphql expects (for example, in my case it is not graphql.relay.Connection), and my point is that upgrading spring-graphql broke pre-existing working code.

Maybe it becomes a conscious decision to be more convention-over-configuration on this aspect, deciding to implicitly treat any type ending in Connection as being graphql.relay.Connection or otherwise generating types for it, in which case maybe we would benefit from some explicit opt-out support, since spring-graphql would now be completely taking over any type that ends in Connection, if I understand correctly, with either of these two behaviors.

Here is a portion of the schema related to PuzzleConnection, but I disagree that it should matter because again, this was pre-existing working code, so if it happened to me it probably will happen to others, and I don't think it's unrealistic to expect that because the connection concept in GraphQL is a common enough convention that people would have probably attempted to implement it on their own before spring-graphql got around to implementing support for it, possibly without the graphql.relay.Connection et al types, as in my case. Maybe I'm wrong, though. To repeat, these types do not implement graphql.relay.Connection or PageInfo or others, but it all worked fine before 1.2.0:

type PuzzleEdge {
  puzzle: Puzzle
  cursor: String!
}

type PuzzlePageInfo {
  total: Int!

  hasPreviousPage: Boolean!
  startCursor: String

  hasNextPage: Boolean!
  endCursor: String
}

type PuzzleConnection {
  pageInfo: PuzzlePageInfo!
  puzzles: [PuzzleEdge]
}

Here is one example stacktrace emitted during an integration test (so there is no sensitive data):

2023-05-21T07:52:54.107Z ERROR 2944 --- [           main] s.g.e.ExceptionResolversExceptionHandler : Unresolved IllegalArgumentException for executionId 43cbc385-2afd-a0ed-30c4-fd49c153ae3e

java.lang.IllegalArgumentException: No ConnectionAdapter for: jip.app.models.puzzles.connections.PuzzleConnection
	at org.springframework.util.Assert.notNull(Assert.java:204)
	at org.springframework.graphql.data.pagination.CompositeConnectionAdapter.getRequiredAdapter(CompositeConnectionAdapter.java:66)
	at org.springframework.graphql.data.pagination.CompositeConnectionAdapter.getContent(CompositeConnectionAdapter.java:49)
	at org.springframework.graphql.data.pagination.ConnectionFieldTypeVisitor$ConnectionDataFetcher.adapt(ConnectionFieldTypeVisitor.java:163)
	at org.springframework.graphql.data.pagination.ConnectionFieldTypeVisitor$ConnectionDataFetcher.get(ConnectionFieldTypeVisitor.java:152)
	at org.springframework.graphql.execution.ContextDataFetcherDecorator.lambda$get$0(ContextDataFetcherDecorator.java:88)
	at io.micrometer.context.ContextSnapshot.lambda$wrap$1(ContextSnapshot.java:92)
	at org.springframework.graphql.execution.ContextDataFetcherDecorator.get(ContextDataFetcherDecorator.java:88)
	at graphql.execution.ExecutionStrategy.invokeDataFetcher(ExecutionStrategy.java:309)
	at graphql.execution.ExecutionStrategy.fetchField(ExecutionStrategy.java:286)
	at graphql.execution.ExecutionStrategy.resolveFieldWithInfo(ExecutionStrategy.java:212)
	at graphql.execution.AsyncExecutionStrategy.execute(AsyncExecutionStrategy.java:55)
	at graphql.execution.Execution.executeOperation(Execution.java:161)
	at graphql.execution.Execution.execute(Execution.java:104)
	at graphql.GraphQL.execute(GraphQL.java:557)
	at graphql.GraphQL.lambda$parseValidateAndExecute$11(GraphQL.java:476)
	at java.base/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187)
	at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2341)
	at graphql.GraphQL.parseValidateAndExecute(GraphQL.java:471)
	at graphql.GraphQL.executeAsync(GraphQL.java:439)
	at org.springframework.graphql.execution.DefaultExecutionGraphQlService.lambda$execute$2(DefaultExecutionGraphQlService.java:82)
	at reactor.core.publisher.MonoDeferContextual.subscribe(MonoDeferContextual.java:47)
	at reactor.core.publisher.Mono.subscribe(Mono.java:4485)
	at reactor.core.publisher.Mono.subscribeWith(Mono.java:4551)
	at reactor.core.publisher.Mono.toFuture(Mono.java:5063)
	at org.springframework.core.ReactiveAdapterRegistry$ReactorRegistrar.lambda$registerAdapters$5(ReactiveAdapterRegistry.java:244)
	at org.springframework.core.ReactiveAdapter.fromPublisher(ReactiveAdapter.java:121)
	at org.springframework.web.servlet.function.DefaultAsyncServerResponse.create(DefaultAsyncServerResponse.java:186)
	at org.springframework.web.servlet.function.ServerResponse.async(ServerResponse.java:250)
	at org.springframework.graphql.server.webmvc.GraphQlHttpHandler.handleRequest(GraphQlHttpHandler.java:111)
	at org.springframework.web.servlet.function.support.HandlerFunctionAdapter.handle(HandlerFunctionAdapter.java:107)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
	at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:72)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
	at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:165)
	at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:132)
	at org.springframework.security.web.FilterChainProxy.lambda$doFilterInternal$3(FilterChainProxy.java:231)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:365)
	at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:100)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126)
	at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter.doFilterInternal(OAuth2AuthorizationCodeGrantFilter.java:183)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter.doFilterInternal(BearerTokenAuthenticationFilter.java:128)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter.doFilterInternal(DefaultLogoutPageGeneratingFilter.java:58)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter.doFilter(DefaultLoginPageGeneratingFilter.java:188)
	at org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter.doFilter(DefaultLoginPageGeneratingFilter.java:174)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227)
	at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227)
	at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.doFilterInternal(OAuth2AuthorizationRequestRedirectFilter.java:181)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.doFilterInternal(OAuth2AuthorizationRequestRedirectFilter.java:181)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107)
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:131)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
	at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82)
	at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233)
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352)
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268)
	at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:132)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:132)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:132)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:132)
	at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:201)
	at org.springframework.test.web.servlet.client.MockMvcHttpConnector.connect(MockMvcHttpConnector.java:97)
	at org.springframework.test.web.reactive.server.WiretapConnector.connect(WiretapConnector.java:71)
	at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.exchange(ExchangeFunctions.java:102)
	at org.springframework.test.web.reactive.server.DefaultWebTestClient$DefaultRequestBodyUriSpec.exchange(DefaultWebTestClient.java:361)
	at org.springframework.graphql.test.tester.WebTestClientTransport.execute(WebTestClientTransport.java:61)
	at org.springframework.graphql.test.tester.DefaultGraphQlTester$DefaultRequest.execute(DefaultGraphQlTester.java:156)
	at jip.app.api.graphql.PuzzlesControllerGraphQLIT.puzzles(PuzzlesControllerGraphQLIT.java:164)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:578)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:50)
	at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184)
	at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148)
	at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122)
	at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385)
	at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162)
	at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507)
	at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495)

To conclude, I am interested in seeing how I can adapt to using spring-graphql's own support for pagination/connections. It seems the way to go would be to implement graphql.relay.Connection and PageInfo and other interfaces. Unfortunately, until then, the fact remains that my pre-existing code was broken by a point-release, and others may run into this as well.

No stress or pressure on this by the way! I am truly grateful for all of the work you all put into this project, and my use of it is for a side-project anyway.

My only aim with this ticket was (1) to make you all aware of this situation in case you weren't already and (2) to learn from you all what you believe to be the best course of action, whether on my end and/or on changes necessary in the project itself (e.g. opt-out annotations or more intelligent implicit behavior).

Thanks again!

@rstoyanchev
Copy link
Contributor

rstoyanchev commented May 22, 2023

@blaenk, thanks for the extra details. The issue is scheduled for 1.2.1, and goal is to find a fix.

To conclude, I am interested in seeing how I can adapt to using spring-graphql's own support for pagination/connections. It seems the way to go would be to implement graphql.relay.Connection and PageInfo and other interfaces.

I have some suggestions, but let's make the existing code work first.

rstoyanchev added a commit that referenced this issue May 22, 2023
ConnectionFieldTypeVisitor now checks the complete structure
of Connection, Edge, and PageInfo schema types before
decorating a DataFetcher.

See gh-709
rstoyanchev added a commit that referenced this issue May 22, 2023
ConnectionFieldTypeVisitor now also checks if the container type
ends on "Connection", and if so it lets it pass through.

See gh-709
@rstoyanchev
Copy link
Contributor

rstoyanchev commented May 22, 2023

I've made a couple of updates, 1) to check the complete structure of a Connection schema type before deciding to apply a DataFetcher decorator to adapt its return values to graphql.relay.Connection, and 2) at runtime to ignore return values with a name that ends on Connection. This double protection I expect will go pretty far. If you could give those a try with 1.2.1-SNAPSHOT and confirm, that would be much appreciated.

maybe we would benefit from some explicit opt-out support

It could be opt-in or opt-out, but it makes sense to strengthen checks first, and exhaust our options. In the end, my sense is that we will be able to do without that.

@blaenk
Copy link
Author

blaenk commented May 22, 2023

Sure I'm happy to help try it out!

I can confirm that the issue appears to be resolved with everything else on 3.1.0, and an explicit dependency of:

<dependency>
    <groupId>org.springframework.graphql</groupId>
    <artifactId>spring-graphql</artifactId>
    <version>1.2.1-SNAPSHOT</version>
</dependency>

I appreciate your prompt response! Let me know if there's anything else you need.

As for adapting my code, it seems like I would need to implement graphql.relay.Connection, graphql.relay.PageInfo, graphql.relay.ConnectionCursor, and graphql.relay.Edge.

Once I do that, then spring-graphql will detect that it is of type graphql.relay.Connection and behave differently how, again? In other words, what would be the benefit to me implementing those interfaces for my types vs not doing so in this fixed version?

@rstoyanchev
Copy link
Contributor

rstoyanchev commented May 22, 2023

Thanks for confirming the fix!

As for adapting my code, it seems like I would need to implement graphql.relay.Connection, graphql.relay.PageInfo, graphql.relay.ConnectionCursor, and graphql.relay.Edge.

The idea with our pagination support is that you don't have to implement these types. Instead, you would return the actual result items however they may be represented in your underlying service/data layer, and then configure a ConnectionAdapter along with a CursorStrategy to help us turn that transparently to graphql.relay.Connection.

For example if using Spring Data repositories, you would naturally have Page or Window from paginated requests, and we support returning those directly from the controller method. They are then transparently adapted to graphql.relay.Connection with our built-in support, but you can also write a ConnectionAdapter for any other paginated type, essentially some kind of simple container of a List of items along with boolean flags about hasNext and hasPrevious. The benefit is to use simpler (or any) pagination type, and use ConnectionAdapter to produce the expected, complete paginated response.

This does mean that you would need to change your schema to match the GraphQL Cursor Connection spec, which might not be an option, and this is why we make sure your exiting pagination can continue to work as it did before.

This is just to give you an idea, but have a look around the documentation for more details.

@blaenk
Copy link
Author

blaenk commented May 22, 2023

Thanks again @rstoyanchev. No rush or anything but any idea more or less when to expect 1.2.1 to be released?

@koenpunt
Copy link
Contributor

@rstoyanchev if I understand correctly, the fix planned for 1.2.1 only applies to connection types that do not follow the spec structure. In our application we're following the spec structure, but we don't want to update all our connection implementations all at once.
Is there a way to disable or bypass the adapter functionality, so we can adopt it gradually?

@rstoyanchev
Copy link
Contributor

@koenpunt, it should work if your connection implementation is based on graphql.relay.Connection, or even if the class name ends on Connection. We pass those through. Give it a try in a small sample maybe or provide some more detail about your connection implementations.

@rstoyanchev
Copy link
Contributor

rstoyanchev commented May 23, 2023

@blaenk I've set the release date for 1.2.1 for June 20. We could decide to release sooner depending on the issues that come in, but it's good to allow some additional time for feedback, and would be timed with the Boot 3.1.1 release on June 22.

In the mean time, looking at the Boot auto-configuration that installs the ConnectionAdapter, it's triggered by the presence of ScrollPosition from spring-data-commons. That's probably on the classpath to get this issue. You could define your own EncodingCursorStrategy<?> cursorStrategy() bean with a custom, no-op CursorStrategy (as long as it doesn't support ScrollPosition).

@koenpunt
Copy link
Contributor

Our connections currently aren't based on the graphql.relay.Connection interface, and we prefer not to change anything to our implementation right now, just because we're updating to a new Spring-Graphql version.

I'm not sure what you mean with "it should work", because we don't have an ConnectionAdapter configured, and don't want to configure one at this point, but it still tries to find a connection adapter for our existing connections.

I believe that if this part of the autoconfiguration would be excluded the error would disappear, but haven't tried that yet;

https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java#L168-L192

@koenpunt
Copy link
Contributor

I have to add that I haven't tried the snapshot yet, so will do that first and report back.

@rstoyanchev
Copy link
Contributor

rstoyanchev commented May 23, 2023

Our connections currently aren't based on the graphql.relay.Connection interface, and we prefer not to change anything to our implementation right now, just because we're updating to a new Spring-Graphql version.

There is no suggestion that you should have to change anything.

I'm not sure what you mean with "it should work", because we don't have an ConnectionAdapter configured

You don't need to have a ConnectionAdapter. The Boot auto-configuration you referenced adds one for Spring Data's Window and Page types. However, if that's not what a controller method returns at runtime, we try to check if it is already a "connection" object that doesn't need adapting. Those checks were enhanced as part of this issue, and there are further improvements we can make in this space.

Please give it a try, and create a new issue if needed.

@koenpunt
Copy link
Contributor

The snapshot seems to work without changes to our current connection implementations 👍

@mzvankovich
Copy link

mzvankovich commented Dec 13, 2023

Thanks for confirming the fix!

As for adapting my code, it seems like I would need to implement graphql.relay.Connection, graphql.relay.PageInfo, graphql.relay.ConnectionCursor, and graphql.relay.Edge.

The idea with our pagination support is that you don't have to implement these types. Instead, you would return the actual result items however they may be represented in your underlying service/data layer, and then configure a ConnectionAdapter along with a CursorStrategy to help us turn that transparently to graphql.relay.Connection.

For example if using Spring Data repositories, you would naturally have Page or Window from paginated requests, and we support returning those directly from the controller method. They are then transparently adapted to graphql.relay.Connection with our built-in support, but you can also write a ConnectionAdapter for any other paginated type, essentially some kind of simple container of a List of items along with boolean flags about hasNext and hasPrevious. The benefit is to use simpler (or any) pagination type, and use ConnectionAdapter to produce the expected, complete paginated response.

This does mean that you would need to change your schema to match the GraphQL Cursor Connection spec, which might not be an option, and this is why we make sure your exiting pagination can continue to work as it did before.

This is just to give you an idea, but have a look around the documentation for more details.

@rstoyanchev thank you for the clarification. I'm just curious if it also works for reactive annotated controllers? For example:

@Controller
public class BookController {

	@QueryMapping
	public Mono<Window<Book>> books(ScrollSubrange subrange) {
		ScrollPosition position = subrange.position().orElse(ScrollPosition.offset());
		int count = subrange.count().orElse(20);
		// ...
	}

}

Thanks!

@rstoyanchev
Copy link
Contributor

@mzvankovich yes that is supported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

5 participants