diff --git a/dgs-codegen/src/main/java/com/example/demo/gql/directives/UppercaseDirectiveWiring.java b/dgs-codegen/src/main/java/com/example/demo/gql/directives/UppercaseDirectiveWiring.java index a648843c8..b0e51b065 100644 --- a/dgs-codegen/src/main/java/com/example/demo/gql/directives/UppercaseDirectiveWiring.java +++ b/dgs-codegen/src/main/java/com/example/demo/gql/directives/UppercaseDirectiveWiring.java @@ -2,11 +2,11 @@ import com.netflix.graphql.dgs.DgsDirective; import graphql.schema.DataFetcherFactories; +import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.idl.SchemaDirectiveWiring; import graphql.schema.idl.SchemaDirectiveWiringEnvironment; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; @DgsDirective(name = "uppercase") @Slf4j @@ -16,8 +16,8 @@ public GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment { @@ -29,7 +29,7 @@ public GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment) { } @Component -class DataInitializer(val posts: PostRepository, val comments: CommentRepository) : ApplicationRunner { - private val log = LoggerFactory.getLogger(DataInitializer::class.java) - override fun run(args: ApplicationArguments?) { +class DataInitializer(val posts: PostRepository, val comments: CommentRepository) { + + companion object { + private val log = LoggerFactory.getLogger(DataInitializer::class.java) + } + + @EventListener(ApplicationReadyEvent::class) + suspend fun init() { val data = listOf( PostEntity(title = "Learn Spring", content = "content of Learn Spring"), PostEntity(title = "Learn Dgs framework", content = "content of Learn Dgs framework") ) - runBlocking { - comments.deleteAll() - posts.deleteAll() - val saved = posts.saveAll(data).toList() - saved.forEach { log.debug("saved: {}", it) } - } + comments.deleteAll() + posts.deleteAll() + + val saved = posts.saveAll(data).toList() + saved.forEach { log.debug("saved: {}", it) } } } @@ -51,7 +54,7 @@ class SecurityConfig { fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { return http .csrf { it.disable() } - .httpBasic{} + .httpBasic {} .securityMatcher(PathPatternParserServerWebExchangeMatcher("/graphql")) .build() } diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/ExceptionHandlers.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/ExceptionHandlers.kt index 0c0b42ea5..ec238b65b 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/ExceptionHandlers.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/ExceptionHandlers.kt @@ -18,7 +18,7 @@ class ExceptionHandlers : DataFetcherExceptionHandler { when (val exception = handlerParameters.exception) { is PostNotFoundException, is AuthorNotFoundException -> { val graphqlError = TypedGraphQLError.newNotFoundBuilder() - .message(exception.message) + .message(exception.message?: "Not Found") .path(handlerParameters.path) .build(); CompletableFuture.completedFuture( diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/AuthorsDataFetcher.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/AuthorsDataFetcher.kt index ce3c0804b..2da4ea811 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/AuthorsDataFetcher.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/AuthorsDataFetcher.kt @@ -19,7 +19,7 @@ class AuthorsDataFetcher( @DgsData(parentType = DgsConstants.AUTHOR.TYPE_NAME, field = DgsConstants.AUTHOR.Posts) suspend fun posts(dfe: DgsDataFetchingEnvironment): List { - val a: Author = dfe.getSource() + val a: Author = dfe.getSource()!! return postService.getPostsByAuthorId(a.id).toList() } } \ No newline at end of file diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/PostsDataFetcher.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/PostsDataFetcher.kt index 3ddfffae1..6a94efb5e 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/PostsDataFetcher.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/datafetcher/PostsDataFetcher.kt @@ -23,13 +23,13 @@ class PostsDataFetcher(val postService: PostService) { fun author(dfe: DgsDataFetchingEnvironment): CompletableFuture { val dataLoader = dfe.getDataLoader("authorsLoader") val post = dfe.getSource() - return dataLoader.load(post.authorId) + return dataLoader!!.load(post!!.authorId) } @DgsData(parentType = DgsConstants.POST.TYPE_NAME, field = DgsConstants.POST.Comments) fun comments(dfe: DgsDataFetchingEnvironment): CompletableFuture> { val dataLoader = dfe.getDataLoader>(CommentsDataLoader::class.java) - val (id) = dfe.getSource() + val (id) = dfe.getSource()!! return dataLoader.load(id) } diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/DemoApplication.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/DemoApplication.kt index 9739d31ac..2624668bb 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/DemoApplication.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/DemoApplication.kt @@ -192,7 +192,7 @@ val beans = beans { if (!passwordEncoder.matches(password, user.password)) { throw BadCredentialsException("username or password was not matched.") } - if (!user.isEnabled()) { + if (!user.isEnabled) { throw DisabledException("user is not enabled.") } AuthenticationTokenWithId(user.id, username, user.authorities) diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/ExceptionHandlers.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/ExceptionHandlers.kt index 4b5f48ffc..5ea112f95 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/ExceptionHandlers.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/ExceptionHandlers.kt @@ -19,7 +19,7 @@ class ExceptionHandlers : DataFetcherExceptionHandler { return when (val exception = handlerParameters.exception) { is PostNotFoundException, is AuthorNotFoundException -> { val graphqlError = TypedGraphQLError.newNotFoundBuilder() - .message(exception.message) + .message(exception.message?: "Not found") .path(handlerParameters.path) .build(); val result = DataFetcherExceptionHandlerResult.newResult() diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/datafetchers/AuthDataFetcher.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/datafetchers/AuthDataFetcher.kt index 75a3d70b9..5f2f0bdcc 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/datafetchers/AuthDataFetcher.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/datafetchers/AuthDataFetcher.kt @@ -20,7 +20,7 @@ class AuthDataFetcher( @DgsMutation fun signIn(@InputArgument credentials: Credentials, dfe: DgsDataFetchingEnvironment): Map { - var auth = authenticationManager.authenticate( + val auth = authenticationManager.authenticate( UsernamePasswordAuthenticationToken( credentials.username, credentials.password diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/datafetchers/AuthorsDataFetcher.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/datafetchers/AuthorsDataFetcher.kt index 2a08a14bb..f6de7f3aa 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/datafetchers/AuthorsDataFetcher.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/datafetchers/AuthorsDataFetcher.kt @@ -2,6 +2,7 @@ package com.example.demo.gql.datafetchers import com.example.demo.gql.DgsConstants import com.example.demo.gql.types.* +import com.example.demo.service.AuthorNotFoundException import com.example.demo.service.AuthorService import com.example.demo.service.PostService import com.netflix.graphql.dgs.* @@ -19,7 +20,7 @@ class AuthorsDataFetcher( @DgsData(parentType = DgsConstants.AUTHOR.TYPE_NAME, field = DgsConstants.AUTHOR.Posts) fun posts(dfe: DgsDataFetchingEnvironment): List { - val a: Author = dfe.getSource() + val a: Author = dfe.getSource()!! return postService.getPostsByAuthorId(a.id) } @@ -30,7 +31,7 @@ class AuthorsDataFetcher( @DgsData(parentType = DgsConstants.AUTHOR.TYPE_NAME, field = DgsConstants.AUTHOR.Profile) fun profile(dfe: DgsDataFetchingEnvironment): Profile? { - val a: Author = dfe.getSource() + val a: Author = dfe.getSource()!! return authorService.getProfileByUserId(a.id) } } diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/datafetchers/PostsDataFetcher.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/datafetchers/PostsDataFetcher.kt index 8ee7237b9..ebb7afaee 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/datafetchers/PostsDataFetcher.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/datafetchers/PostsDataFetcher.kt @@ -21,8 +21,8 @@ class PostsDataFetcher(val postService: PostService) { @DgsData(parentType = DgsConstants.POST.TYPE_NAME, field = DgsConstants.POST.Author) fun author(dfe: DgsDataFetchingEnvironment): CompletableFuture { val dataLoader = dfe.getDataLoader("authorsLoader") - val post = dfe.getSource() - return dataLoader.load(post.authorId) + val post = dfe.getSource()!! + return dataLoader!!.load(post.authorId) } @DgsData(parentType = DgsConstants.POST.TYPE_NAME, field = DgsConstants.POST.Comments) @@ -30,7 +30,7 @@ class PostsDataFetcher(val postService: PostService) { val dataLoader = dfe.getDataLoader>( CommentsDataLoader::class.java ) - val (id) = dfe.getSource() + val (id) = dfe.getSource()!! return dataLoader.load(id) } diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/dataloaders/AuthorsDataLoader.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/dataloaders/AuthorsDataLoader.kt index f6d3b5ec5..e261d191d 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/dataloaders/AuthorsDataLoader.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/dataloaders/AuthorsDataLoader.kt @@ -4,13 +4,14 @@ import com.example.demo.gql.types.Author import com.example.demo.service.AuthorService import com.netflix.graphql.dgs.DgsDataLoader import org.dataloader.BatchLoader +import java.util.concurrent.CompletableFuture.completedFuture import java.util.concurrent.CompletableFuture.supplyAsync import java.util.concurrent.CompletionStage @DgsDataLoader(name = "authorsLoader") class AuthorsDataLoader(val authorService: AuthorService) : BatchLoader { - override fun load(keys: List): CompletionStage> = supplyAsync { + override fun load(keys: List): CompletionStage> = completedFuture( authorService.getAuthorByIdIn(keys) - } + ) } diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/dataloaders/CommentsDataLoader.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/dataloaders/CommentsDataLoader.kt index f2e0aea21..81bbb85da 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/dataloaders/CommentsDataLoader.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/dataloaders/CommentsDataLoader.kt @@ -19,7 +19,7 @@ class CommentsDataLoader(val postService: PostService) : MappedBatchLoader postId == it } } log.info("mapped comments: {}", mappedComments) - return CompletableFuture.supplyAsync { mappedComments } + return CompletableFuture.completedFuture(mappedComments) } companion object { diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/scalars/LocalDateTimeScalar.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/scalars/LocalDateTimeScalar.kt index 98adea02e..090bca208 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/scalars/LocalDateTimeScalar.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/scalars/LocalDateTimeScalar.kt @@ -1,43 +1,47 @@ package com.example.demo.gql.scalars import com.netflix.graphql.dgs.DgsScalar +import graphql.GraphQLContext +import graphql.execution.CoercedVariables import graphql.language.StringValue +import graphql.language.Value import graphql.schema.Coercing import graphql.schema.CoercingParseLiteralException import graphql.schema.CoercingParseValueException import graphql.schema.CoercingSerializeException import java.time.LocalDateTime import java.time.format.DateTimeFormatter - - -//@DgsComponent -//class DateTimeScalar { -// @DgsRuntimeWiring -// fun addScalar(builder: RuntimeWiring.Builder): RuntimeWiring.Builder { -// return builder.scalar(ExtendedScalars.DateTime) -// } -//} +import java.util.* @DgsScalar(name = "LocalDateTime") class LocalDateTimeScalar : Coercing { - @Throws(CoercingSerializeException::class) - override fun serialize(dataFetcherResult: Any): String? { + override fun serialize(dataFetcherResult: Any, graphQLContext: GraphQLContext, locale: Locale): String? { return when (dataFetcherResult) { is LocalDateTime -> dataFetcherResult.format(DateTimeFormatter.ISO_DATE_TIME) else -> throw CoercingSerializeException("Not a valid DateTime") } } - @Throws(CoercingParseValueException::class) - override fun parseValue(input: Any): LocalDateTime { + override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): LocalDateTime? { return LocalDateTime.parse(input.toString(), DateTimeFormatter.ISO_DATE_TIME) } - @Throws(CoercingParseLiteralException::class) - override fun parseLiteral(input: Any): LocalDateTime { + override fun parseLiteral( + input: Value<*>, + variables: CoercedVariables, + graphQLContext: GraphQLContext, + locale: Locale + ): LocalDateTime? { when (input) { is StringValue -> return LocalDateTime.parse(input.value, DateTimeFormatter.ISO_DATE_TIME) else -> throw CoercingParseLiteralException("Value is not a valid ISO date time") } } + + override fun valueToLiteral(input: Any, graphQLContext: GraphQLContext, locale: Locale): Value<*> { + return when (input) { + is String -> StringValue.newStringValue(input).build() + else -> throw CoercingParseValueException("Value is not a string") + } + } } diff --git a/dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt b/dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt index 2ddd1d8ad..6080f0808 100644 --- a/dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt +++ b/dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt @@ -1,23 +1,22 @@ package com.example.demo import com.jayway.jsonpath.TypeRef -import com.netflix.graphql.dgs.DgsQueryExecutor +import com.netflix.graphql.dgs.client.WebClientGraphQLClient import com.netflix.graphql.dgs.client.WebSocketGraphQLClient import org.assertj.core.api.Assertions.assertThat +import org.intellij.lang.annotations.Language 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.boot.test.web.server.LocalServerPort +import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient import reactor.test.StepVerifier +import java.time.Duration @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class DemoApplicationTestsWithGraphQLClient { - - @Autowired - lateinit var dgsQueryExecutor: DgsQueryExecutor - + lateinit var webClientGraphQLClient: WebClientGraphQLClient lateinit var socketGraphQLClient: WebSocketGraphQLClient @LocalServerPort @@ -25,52 +24,72 @@ class DemoApplicationTestsWithGraphQLClient { @BeforeEach fun setup() { - this.socketGraphQLClient = WebSocketGraphQLClient("ws://localhost:$port/subscriptions", ReactorNettyWebSocketClient()) + this.webClientGraphQLClient = WebClientGraphQLClient(WebClient.create("http://localhost:$port/graphql")) + this.socketGraphQLClient = + WebSocketGraphQLClient("ws://localhost:$port/subscriptions", ReactorNettyWebSocketClient()) } @Test fun testMessages() { - //Hooks.onOperatorDebug(); - val query = "subscription { messageSent { body } }" + @Language("graphql") val messageSentSubscriptionQuery = """ + subscription { + messageSent { + body + } + } + """.trimIndent() val variables = emptyMap() - val executionResult = socketGraphQLClient.reactiveExecuteQuery(query, variables) + val executionResult = socketGraphQLClient.reactiveExecuteQuery(messageSentSubscriptionQuery, variables) .map { it.extractValueAsObject( "data.messageSent", object : TypeRef>() {} )["body"] as String } + + val message1 = "text1" + val message2 = "text2" val verifier = StepVerifier.create(executionResult) - .consumeNextWith { assertThat(it).isEqualTo("text1 message") } - // .consumeNextWith { assertThat(it).isEqualTo("text2 message") } + .thenAwait(Duration.ofMillis(1000)) // see: https://github.com/Netflix/dgs-framework/issues/657 + .consumeNextWith { assertThat(it).isEqualTo(message1) } + .consumeNextWith { assertThat(it).isEqualTo(message2) } .thenCancel() .verifyLater() - val sendText1 = dgsQueryExecutor.executeAndExtractJsonPath( - "mutation sendMessage(\$msg: TextMessageInput!) { send(message:\$msg) { body}}", - "data.send.body", - mapOf("msg" to (mapOf("body" to "text1 message"))) - ) - assertThat(sendText1).contains("text1"); + @Language("graphql") val sendMessageQuery = """ + mutation sendMessage(${'$'}msg: TextMessageInput!) { + send(message:${'$'}msg) { + body + } + } + """.trimIndent() + webClientGraphQLClient.reactiveExecuteQuery(sendMessageQuery, mapOf("msg" to (mapOf("body" to message1)))) + .map { it.extractValueAsObject("data.send.body", String::class.java) } + .`as` { StepVerifier.create(it) } + .consumeNextWith { assertThat(it).isEqualTo(message1) } + .verifyComplete() -// val sendText2 = dgsQueryExecutor.executeAndExtractJsonPath( -// "mutation sendMessage(\$msg: TextMessageInput!) { send(message:\$msg) { body}}", -// "data.send.body", -// mapOf("msg" to (mapOf("body" to "text2 message"))) -// ) -// assertThat(sendText2).contains("text2"); + webClientGraphQLClient.reactiveExecuteQuery(sendMessageQuery, mapOf("msg" to (mapOf("body" to message2)))) + .map { it.extractValueAsObject("data.send.body", String::class.java) } + .`as` { StepVerifier.create(it) } + .consumeNextWith { assertThat(it).isEqualTo(message2) } + .verifyComplete() //verify it now. verifier.verify() - val msgs = dgsQueryExecutor.executeAndExtractJsonPath>( - " { messages { body }}", - "data.messages[*].body" - ) - assertThat(msgs).allMatch { s: String -> - s.contains( - "message" - ) - } + @Language("graphql") val allMessagesQuery = """ + query allMessages { + messages { + body + } + } + """.trimIndent() + webClientGraphQLClient.reactiveExecuteQuery(allMessagesQuery) + .map { it.extractValueAsObject("data.messages[*].body", object : TypeRef>() {}) } + .`as` { StepVerifier.create(it) } + .consumeNextWith { assertThat(it).isEqualTo(message1) } + .consumeNextWith { assertThat(it).isEqualTo(message2) } + .verifyComplete() } } diff --git a/dgs-webflux/src/test/java/com/example/demo/MutationTests.java b/dgs-webflux/src/test/java/com/example/demo/MutationTests.java index 47ae6a263..9d39fd373 100644 --- a/dgs-webflux/src/test/java/com/example/demo/MutationTests.java +++ b/dgs-webflux/src/test/java/com/example/demo/MutationTests.java @@ -15,9 +15,9 @@ import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -45,10 +45,10 @@ static class MutationTestsConfig { @Autowired DgsReactiveQueryExecutor dgsQueryExecutor; - @MockBean + @MockitoBean PostService postService; - @MockBean + @MockitoBean AuthorService authorService; @Test diff --git a/dgs-webflux/src/test/java/com/example/demo/QueryTests.java b/dgs-webflux/src/test/java/com/example/demo/QueryTests.java index 993585e69..c31c75da1 100644 --- a/dgs-webflux/src/test/java/com/example/demo/QueryTests.java +++ b/dgs-webflux/src/test/java/com/example/demo/QueryTests.java @@ -18,6 +18,7 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -35,10 +36,10 @@ class QueryTests { @Autowired DgsReactiveQueryExecutor dgsQueryExecutor; - @MockBean + @MockitoBean PostService postService; - @MockBean + @MockitoBean AuthorService authorService; @Configuration diff --git a/dgs-webflux/src/test/java/com/example/demo/SubscriptionTests.java b/dgs-webflux/src/test/java/com/example/demo/SubscriptionTests.java index fb2e12d15..73396fdd7 100644 --- a/dgs-webflux/src/test/java/com/example/demo/SubscriptionTests.java +++ b/dgs-webflux/src/test/java/com/example/demo/SubscriptionTests.java @@ -25,10 +25,10 @@ import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import reactor.core.publisher.Mono; import java.util.Collections; @@ -65,19 +65,19 @@ static class SubscriptionTestsConfig { @Autowired ObjectMapper objectMapper; - @SpyBean + @MockitoSpyBean PostService postService; - @MockBean + @MockitoBean PostRepository postRepository; - @MockBean + @MockitoBean CommentRepository commentRepository; - @MockBean + @MockitoBean AuthorRepository authorRepository; - @MockBean + @MockitoBean AuthorService authorService; @SneakyThrows @@ -98,7 +98,15 @@ void createCommentAndSubscription() { // commentAdded producer var comments = new CopyOnWriteArrayList(); - @Language("GraphQL") var subscriptionQuery = "subscription onCommentAdded { commentAdded { id postId content } }"; + @Language("GraphQL") var subscriptionQuery = """ + subscription onCommentAdded { + commentAdded { + id + postId + content + } + } + """.stripIndent(); // var executionResult = dgsReactiveQueryExecutor.execute(subscriptionQuery, Collections.emptyMap()).block(); // var publisher = executionResult.>getData(); // publisher.subscribe(new Subscriber() { @@ -130,7 +138,7 @@ void createCommentAndSubscription() { // }); // var executionResultMono = dgsReactiveQueryExecutor.execute(subscriptionQuery, Collections.emptyMap()); - var publisher = executionResultMono.flatMapMany(result -> result.>getData()); + var publisher = executionResultMono.flatMapMany(ExecutionResult::>getData); publisher.subscribe(executionResult -> { log.debug("execution result in publisher: {}", executionResult); var commentAdded = objectMapper.convertValue( diff --git a/dgs-webflux/src/test/java/com/example/demo/SubscriptionTestsWithGraphQLClient.java b/dgs-webflux/src/test/java/com/example/demo/SubscriptionTestsWithGraphQLClient.java index e4c46c27e..cdeb51522 100644 --- a/dgs-webflux/src/test/java/com/example/demo/SubscriptionTestsWithGraphQLClient.java +++ b/dgs-webflux/src/test/java/com/example/demo/SubscriptionTestsWithGraphQLClient.java @@ -6,6 +6,7 @@ import com.netflix.graphql.dgs.client.WebSocketGraphQLClient; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @@ -14,9 +15,11 @@ import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; import reactor.test.StepVerifier; +import java.time.Duration; import java.util.Collections; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicLong; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; @@ -45,7 +48,14 @@ public void setup() { @SneakyThrows @Test void createCommentAndSubscription() { - var createPostQuery = "mutation createPost($input: CreatePostInput!){ createPost(createPostInput:$input) {id, title} }"; + @Language("graphql") var createPostQuery = """ + mutation createPost($input: CreatePostInput!){ + createPost(createPostInput:$input) { + id + title + } + } + """.stripIndent(); var createPostVariables = Map.of( "input", Map.of( "title", "test title", @@ -54,79 +64,86 @@ void createCommentAndSubscription() { ); var countDownLatch = new CountDownLatch(1); - var postIdHolder = new PostIdHolder(); + var postIdHolder = new AtomicLong(); var createPostResult = this.client.reactiveExecuteQuery(createPostQuery, createPostVariables) .map(response -> response.extractValueAsObject("createPost", Post.class)) .map(Post::getId) //.doOnTerminate(countDownLatch::countDown) .subscribe(id -> { log.debug("post created, id: {}", id); - postIdHolder.setPostId(id); + postIdHolder.set(id); countDownLatch.countDown(); }); countDownLatch.await(5, SECONDS); log.debug("created post:{}", createPostResult); - Long postId = postIdHolder.getPostId(); + Long postId = postIdHolder.get(); log.debug("post id get from amotic long: {}", postId); assertThat(postId).isNotNull(); - String subscriptionQuery = "subscription onCommentAdded { commentAdded { id postId content } }"; - var executionResultMono = this.socketClient + @Language("graphql") var subscriptionQuery = """ + subscription onCommentAdded { + commentAdded { + id + postId + content + } + } + """.stripIndent(); + var executionResultPublisher = this.socketClient .reactiveExecuteQuery(subscriptionQuery, Collections.emptyMap()); - var publisher = executionResultMono + var commentAddedDataPublisher = executionResultPublisher .map(it -> it.extractValueAsObject("commentAdded", Comment.class)); - var verifier = StepVerifier.create(publisher) - .expectNextCount(1) + + // add two comments + String comment1 = "test comment"; + String comment2 = "test comment2"; + var verifier = StepVerifier.create(commentAddedDataPublisher) + .thenAwait(Duration.ofMillis(1000)) // see: https://github.com/Netflix/dgs-framework/issues/657 + .consumeNextWith(comment -> assertThat(comment.getContent()).isEqualTo(comment1)) + .consumeNextWith(comment -> assertThat(comment.getContent()).isEqualTo(comment2)) .thenCancel() .verifyLater(); // add comment - var addCommentQuery = "mutation addComment($input: CommentInput!) { addComment(commentInput:$input) { id postId content}}"; + @Language("graphql") var addCommentQuery = """ + mutation addComment($input: CommentInput!) { + addComment(commentInput:$input) { + id + postId + content + } + } + """.stripIndent(); + var addCommentVariables = Map.of( "input", Map.of( "postId", postId, - "content", "test comment" + "content", comment1 ) ); var addCommentVariables2 = Map.of( "input", Map.of( "postId", postId, - "content", "test comment2" + "content", comment2 ) ); this.client.reactiveExecuteQuery(addCommentQuery, addCommentVariables) .map(response -> response.extractValueAsObject("addComment", Comment.class)) .as(StepVerifier::create) - .consumeNextWith(comment -> assertThat(comment.getContent()).isEqualTo("test comment")) + .consumeNextWith(comment -> assertThat(comment.getContent()).isEqualTo(comment1)) .verifyComplete(); this.client.reactiveExecuteQuery(addCommentQuery, addCommentVariables2) .map(response -> response.extractValueAsObject("addComment", Comment.class)) .as(StepVerifier::create) - .consumeNextWith(comment -> assertThat(comment.getContent()).isEqualTo("test comment2")) + .consumeNextWith(comment -> assertThat(comment.getContent()).isEqualTo(comment2)) .verifyComplete(); // verify - await() - .atMost(5, SECONDS) - .untilAsserted( - () -> verifier.verify() - ); - + // await().atMost(5, SECONDS).untilAsserted(verifier::verify); + verifier.verify(); } } - -class PostIdHolder { - private Long postId; - - public Long getPostId() { - return postId; - } - - public void setPostId(Long postId) { - this.postId = postId; - } -} \ No newline at end of file diff --git a/graphql-kotlin/build.gradle.kts b/graphql-kotlin/build.gradle.kts index f70b308df..22f44c970 100644 --- a/graphql-kotlin/build.gradle.kts +++ b/graphql-kotlin/build.gradle.kts @@ -27,7 +27,7 @@ repositories { extra["testcontainersVersion"] = "1.19.8" extra["graphqlKotlinVersion"] = "8.0.0-alpha.1" -extra["coroutinesVersion"] = "1.8.1" +extra["coroutinesVersion"] = "1.9.0" extra["mockkVersion"] = "1.13.11" extra["springmockkVersion"] = "4.0.2" extra["kotestVersion"] = "5.9.1" @@ -39,8 +39,8 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-validation") // Expediagroup GraphQL Kotlin - implementation("com.expediagroup:graphql-kotlin-spring-server:${property("graphqlKotlinVersion")}") - implementation("com.graphql-java:graphql-java:${property("graphqlJavaVersion")}") + implementation("com.expediagroup:graphql-kotlin-spring-server:8.2.1") + // implementation("com.graphql-java:graphql-java:22.1") // r2dbc implementation("org.springframework.boot:spring-boot-starter-data-r2dbc") runtimeOnly("org.postgresql:r2dbc-postgresql") @@ -52,23 +52,23 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib") // kotlin coroutines - developmentOnly("org.jetbrains.kotlinx:kotlinx-coroutines-debug:${property("coroutinesVersion")}") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:${property("coroutinesVersion")}") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${property("coroutinesVersion")}") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:${property("coroutinesVersion")}") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${property("coroutinesVersion")}") + developmentOnly("org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.9.0") // test testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(module = "mockito-core") } testImplementation("io.projectreactor:reactor-test") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:${property("coroutinesVersion")}") - testImplementation("io.mockk:mockk-jvm:${property("mockkVersion")}") - testImplementation("com.ninja-squad:springmockk:${property("springmockkVersion")}") - testImplementation("io.kotest:kotest-runner-junit5-jvm:${property("kotestVersion")}") - testImplementation("io.kotest:kotest-assertions-core-jvm:${property("kotestVersion")}") - testImplementation("io.kotest:kotest-framework-concurrency:${property("kotestVersion")}") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.9.0") + testImplementation("io.mockk:mockk-jvm:1.13.13") + testImplementation("com.ninja-squad:springmockk:4.0.2") + testImplementation("io.kotest:kotest-runner-junit5-jvm:5.9.1") + testImplementation("io.kotest:kotest-assertions-core-jvm:5.9.1") + testImplementation("io.kotest:kotest-framework-concurrency:5.9.1") // testcontainters testImplementation("org.testcontainers:junit-jupiter") @@ -77,12 +77,6 @@ dependencies { runtimeOnly("org.postgresql:postgresql") } -dependencyManagement { - imports { - mavenBom("org.testcontainers:testcontainers-bom:${property("testcontainersVersion")}") - } -} - kotlin { compilerOptions { apiVersion.set(KotlinVersion.KOTLIN_2_0) diff --git a/graphql-kotlin/src/main/kotlin/com/example/demo/ValidationConfig.kt b/graphql-kotlin/src/main/kotlin/com/example/demo/ValidationConfig.kt index c7f30fb89..662d5169c 100644 --- a/graphql-kotlin/src/main/kotlin/com/example/demo/ValidationConfig.kt +++ b/graphql-kotlin/src/main/kotlin/com/example/demo/ValidationConfig.kt @@ -22,55 +22,58 @@ import kotlin.reflect.jvm.kotlinFunction // see: https://github.com/spring-projects/spring-framework/issues/23499#issuecomment-900369875 // related issues see: https://github.com/spring-projects/spring-framework/issues/23499 // and https://hibernate.atlassian.net/browse/HV-1638 -@Configuration -class ValidationConfig { - @Primary - @Bean - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - fun defaultValidator(): LocalValidatorFactoryBean { - val factoryBean = KotlinCoroutinesLocalValidatorFactoryBean() - factoryBean.messageInterpolator = MessageInterpolatorFactory().getObject() - return factoryBean - } -} - -class KotlinCoroutinesLocalValidatorFactoryBean : LocalValidatorFactoryBean() { - override fun getClockProvider(): ClockProvider = DefaultClockProvider.INSTANCE - - override fun postProcessConfiguration(configuration: jakarta.validation.Configuration<*>) { - super.postProcessConfiguration(configuration) - - val discoverer = PrioritizedParameterNameDiscoverer() - discoverer.addDiscoverer(SuspendAwareKotlinParameterNameDiscoverer()) - discoverer.addDiscoverer(StandardReflectionParameterNameDiscoverer()) - - val defaultProvider = configuration.defaultParameterNameProvider - configuration.parameterNameProvider(object : ParameterNameProvider { - override fun getParameterNames(constructor: Constructor<*>): List { - val paramNames: Array? = discoverer.getParameterNames(constructor) - return paramNames?.toList() ?: defaultProvider.getParameterNames(constructor) - } - - override fun getParameterNames(method: Method): List { - val paramNames: Array? = discoverer.getParameterNames(method) - return paramNames?.toList() ?: defaultProvider.getParameterNames(method) - } - }) - } -} - -class SuspendAwareKotlinParameterNameDiscoverer : ParameterNameDiscoverer { - - private val defaultProvider = KotlinReflectionParameterNameDiscoverer() - - override fun getParameterNames(constructor: Constructor<*>): Array? = - defaultProvider.getParameterNames(constructor) - - override fun getParameterNames(method: Method): Array? { - val defaultNames = defaultProvider.getParameterNames(method) ?: return null - val function = method.kotlinFunction - return if (function != null && function.isSuspend) { - defaultNames + "" - } else defaultNames - } -} \ No newline at end of file +// =================================================== +// this is fixed in the latest spring boot +// =================================================== +//@Configuration +//class ValidationConfig { +// @Primary +// @Bean +// @Role(BeanDefinition.ROLE_INFRASTRUCTURE) +// fun defaultValidator(): LocalValidatorFactoryBean { +// val factoryBean = KotlinCoroutinesLocalValidatorFactoryBean() +// factoryBean.messageInterpolator = MessageInterpolatorFactory().getObject() +// return factoryBean +// } +//} +// +//class KotlinCoroutinesLocalValidatorFactoryBean : LocalValidatorFactoryBean() { +// override fun getClockProvider(): ClockProvider = DefaultClockProvider.INSTANCE +// +// override fun postProcessConfiguration(configuration: jakarta.validation.Configuration<*>) { +// super.postProcessConfiguration(configuration) +// +// val discoverer = PrioritizedParameterNameDiscoverer() +// discoverer.addDiscoverer(SuspendAwareKotlinParameterNameDiscoverer()) +// discoverer.addDiscoverer(StandardReflectionParameterNameDiscoverer()) +// +// val defaultProvider = configuration.defaultParameterNameProvider +// configuration.parameterNameProvider(object : ParameterNameProvider { +// override fun getParameterNames(constructor: Constructor<*>): List { +// val paramNames: Array? = discoverer.getParameterNames(constructor) +// return paramNames?.toList() ?: defaultProvider.getParameterNames(constructor) +// } +// +// override fun getParameterNames(method: Method): List { +// val paramNames: Array? = discoverer.getParameterNames(method) +// return paramNames?.toList() ?: defaultProvider.getParameterNames(method) +// } +// }) +// } +//} +// +//class SuspendAwareKotlinParameterNameDiscoverer : ParameterNameDiscoverer { +// +// private val defaultProvider = KotlinReflectionParameterNameDiscoverer() +// +// override fun getParameterNames(constructor: Constructor<*>): Array? = +// defaultProvider.getParameterNames(constructor) +// +// override fun getParameterNames(method: Method): Array? { +// val defaultNames = defaultProvider.getParameterNames(method) ?: return null +// val function = method.kotlinFunction +// return if (function != null && function.isSuspend) { +// defaultNames + "" +// } else defaultNames +// } +//} \ No newline at end of file diff --git a/graphql-kotlin/src/main/resources/application.properties b/graphql-kotlin/src/main/resources/application.properties index fd1a5275c..516866413 100644 --- a/graphql-kotlin/src/main/resources/application.properties +++ b/graphql-kotlin/src/main/resources/application.properties @@ -1,7 +1,9 @@ graphql.packages=com.example.demo.gql # Send a ka message every 1000 ms (1 second) graphql.subscriptions.keepAliveInterval=1000 -graphql.subscriptions.protocol=APOLLO_SUBSCRIPTIONS_WS +# APOLLO_SUBSCRIPTIONS_WS id deprecated +graphql.subscriptions.protocol=GRAPHQL_WS +graphql.print-schema=true logging.level.root=INFO logging.level.web=DEBUG diff --git a/graphql-kotlin/src/test/kotlin/com/example/demo/DemoApplicationTests.kt b/graphql-kotlin/src/test/kotlin/com/example/demo/DemoApplicationTests.kt index f6e7415c3..14988e3df 100644 --- a/graphql-kotlin/src/test/kotlin/com/example/demo/DemoApplicationTests.kt +++ b/graphql-kotlin/src/test/kotlin/com/example/demo/DemoApplicationTests.kt @@ -30,7 +30,6 @@ import java.util.* import kotlin.random.Random -@OptIn(ExperimentalCoroutinesApi::class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class DemoApplicationTests { diff --git a/spring-graphql-querydsl/build.gradle b/spring-graphql-querydsl/build.gradle index ad9d6b771..989101957 100644 --- a/spring-graphql-querydsl/build.gradle +++ b/spring-graphql-querydsl/build.gradle @@ -36,7 +36,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-graphql' // Jakarta EE 10 - implementation 'jakarta.persistence:jakarta.persistence-api:3.2.0' + implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0' implementation 'jakarta.inject:jakarta.inject-api:2.0.1' implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' @@ -58,7 +58,7 @@ dependencies { implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta', - 'jakarta.persistence:jakarta.persistence-api:3.2.0', + 'jakarta.persistence:jakarta.persistence-api:3.1.0', 'jakarta.annotation:jakarta.annotation-api:2.1.1' //test diff --git a/spring-graphql-querydsl/src/test/java/com/example/demo/QueryTests.java b/spring-graphql-querydsl/src/test/java/com/example/demo/QueryTests.java index e4f7af45a..6c98dcc42 100644 --- a/spring-graphql-querydsl/src/test/java/com/example/demo/QueryTests.java +++ b/spring-graphql-querydsl/src/test/java/com/example/demo/QueryTests.java @@ -13,8 +13,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.graphql.test.tester.HttpGraphQlTester; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import java.time.LocalDateTime; import java.util.List; @@ -38,7 +38,7 @@ public class QueryTests { @Autowired ObjectMapper objectMapper; - @MockBean + @MockitoBean PostRepository postRepository; @BeforeEach @@ -76,7 +76,7 @@ void allPosts() { title content } - }"""; + }""".stripIndent(); graphQlTester.document(allPosts) .execute() .path("posts[*].title") @@ -101,7 +101,7 @@ query postById($id: ID!){ title content } - }""".trim(); + }""".stripIndent(); var id = UUID.randomUUID(); graphQlTester.document(postById) .variable("id", id)