diff --git a/dgs-client/src/main/java/com/example/demo/DemoApplication.java b/dgs-client/src/main/java/com/example/demo/DemoApplication.java index 598e12604..2e92251eb 100644 --- a/dgs-client/src/main/java/com/example/demo/DemoApplication.java +++ b/dgs-client/src/main/java/com/example/demo/DemoApplication.java @@ -6,9 +6,13 @@ import com.example.demo.gql.client.PostProjection; import com.example.demo.gql.types.Post; import com.jayway.jsonpath.TypeRef; -import com.netflix.graphql.dgs.client.*; +import com.netflix.graphql.dgs.client.CustomGraphQLClient; +import com.netflix.graphql.dgs.client.GraphQLClient; +import com.netflix.graphql.dgs.client.GraphQLResponse; +import com.netflix.graphql.dgs.client.HttpResponse; import com.netflix.graphql.dgs.client.codegen.GraphQLQueryRequest; import lombok.extern.slf4j.Slf4j; +import org.intellij.lang.annotations.Language; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; @@ -44,20 +48,20 @@ private static HttpResponse execute(String url, Map exchange = dgsRestTemplate.exchange(url, HttpMethod.POST, new HttpEntity(body, requestHeaders), String.class); + ResponseEntity exchange = dgsRestTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(body, requestHeaders), String.class); /** * Return a HttpResponse, which contains the HTTP status code and response body (as a String). * The way to get these depend on the HTTP client. */ - return new HttpResponse(exchange.getStatusCodeValue(), exchange.getBody()); + return new HttpResponse(exchange.getStatusCode().value(), exchange.getBody()); } @@ -87,7 +91,7 @@ public void run(ApplicationArguments args) throws Exception { GraphQLQueryRequest graphQLQueryRequest = new GraphQLQueryRequest( new AllPostsGraphQLQuery(), - new AllPostsProjectionRoot,PostProjection>() + new AllPostsProjectionRoot, PostProjection>() .id() .title() .content() @@ -98,11 +102,14 @@ public void run(ApplicationArguments args) throws Exception { .createdAt() ); - String query = graphQLQueryRequest.serialize(); + @Language("graphql") String query = graphQLQueryRequest.serialize(); + log.info("query string: {}", query); + GraphQLClient client = new CustomGraphQLClient(url, DemoApplication::execute); - GraphQLResponse response = client.executeQuery(query, new HashMap<>() ); + GraphQLResponse response = client.executeQuery(query, new HashMap<>()); - var data = response.extractValueAsObject("allPosts", new TypeRef>() { }); + var data = response.extractValueAsObject("allPosts", new TypeRef>() { + }); log.info("fetched all posts from client: {}", data); } } diff --git a/dgs-codegen/build.gradle b/dgs-codegen/build.gradle index 94da30eb0..b9d9935f4 100644 --- a/dgs-codegen/build.gradle +++ b/dgs-codegen/build.gradle @@ -24,13 +24,13 @@ repositories { dependencyManagement { imports { - mavenBom("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:9.2.2") + mavenBom("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:10.0.1") } } dependencies { // implementation(platform("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:8.2.0")) - implementation "com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter", { + implementation "com.netflix.graphql.dgs:dgs-starter", { exclude group: 'org.yaml', module: 'snakeyaml' } @@ -55,6 +55,7 @@ dependencies { testCompileOnly 'org.projectlombok:lombok:1.18.36' testAnnotationProcessor 'org.projectlombok:lombok:1.18.36' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation "com.netflix.graphql.dgs:dgs-starter-test" } generateJava { diff --git a/dgs-codegen/src/main/java/com/example/demo/gql/CustomRuntimeWiring.java b/dgs-codegen/src/main/java/com/example/demo/gql/CustomRuntimeWiring.java index 20aa156f8..b455bcaa4 100644 --- a/dgs-codegen/src/main/java/com/example/demo/gql/CustomRuntimeWiring.java +++ b/dgs-codegen/src/main/java/com/example/demo/gql/CustomRuntimeWiring.java @@ -1,12 +1,6 @@ package com.example.demo.gql; import com.example.demo.gql.directives.UppercaseDirectiveWiring; -import com.netflix.graphql.dgs.DgsComponent; -import com.netflix.graphql.dgs.DgsRuntimeWiring; -import graphql.schema.idl.RuntimeWiring; -import graphql.validation.rules.OnValidationErrorStrategy; -import graphql.validation.rules.ValidationRules; -import graphql.validation.schemawiring.ValidationSchemaWiring; import lombok.RequiredArgsConstructor; //@DgsComponent diff --git a/dgs-codegen/src/main/java/com/example/demo/gql/scalars/LocalDateTimeScalar.java b/dgs-codegen/src/main/java/com/example/demo/gql/scalars/LocalDateTimeScalar.java index 1d3efa53a..d152c53e5 100644 --- a/dgs-codegen/src/main/java/com/example/demo/gql/scalars/LocalDateTimeScalar.java +++ b/dgs-codegen/src/main/java/com/example/demo/gql/scalars/LocalDateTimeScalar.java @@ -2,37 +2,61 @@ import com.netflix.graphql.dgs.DgsScalar; +import graphql.GraphQLContext; +import graphql.execution.CoercedVariables; +import graphql.language.NullValue; 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 org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.Locale; @DgsScalar(name = "LocalDateTime") public class LocalDateTimeScalar implements Coercing { + + @Nullable @Override - public String serialize(Object dataFetcherResult) throws CoercingSerializeException { - if (dataFetcherResult instanceof LocalDateTime) { - return ((LocalDateTime) dataFetcherResult).format(DateTimeFormatter.ISO_DATE_TIME); - } else { - throw new CoercingSerializeException("Not a valid DateTime"); + public String serialize(@NotNull Object dataFetcherResult, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingSerializeException { + if (dataFetcherResult instanceof LocalDateTime dateTime) { + return dateTime.format(DateTimeFormatter.ISO_DATE_TIME); } + + throw new CoercingSerializeException("Not a valid DateTime"); } + @Nullable @Override - public LocalDateTime parseValue(Object input) throws CoercingParseValueException { - return LocalDateTime.parse(input.toString(), DateTimeFormatter.ISO_DATE_TIME); + public LocalDateTime parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseValueException { + if (input instanceof LocalDateTime dateTime) { + return LocalDateTime.parse(dateTime.toString(), DateTimeFormatter.ISO_DATE_TIME); + } + + throw new CoercingParseValueException("Value is not a valid ISO date time"); } + @Nullable @Override - public LocalDateTime parseLiteral(Object input) throws CoercingParseLiteralException { - if (input instanceof StringValue) { - return LocalDateTime.parse(((StringValue) input).getValue(), DateTimeFormatter.ISO_DATE_TIME); + public LocalDateTime parseLiteral(@NotNull Value input, @NotNull CoercedVariables variables, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseLiteralException { + if (input instanceof StringValue value) { + return LocalDateTime.parse(value.getValue(), DateTimeFormatter.ISO_DATE_TIME); } throw new CoercingParseLiteralException("Value is not a valid ISO date time"); } + + @Override + public @NotNull Value valueToLiteral(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { + if (input instanceof LocalDateTime dateTime) { + return StringValue.of(dateTime.format(DateTimeFormatter.ISO_DATE_TIME)); + } + return NullValue.of(); + } + } \ No newline at end of file diff --git a/dgs-codegen/src/main/java/com/example/demo/gql/scalars/UUIDScalar.java b/dgs-codegen/src/main/java/com/example/demo/gql/scalars/UUIDScalar.java index ec677006f..022c627ce 100644 --- a/dgs-codegen/src/main/java/com/example/demo/gql/scalars/UUIDScalar.java +++ b/dgs-codegen/src/main/java/com/example/demo/gql/scalars/UUIDScalar.java @@ -1,36 +1,51 @@ 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 org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.Locale; import java.util.UUID; @DgsScalar(name = "UUID") public class UUIDScalar implements Coercing { + + @Override + public @NotNull Value valueToLiteral(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) { + return StringValue.of(input.toString()); + } + + @Nullable @Override - public String serialize(Object o) throws CoercingSerializeException { - if (o instanceof UUID) { - return ((UUID) o).toString(); - } else { - throw new CoercingSerializeException("Not a valid UUID"); + public UUID parseLiteral(@NotNull Value input, @NotNull CoercedVariables variables, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseLiteralException { + if (input instanceof StringValue value) { + return UUID.fromString(value.getValue()); } + + throw new CoercingParseLiteralException("Value is not a valid UUID string"); } + @Nullable @Override - public UUID parseValue(Object o) throws CoercingParseValueException { - return UUID.fromString(o.toString()); + public UUID parseValue(@NotNull Object input, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingParseValueException { + return UUID.fromString(input.toString()); } + @Nullable @Override - public UUID parseLiteral(Object input) throws CoercingParseLiteralException { - if (input instanceof StringValue) { - return UUID.fromString(((StringValue) input).getValue()); + public String serialize(@NotNull Object dataFetcherResult, @NotNull GraphQLContext graphQLContext, @NotNull Locale locale) throws CoercingSerializeException { + if (dataFetcherResult instanceof UUID uuid) { + return uuid.toString(); } - throw new CoercingParseLiteralException("Value is not a valid UUID string"); + throw new CoercingSerializeException("Not a valid UUID"); } } diff --git a/dgs-codegen/src/test/java/com/example/demo/MutationTests.java b/dgs-codegen/src/test/java/com/example/demo/MutationTests.java index e1c0b027f..2229d4631 100644 --- a/dgs-codegen/src/test/java/com/example/demo/MutationTests.java +++ b/dgs-codegen/src/test/java/com/example/demo/MutationTests.java @@ -10,18 +10,17 @@ import com.example.demo.service.AuthorService; import com.example.demo.service.PostService; import com.netflix.graphql.dgs.DgsQueryExecutor; -import com.netflix.graphql.dgs.autoconfig.DgsAutoConfiguration; import com.netflix.graphql.dgs.autoconfig.DgsExtendedValidationAutoConfiguration; import com.netflix.graphql.dgs.client.codegen.GraphQLQueryRequest; +import com.netflix.graphql.dgs.test.EnableDgsTest; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; 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 java.util.UUID; @@ -30,6 +29,7 @@ import static org.mockito.Mockito.*; @SpringBootTest(classes = MutationTests.MutationTestsConfig.class) +@EnableDgsTest @Slf4j class MutationTests { @@ -42,9 +42,7 @@ class MutationTests { CustomDataFetchingExceptionHandler.class }) @ImportAutoConfiguration(classes = { - DgsAutoConfiguration.class, DgsExtendedValidationAutoConfiguration.class, - JacksonAutoConfiguration.class }) static class MutationTestsConfig { @@ -53,10 +51,10 @@ static class MutationTestsConfig { @Autowired DgsQueryExecutor dgsQueryExecutor; - @MockBean + @MockitoBean PostService postService; - @MockBean + @MockitoBean AuthorService authorService; @Test diff --git a/dgs-codegen/src/test/java/com/example/demo/QueryTests.java b/dgs-codegen/src/test/java/com/example/demo/QueryTests.java index 14f673cb7..63331a6a0 100644 --- a/dgs-codegen/src/test/java/com/example/demo/QueryTests.java +++ b/dgs-codegen/src/test/java/com/example/demo/QueryTests.java @@ -11,17 +11,15 @@ import com.example.demo.service.PostNotFoundException; import com.example.demo.service.PostService; import com.netflix.graphql.dgs.DgsQueryExecutor; -import com.netflix.graphql.dgs.autoconfig.DgsAutoConfiguration; import com.netflix.graphql.dgs.client.codegen.GraphQLQueryRequest; +import com.netflix.graphql.dgs.test.EnableDgsTest; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; 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 java.util.List; import java.util.Map; @@ -31,16 +29,17 @@ import static org.mockito.Mockito.*; @SpringBootTest(classes = {QueryTests.QueryTestsConfig.class}) +@EnableDgsTest @Slf4j class QueryTests { @Autowired DgsQueryExecutor dgsQueryExecutor; - @MockBean + @MockitoBean PostService postService; - @MockBean + @MockitoBean AuthorService authorService; @Configuration @@ -50,10 +49,6 @@ class QueryTests { LocalDateTimeScalar.class, UppercaseDirectiveWiring.class }) - @ImportAutoConfiguration(value = { - DgsAutoConfiguration.class, - JacksonAutoConfiguration.class - }) static class QueryTestsConfig { } diff --git a/dgs-fileupload/build.gradle b/dgs-fileupload/build.gradle index 54221e605..4b8daccb7 100644 --- a/dgs-fileupload/build.gradle +++ b/dgs-fileupload/build.gradle @@ -23,23 +23,18 @@ repositories { dependencyManagement { imports { - mavenBom("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:9.2.2") + mavenBom("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:10.0.1") } } dependencies { - //implementation(platform("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:8.2.0")) - implementation "com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter", { - exclude group: 'org.yaml', module: 'snakeyaml' - } - implementation 'com.netflix.graphql.dgs:graphql-dgs-extended-scalars', { - exclude group: 'org.yaml', module: 'snakeyaml' - }// auto-configure graphql extended scalars - implementation 'org.yaml:snakeyaml:2.3' + implementation "com.netflix.graphql.dgs:dgs-starter" + implementation 'com.netflix.graphql.dgs:graphql-dgs-extended-scalars' implementation 'org.apache.commons:commons-lang3:3.17.0' // spring web implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'name.nkonev.multipart-spring-graphql:multipart-spring-graphql:1.5.3' //configure Lombok for compile java/ compile tests compileOnly 'org.projectlombok:lombok:1.18.36' @@ -47,6 +42,7 @@ dependencies { testCompileOnly 'org.projectlombok:lombok:1.18.36' testAnnotationProcessor 'org.projectlombok:lombok:1.18.36' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'com.netflix.graphql.dgs:dgs-starter-test' } test { diff --git a/dgs-fileupload/src/main/java/com/example/demo/gql/FileUploadMutation.java b/dgs-fileupload/src/main/java/com/example/demo/gql/FileUploadMutation.java index d72c5f612..d3d0bc19c 100644 --- a/dgs-fileupload/src/main/java/com/example/demo/gql/FileUploadMutation.java +++ b/dgs-fileupload/src/main/java/com/example/demo/gql/FileUploadMutation.java @@ -1,6 +1,7 @@ package com.example.demo.gql; import com.netflix.graphql.dgs.DgsComponent; +import com.netflix.graphql.dgs.DgsDataFetchingEnvironment; import com.netflix.graphql.dgs.DgsMutation; import com.netflix.graphql.dgs.InputArgument; import lombok.RequiredArgsConstructor; @@ -18,13 +19,16 @@ public class FileUploadMutation { @DgsMutation - public Boolean upload(@InputArgument("file") MultipartFile file) { + public Boolean upload(/*@InputArgument("file") MultipartFile file*/ DgsDataFetchingEnvironment env) { + var file = (MultipartFile)env.getArgument("file"); printFileInfo(file); return true; } @DgsMutation - public Boolean uploadWithDesc(@InputArgument("desc") String desc, @InputArgument("file") MultipartFile file) { + public Boolean uploadWithDesc(@InputArgument("desc") String desc, /*@InputArgument("file") MultipartFile file*/ + DgsDataFetchingEnvironment env) { + var file = (MultipartFile)env.getArgument("file"); log.info("description: {}", desc); printFileInfo(file); return true; @@ -32,26 +36,28 @@ public Boolean uploadWithDesc(@InputArgument("desc") String desc, @InputArgument @DgsMutation - public Boolean uploads(@InputArgument("files") List files) { + public Boolean uploads(/*@InputArgument("files") List files*/ + DgsDataFetchingEnvironment env) { + var files = (List)env.getArgument("files"); files.forEach(file -> printFileInfo(file)); return true; } - - @DgsMutation - public Boolean fileUpload(@InputArgument("file") FileUploadInput file) { - log.info("description: {}", file.getDescription()); - printFileInfo(file.getFile()); - return true; - } - - @DgsMutation - public Boolean fileUploads(@InputArgument("files") List files) { - files.forEach(file -> { - log.info("description: {}", file.getDescription()); - printFileInfo(file.getFile()); - }); - return true; - } +// does not work +// @DgsMutation +// public Boolean fileUpload(@InputArgument("file") FileUploadInput file) { +// log.info("description: {}", file.getDescription()); +// printFileInfo(file.getFile()); +// return true; +// } +// +// @DgsMutation +// public Boolean fileUploads(@InputArgument("files") List files) { +// files.forEach(file -> { +// log.info("description: {}", file.getDescription()); +// printFileInfo(file.getFile()); +// }); +// return true; +// } @SneakyThrows private void printFileInfo(MultipartFile file) { diff --git a/dgs-fileupload/src/test/java/com/example/demo/DemoApplicationTests.java b/dgs-fileupload/src/test/java/com/example/demo/DemoApplicationTests.java index 1cd8c2f35..c0acbce87 100644 --- a/dgs-fileupload/src/test/java/com/example/demo/DemoApplicationTests.java +++ b/dgs-fileupload/src/test/java/com/example/demo/DemoApplicationTests.java @@ -2,6 +2,8 @@ import com.example.demo.gql.FileUploadInput; import com.netflix.graphql.dgs.DgsQueryExecutor; +import com.netflix.graphql.dgs.test.EnableDgsTest; +import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -14,6 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest() +@EnableDgsTest class DemoApplicationTests { @Autowired @@ -21,7 +24,7 @@ class DemoApplicationTests { @Test public void testUpload() { - var query = "mutation upload($file:Upload!){ upload(file:$file) }"; + @Language("GraphQL") var query = "mutation upload($file:Upload!){ upload(file:$file) }"; var result = dgsQueryExecutor.executeAndExtractJsonPathAsObject( query, "data.upload", @@ -34,7 +37,7 @@ public void testUpload() { @Test public void testUploadWithDesc() { - var query = "mutation uploadWithDesc($desc:String, $file:Upload!){ uploadWithDesc(desc:$desc, file:$file) }"; + @Language("GraphQL") var query = "mutation uploadWithDesc($desc:String, $file:Upload!){ uploadWithDesc(desc:$desc, file:$file) }"; var result = dgsQueryExecutor.executeAndExtractJsonPathAsObject( query, "data.uploadWithDesc", @@ -48,7 +51,7 @@ public void testUploadWithDesc() { @Test public void testUploads() { - var query = "mutation uploads($files:[Upload!]!){ uploads(files:$files) }"; + @Language("GraphQL") var query = "mutation uploads($files:[Upload!]!){ uploads(files:$files) }"; var result = dgsQueryExecutor.executeAndExtractJsonPathAsObject( query, "data.uploads", @@ -67,7 +70,7 @@ public void testUploads() { @Test @Disabled("this dose not work") public void testFileUpload() { - var query = "mutation fileUpload($file:FileUploadInput!){ fileUpload(file:$file) }"; + @Language("GraphQL") var query = "mutation fileUpload($file:FileUploadInput!){ fileUpload(file:$file) }"; var result = dgsQueryExecutor.executeAndExtractJsonPathAsObject( query, "data.fileUpload", @@ -86,8 +89,9 @@ public void testFileUpload() { @Test @Disabled("this dose not work") public void testFileUploads() { - var query = "mutation fileUploads($files:[FileUploadInput!]!){ fileUploads(files:$files)}"; + @Language("GraphQL") var query = "mutation fileUploads($files:[FileUploadInput!]!){ fileUploads(files:$files)}"; var result = dgsQueryExecutor.executeAndExtractJsonPathAsObject( + query, "data.fileUploads", Map.of("files", diff --git a/dgs-kotlin-co/build.gradle.kts b/dgs-kotlin-co/build.gradle.kts index 2407d8fd9..cc7057e0f 100644 --- a/dgs-kotlin-co/build.gradle.kts +++ b/dgs-kotlin-co/build.gradle.kts @@ -1,6 +1,4 @@ import com.netflix.graphql.dgs.codegen.gradle.GenerateJavaTask -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.jetbrains.kotlin.config.ApiVersion.Companion.KOTLIN_2_0 import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion @@ -18,9 +16,9 @@ group = "com.example" version = "0.0.1-SNAPSHOT" java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } repositories { @@ -31,14 +29,15 @@ repositories { dependencyManagement { imports { - mavenBom("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:9.2.2") + mavenBom("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:10.0.1") + mavenBom("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.1") + mavenBom("io.kotest:kotest-bom:5.9.1") } } dependencies { //implementation(platform("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:8.1.1")) - implementation("com.netflix.graphql.dgs:graphql-dgs-webflux-starter") - implementation("io.projectreactor:reactor-core:3.7.1") + implementation("com.netflix.graphql.dgs:dgs-starter") //Spring implementation("org.springframework.boot:spring-boot-starter-webflux") @@ -53,22 +52,23 @@ dependencies { implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") //kotlin coroutines extensions - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.10.1") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") // test testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(module = "mockito-core") } + testImplementation("com.netflix.graphql.dgs:dgs-starter-test") testImplementation("io.projectreactor:reactor-test") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test") testImplementation("io.mockk:mockk-jvm:1.13.14") - testImplementation("com.ninja-squad:springmockk:4.0.2"){ + testImplementation("com.ninja-squad:springmockk:4.0.2") { exclude(module = "mockk") } - 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") + testImplementation("io.kotest:kotest-runner-junit5-jvm") + testImplementation("io.kotest:kotest-assertions-core-jvm") + testImplementation("io.kotest:kotest-framework-concurrency") } tasks.withType { @@ -79,6 +79,7 @@ tasks.withType { shortProjectionNames = false maxProjectionDepth = 2 snakeCaseConstantNames = true + typeMapping = mutableMapOf("UUID" to "java.util.UUID") } diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/Extensions.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/Extensions.kt index a25c0e557..3644ba539 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/Extensions.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/Extensions.kt @@ -8,22 +8,22 @@ import com.example.demo.model.CommentEntity import com.example.demo.model.PostEntity fun PostEntity.asGqlType(): Post = Post( - id = this.id!!.toString(), + id = this.id!!, title = this.title, content = this.content, createdAt = this.createdAt, - authorId = this.authorId.toString() + authorId = this.authorId ) fun CommentEntity.asGqlType(): Comment = Comment( - id = this.id!!.toString(), + id = this.id!!, content = this.content, createdAt = this.createdAt, - postId = this.postId!!.toString() + postId = this.postId!! ) fun AuthorEntity.asGqlType(): Author = Author( - id = this.id!!.toString(), + id = this.id!!, name = this.name, email = this.email, createdAt = this.createdAt 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 2da4ea811..c8c824076 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 @@ -7,6 +7,7 @@ import com.example.demo.service.AuthorService import com.example.demo.service.PostService import com.netflix.graphql.dgs.* import kotlinx.coroutines.flow.toList +import java.util.UUID @DgsComponent class AuthorsDataFetcher( @@ -15,7 +16,7 @@ class AuthorsDataFetcher( ) { @DgsQuery - suspend fun author(@InputArgument authorId: String) = authorService.getAuthorById(authorId) + suspend fun author(@InputArgument authorId: UUID) = authorService.getAuthorById(authorId) @DgsData(parentType = DgsConstants.AUTHOR.TYPE_NAME, field = DgsConstants.AUTHOR.Posts) suspend fun posts(dfe: DgsDataFetchingEnvironment): List { 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 6a94efb5e..84904904a 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 @@ -6,6 +6,7 @@ import com.example.demo.gql.types.* import com.example.demo.service.PostService import com.netflix.graphql.dgs.* import kotlinx.coroutines.flow.toList +import java.util.UUID import java.util.concurrent.CompletableFuture @DgsComponent @@ -17,18 +18,18 @@ class PostsDataFetcher(val postService: PostService) { suspend fun allPosts(): List = postService.allPosts().toList() @DgsQuery - suspend fun postById(@InputArgument postId: String) = postService.getPostById(postId) + suspend fun postById(@InputArgument postId: UUID) = postService.getPostById(postId) @DgsData(parentType = DgsConstants.POST.TYPE_NAME, field = DgsConstants.POST.Author) fun author(dfe: DgsDataFetchingEnvironment): CompletableFuture { - val dataLoader = dfe.getDataLoader("authorsLoader") + val dataLoader = dfe.getDataLoader("authorsLoader") val post = dfe.getSource() 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 dataLoader = dfe.getDataLoader>(CommentsDataLoader::class.java) val (id) = dfe.getSource()!! return dataLoader.load(id) } diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/dataloader/AuthorsDataLoader.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/dataloader/AuthorsDataLoader.kt index 9cdaebf3c..df62c0511 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/dataloader/AuthorsDataLoader.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/dataloader/AuthorsDataLoader.kt @@ -8,13 +8,14 @@ import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.flow.toList import kotlinx.coroutines.future.future import org.dataloader.BatchLoader +import java.util.UUID import java.util.concurrent.CompletionStage import java.util.concurrent.Executors @DgsDataLoader(name = "authorsLoader") -class AuthorsDataLoader(val authorService: AuthorService) : BatchLoader { +class AuthorsDataLoader(val authorService: AuthorService) : BatchLoader { val loaderScope = CoroutineScope(Executors.newCachedThreadPool().asCoroutineDispatcher()) - override fun load(keys: List): CompletionStage> = loaderScope.future { + override fun load(keys: List): CompletionStage> = loaderScope.future { authorService.getAuthorByIdIn(keys).toList() } } \ No newline at end of file diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/dataloader/CommentsDataLoader.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/dataloader/CommentsDataLoader.kt index 7ff4fab72..2b757fc62 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/dataloader/CommentsDataLoader.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/dataloader/CommentsDataLoader.kt @@ -10,19 +10,20 @@ import kotlinx.coroutines.future.future import org.dataloader.MappedBatchLoader import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.util.UUID import java.util.concurrent.CompletionStage import java.util.concurrent.Executors @DgsDataLoader(name = "commentsLoader") -class CommentsDataLoader(val postService: PostService) : MappedBatchLoader> { +class CommentsDataLoader(val postService: PostService) : MappedBatchLoader> { val loaderScope = CoroutineScope(Executors.newCachedThreadPool().asCoroutineDispatcher()) companion object { val log: Logger = LoggerFactory.getLogger(CommentsDataLoader::class.java) } - override fun load(keys: Set): CompletionStage>> = loaderScope.future { - val comments = postService.getCommentsByPostIdIn(keys).toList() - val mappedComments: MutableMap> = mutableMapOf() + override fun load(keys: Set): CompletionStage>> = loaderScope.future { + val comments = postService.getCommentsByPostIdIn(keys.toList()).toList() + val mappedComments: MutableMap> = mutableMapOf() keys.forEach { mappedComments[it] = comments.filter { c -> c.postId == it } } log.info("mapped comments: {}", mappedComments) mappedComments diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/scalar/UUIDScalar.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/scalar/UUIDScalar.kt new file mode 100644 index 000000000..76b5fe674 --- /dev/null +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/gql/scalar/UUIDScalar.kt @@ -0,0 +1,52 @@ +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.CoercingSerializeException +import java.util.* + +@DgsScalar(name = "UUID") +class UUIDScalar : Coercing { + + override fun valueToLiteral( + input: Any, + graphQLContext: GraphQLContext, + locale: Locale + ): Value<*> = StringValue.of(input.toString()) + + override fun parseLiteral( + input: Value<*>, + variables: CoercedVariables, + graphQLContext: GraphQLContext, + locale: Locale + ): UUID? { + if (input is StringValue) { + return UUID.fromString(input.value); + } + + throw CoercingParseLiteralException("Value is not a valid UUID string"); + } + + override fun parseValue( + input: Any, + graphQLContext: GraphQLContext, + locale: Locale + ): UUID? = UUID.fromString(input.toString()) + + override fun serialize( + dataFetcherResult: Any, + graphQLContext: GraphQLContext, + locale: Locale + ): String? { + if (dataFetcherResult is UUID) { + return dataFetcherResult.toString(); + } + + throw CoercingSerializeException("Not a valid UUID"); + } +} \ No newline at end of file diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/AuthorNotFoundException.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/AuthorNotFoundException.kt index 9988b5823..9f9c9b824 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/AuthorNotFoundException.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/AuthorNotFoundException.kt @@ -1,4 +1,6 @@ package com.example.demo.service -class AuthorNotFoundException(id: String) : RuntimeException("Author: $id was not found.") +import java.util.* + +class AuthorNotFoundException(id: UUID) : RuntimeException("Author: $id was not found.") diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/AuthorService.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/AuthorService.kt index db98adc74..9ea254ab8 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/AuthorService.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/AuthorService.kt @@ -11,14 +11,13 @@ import java.util.* @Service class AuthorService(val authors: AuthorRepository) { - suspend fun getAuthorById(id: String): Author { - val author = this.authors.findById(UUID.fromString(id)) ?: throw AuthorNotFoundException(id) + suspend fun getAuthorById(id: UUID): Author { + val author = this.authors.findById(id) ?: throw AuthorNotFoundException(id) return author.asGqlType() } // alternative to use kotlin co `Flow` - fun getAuthorByIdIn(ids: List): Flow { - val uuids = ids.map { UUID.fromString(it) }; - return authors.findAllById(uuids).map { it.asGqlType() } + fun getAuthorByIdIn(ids: List): Flow { + return authors.findAllById(ids).map { it.asGqlType() } } } \ No newline at end of file diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/DefaultPostService.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/DefaultPostService.kt index 4d0e84433..eef055161 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/DefaultPostService.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/DefaultPostService.kt @@ -1,20 +1,20 @@ package com.example.demo.service -import com.example.demo.model.CommentEntity -import com.example.demo.model.PostEntity import com.example.demo.asGqlType import com.example.demo.gql.types.Comment import com.example.demo.gql.types.CommentInput import com.example.demo.gql.types.CreatePostInput import com.example.demo.gql.types.Post +import com.example.demo.model.CommentEntity +import com.example.demo.model.PostEntity import com.example.demo.repository.CommentRepository import com.example.demo.repository.PostRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.map import org.slf4j.LoggerFactory import org.springframework.stereotype.Service -import reactor.core.publisher.Flux -import reactor.core.publisher.Sinks import java.util.* @Service @@ -26,13 +26,13 @@ class DefaultPostService( override fun allPosts() = this.posts.findAll().map { it.asGqlType() } - override suspend fun getPostById(id: String): Post { - val post = this.posts.findById(UUID.fromString(id)) ?: throw PostNotFoundException(id) + override suspend fun getPostById(id: UUID): Post { + val post = this.posts.findById(id) ?: throw PostNotFoundException(id) return post.asGqlType() } - override fun getPostsByAuthorId(id: String): Flow { - return this.posts.findByAuthorId(UUID.fromString(id)) + override fun getPostsByAuthorId(id: UUID): Flow { + return this.posts.findByAuthorId(id) .map { it.asGqlType() } } @@ -43,30 +43,29 @@ class DefaultPostService( } override suspend fun addComment(commentInput: CommentInput): Comment { - val postId = UUID.fromString(commentInput.postId) + val postId = commentInput.postId if (!this.posts.existsById(postId)) { - throw PostNotFoundException(postId.toString()) + throw PostNotFoundException(postId) } val data = CommentEntity(content = commentInput.content, postId = postId) val savedComment = this.comments.save(data) val comment = savedComment.asGqlType() - sink.emitNext(comment, Sinks.EmitFailureHandler.FAIL_FAST) + sink.emit(comment) return comment } - val sink = Sinks.many().replay().latest() + val sink = MutableSharedFlow(replay = 1) // subscription: commentAdded - override fun commentAdded(): Flux = sink.asFlux() + override fun commentAdded(): Flow = sink.asSharedFlow() - override fun getCommentsByPostId(id: String): Flow { - return this.comments.findByPostId(UUID.fromString(id)) + override fun getCommentsByPostId(id: UUID): Flow { + return this.comments.findByPostId(id) .map { it.asGqlType() } } - override fun getCommentsByPostIdIn(ids: Set): Flow { - val uuids = ids.map { UUID.fromString(it) }; - return comments.findByPostIdIn(uuids).map { it.asGqlType() } + override fun getCommentsByPostIdIn(ids: List): Flow { + return comments.findByPostIdIn(ids).map { it.asGqlType() } } } \ No newline at end of file diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/PostNotFoundException.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/PostNotFoundException.kt index 0b8d42362..f7dc93a6c 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/PostNotFoundException.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/PostNotFoundException.kt @@ -1,3 +1,5 @@ package com.example.demo.service -class PostNotFoundException(id: String) : RuntimeException("Post: $id was not found.") \ No newline at end of file +import java.util.UUID + +class PostNotFoundException(id: UUID) : RuntimeException("Post: $id was not found.") \ No newline at end of file diff --git a/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/PostService.kt b/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/PostService.kt index b579c2298..783e8418a 100644 --- a/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/PostService.kt +++ b/dgs-kotlin-co/src/main/kotlin/com/example/demo/service/PostService.kt @@ -6,19 +6,20 @@ import com.example.demo.gql.types.CreatePostInput import com.example.demo.gql.types.Post import kotlinx.coroutines.flow.Flow import reactor.core.publisher.Flux +import java.util.UUID interface PostService { fun allPosts(): Flow - suspend fun getPostById(id: String): Post - fun getPostsByAuthorId(id: String): Flow + suspend fun getPostById(id: UUID): Post + fun getPostsByAuthorId(id: UUID): Flow suspend fun createPost(postInput: CreatePostInput): Post suspend fun addComment(commentInput: CommentInput): Comment // subscription: commentAdded - fun commentAdded(): Flux - fun getCommentsByPostId(id: String): Flow - fun getCommentsByPostIdIn(ids: Set): Flow + fun commentAdded(): Flow + fun getCommentsByPostId(id: UUID): Flow + fun getCommentsByPostIdIn(ids: List): Flow } \ No newline at end of file diff --git a/dgs-kotlin-co/src/main/resources/application.properties b/dgs-kotlin-co/src/main/resources/application.properties index 16d8efc44..bb5704249 100644 --- a/dgs-kotlin-co/src/main/resources/application.properties +++ b/dgs-kotlin-co/src/main/resources/application.properties @@ -3,14 +3,20 @@ logging.level.root=INFO logging.level.web=DEBUG logging.level.sql=DEBUG logging.level.com.example=DEBUG -logging.level.com.netflix.graphql.dgs.webflux=DEBUG +logging.level.org.springframework.graphql=TRACE logging.level.org.springframework.security=DEBUG logging.level.reactor.core.publisher=TRACE +# r2dbc spring.r2dbc.url=r2dbc:postgresql://localhost/blogdb spring.r2dbc.username=user spring.r2dbc.password=password spring.sql.init.mode=always + +# security spring.security.user.name=user spring.security.user.password=password -spring.security.user.roles=USER \ No newline at end of file +spring.security.user.roles=USER + +# graphql +spring.graphql.websocket.path=/graphql \ No newline at end of file diff --git a/dgs-kotlin-co/src/main/resources/schema/schema.graphql b/dgs-kotlin-co/src/main/resources/schema/schema.graphql index f972ef33f..73e5c9645 100644 --- a/dgs-kotlin-co/src/main/resources/schema/schema.graphql +++ b/dgs-kotlin-co/src/main/resources/schema/schema.graphql @@ -1,32 +1,32 @@ directive @uppercase on FIELD_DEFINITION scalar LocalDateTime -#scalar UUID +scalar UUID #scalar Upload type Post{ - id: ID! + id: UUID! title: String! @uppercase content: String comments: [Comment] status: PostStatus createdAt: LocalDateTime - authorId:String + authorId:UUID author:Author } type Author{ - id:ID! + id:UUID! name:String! email:String! createdAt: LocalDateTime posts: [Post] } type Comment{ - id: ID! + id: UUID! content: String! createdAt: LocalDateTime - postId: String! + postId: UUID! } input CreatePostInput { @@ -35,20 +35,19 @@ input CreatePostInput { } input CommentInput{ - postId: String! + postId: UUID! content: String! } type Query { allPosts: [Post!]! - postById(postId: String!): Post - author(authorId: String!): Author + postById(postId: UUID!): Post + author(authorId: UUID!): Author } type Mutation { createPost(createPostInput: CreatePostInput!): Post! addComment(commentInput: CommentInput!): Comment! -# upload(file: Upload!): Boolean } type Subscription { diff --git a/dgs-kotlin-co/src/test/kotlin/com/example/demo/SubscriptionWithGraphQLClientTests.kt b/dgs-kotlin-co/src/test/kotlin/com/example/demo/IntegrationTests.kt similarity index 78% rename from dgs-kotlin-co/src/test/kotlin/com/example/demo/SubscriptionWithGraphQLClientTests.kt rename to dgs-kotlin-co/src/test/kotlin/com/example/demo/IntegrationTests.kt index 3d7a33a90..a48c44efe 100644 --- a/dgs-kotlin-co/src/test/kotlin/com/example/demo/SubscriptionWithGraphQLClientTests.kt +++ b/dgs-kotlin-co/src/test/kotlin/com/example/demo/IntegrationTests.kt @@ -3,7 +3,6 @@ package com.example.demo import com.example.demo.gql.types.Comment import com.example.demo.gql.types.Post import com.netflix.graphql.dgs.client.WebClientGraphQLClient -import com.netflix.graphql.dgs.client.WebSocketGraphQLClient import io.kotest.common.ExperimentalKotest import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -11,20 +10,25 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.reactor.awaitSingle import kotlinx.coroutines.reactor.awaitSingleOrNull import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.slf4j.LoggerFactory import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.graphql.client.WebSocketGraphQlClient import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient import reactor.test.StepVerifier +import java.net.URI +import java.time.Duration +import java.util.UUID @OptIn(ExperimentalCoroutinesApi::class, ExperimentalKotest::class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class SubscriptionWithGraphQLClientTests { +class IntegrationTests { companion object { - private val log = LoggerFactory.getLogger(SubscriptionWithGraphQLClientTests::class.java) + private val log = LoggerFactory.getLogger(IntegrationTests::class.java) } @LocalServerPort @@ -32,7 +36,7 @@ class SubscriptionWithGraphQLClientTests { lateinit var client: WebClientGraphQLClient - lateinit var websocketClient: WebSocketGraphQLClient + lateinit var socketClient: WebSocketGraphQlClient @BeforeEach fun setup() { @@ -42,7 +46,16 @@ class SubscriptionWithGraphQLClientTests { .build() client = WebClientGraphQLClient(webClient) - websocketClient = WebSocketGraphQLClient("ws://localhost:$port/subscriptions", ReactorNettyWebSocketClient()) + socketClient = WebSocketGraphQlClient.create( + URI.create("ws://localhost:$port/graphql"), + ReactorNettyWebSocketClient() + ) + this.socketClient.start().subscribe() + } + + @AfterEach + fun teardown() { + this.socketClient.stop().subscribe() } @Test @@ -67,14 +80,11 @@ class SubscriptionWithGraphQLClientTests { } } """.trimIndent() - val verifier = websocketClient - .reactiveExecuteQuery( - commentAddedSubscriptionQuery, - emptyMap() - ) - .map { + val verifier = socketClient + .document(commentAddedSubscriptionQuery).executeSubscription() + .mapNotNull { log.debug("WebSocket client response: $it") - it.extractValueAsObject("commentAdded", Comment::class.java) + it.getData>>()!!["commentAdded"] } .doOnNext { log.debug("doOnNext: $it") @@ -82,22 +92,22 @@ class SubscriptionWithGraphQLClientTests { //.subscribe { it -> comments.add(it) } .`as` { StepVerifier.create(it) } - .consumeNextWith { - it.content shouldBe "comment1" - } + .thenAwait(Duration.ofMillis(1000)) + .consumeNextWith { it!!["content"]!! shouldBe "comment1" } + .consumeNextWith { it!!["content"]!! shouldBe "comment2" } .thenCancel() .verifyLater() // delay to verify later // add comments to post addComment(postId, "comment1") - //addComment(postId, "comment2") + addComment(postId, "comment2") //addComment(postId, "comment3 ") // verify the result now. verifier.verify() } - private suspend fun addComment(postId: String, comment: String) { + private suspend fun addComment(postId: UUID, comment: String) { val commentQuery = """ mutation addComment(${'$'}input: CommentInput!) @@ -123,9 +133,9 @@ class SubscriptionWithGraphQLClientTests { addedComment.content shouldBe comment } - private suspend fun getPostById(postId: String) { + private suspend fun getPostById(postId: UUID) { val postByIdQuery = """ - query postById(${'$'}id: String!) + query postById(${'$'}id: UUID!) { postById(postId:${'$'}id) { @@ -146,7 +156,7 @@ class SubscriptionWithGraphQLClientTests { postByIdResult?.title shouldBe "test title" } - private suspend fun createPost(): String { + private suspend fun createPost(): UUID { val createPostQuery = """ mutation createPost(${'$'}input: CreatePostInput!) diff --git a/dgs-kotlin-co/src/test/kotlin/com/example/demo/MutationTests.kt b/dgs-kotlin-co/src/test/kotlin/com/example/demo/MutationTests.kt index 89dfefc0f..a8309f844 100644 --- a/dgs-kotlin-co/src/test/kotlin/com/example/demo/MutationTests.kt +++ b/dgs-kotlin-co/src/test/kotlin/com/example/demo/MutationTests.kt @@ -1,16 +1,15 @@ package com.example.demo -import com.example.demo.gql.datafetcher.AuthorsDataFetcher -import com.example.demo.gql.scalar.LocalDateTimeScalar import com.example.demo.gql.datafetcher.PostsDataFetcher +import com.example.demo.gql.scalar.LocalDateTimeScalar +import com.example.demo.gql.scalars.UUIDScalar import com.example.demo.gql.types.Post -import com.example.demo.service.AuthorService import com.example.demo.service.PostService -import com.fasterxml.jackson.databind.ObjectMapper -import com.netflix.graphql.dgs.autoconfig.DgsAutoConfiguration import com.netflix.graphql.dgs.reactive.DgsReactiveQueryExecutor -import com.netflix.graphql.dgs.webflux.autoconfiguration.DgsWebFluxAutoConfiguration +import com.netflix.graphql.dgs.test.EnableDgsTest import com.ninjasquad.springmockk.MockkBean +import graphql.schema.GraphQLScalarType +import graphql.schema.idl.RuntimeWiring import io.kotest.matchers.shouldBe import io.mockk.coEvery import io.mockk.coVerify @@ -22,29 +21,47 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.ImportAutoConfiguration import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Import +import org.springframework.graphql.execution.RuntimeWiringConfigurer import java.util.* @OptIn(ExperimentalCoroutinesApi::class) -@SpringBootTest(classes = [QueryTests.TestConfig::class]) +@SpringBootTest(classes = [MutationTests.TestConfig::class]) +@EnableDgsTest class MutationTests { @Configuration @Import( value = [ PostsDataFetcher::class, - LocalDateTimeScalar::class + LocalDateTimeScalar::class, + UUIDScalar::class ] ) @ImportAutoConfiguration( value = [ - DgsWebFluxAutoConfiguration::class, - DgsAutoConfiguration::class, WebFluxAutoConfiguration::class ] ) - class TestConfig + class TestConfig{ + + @Bean + fun customRuntimeWiring(): RuntimeWiringConfigurer{ + return object: RuntimeWiringConfigurer { + override fun configure(builder: RuntimeWiring.Builder) { + builder.scalar( + GraphQLScalarType.newScalar() + .name("UUID") + .description("UUID type") + .coercing(UUIDScalar()) + .build() + ) + } + } + } + } @Autowired lateinit var dgsQueryExecutor: DgsReactiveQueryExecutor @@ -56,7 +73,7 @@ class MutationTests { @Test fun `create new post`() = runTest { val post = Post( - id = UUID.randomUUID().toString(), + id = UUID.randomUUID(), title = "test title", content = "test content" ) diff --git a/dgs-kotlin-co/src/test/kotlin/com/example/demo/QueryTests.kt b/dgs-kotlin-co/src/test/kotlin/com/example/demo/QueryTests.kt index ce73f0b71..f376cd627 100644 --- a/dgs-kotlin-co/src/test/kotlin/com/example/demo/QueryTests.kt +++ b/dgs-kotlin-co/src/test/kotlin/com/example/demo/QueryTests.kt @@ -1,15 +1,15 @@ package com.example.demo -import com.example.demo.gql.datafetcher.AuthorsDataFetcher -import com.example.demo.gql.scalar.LocalDateTimeScalar import com.example.demo.gql.datafetcher.PostsDataFetcher +import com.example.demo.gql.scalar.LocalDateTimeScalar +import com.example.demo.gql.scalars.UUIDScalar import com.example.demo.gql.types.Post -import com.example.demo.service.AuthorService import com.example.demo.service.PostService -import com.netflix.graphql.dgs.autoconfig.DgsAutoConfiguration import com.netflix.graphql.dgs.reactive.DgsReactiveQueryExecutor -import com.netflix.graphql.dgs.webflux.autoconfiguration.DgsWebFluxAutoConfiguration +import com.netflix.graphql.dgs.test.EnableDgsTest import com.ninjasquad.springmockk.MockkBean +import graphql.schema.GraphQLScalarType +import graphql.schema.idl.RuntimeWiring import io.kotest.matchers.collections.shouldContainAll import io.mockk.coEvery import io.mockk.coVerify @@ -23,16 +23,15 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.ImportAutoConfiguration import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Import +import org.springframework.graphql.execution.RuntimeWiringConfigurer import java.util.* @OptIn(ExperimentalCoroutinesApi::class) -@SpringBootTest( - classes = [ - QueryTests.TestConfig::class, - ] -) +@SpringBootTest(classes = [QueryTests.TestConfig::class]) +@EnableDgsTest class QueryTests { companion object { private val log = LoggerFactory.getLogger(QueryTests::class.java) @@ -47,12 +46,25 @@ class QueryTests { ) @ImportAutoConfiguration( value = [ - DgsWebFluxAutoConfiguration::class, - DgsAutoConfiguration::class, WebFluxAutoConfiguration::class ] ) - class TestConfig + class TestConfig{ + @Bean + fun customRuntimeWiring(): RuntimeWiringConfigurer{ + return object: RuntimeWiringConfigurer { + override fun configure(builder: RuntimeWiring.Builder) { + builder.scalar( + GraphQLScalarType.newScalar() + .name("UUID") + .description("UUID type") + .coercing(UUIDScalar()) + .build() + ) + } + } + } + } @Autowired lateinit var dgsQueryExecutor: DgsReactiveQueryExecutor @@ -65,18 +77,18 @@ class QueryTests { coEvery { postService.allPosts() } returns flowOf( Post( - id = UUID.randomUUID().toString(), + id = UUID.randomUUID(), title = "test title", content = "test content" ), Post( - id = UUID.randomUUID().toString(), + id = UUID.randomUUID(), title = "test title2", content = "test content2" ), ) - val query =""" + val query = """ query allPosts { allPosts diff --git a/dgs-kotlin-co/src/test/kotlin/com/example/demo/SubscriptionTests.kt b/dgs-kotlin-co/src/test/kotlin/com/example/demo/SubscriptionTests.kt index 15be77a2e..e61cda0a8 100644 --- a/dgs-kotlin-co/src/test/kotlin/com/example/demo/SubscriptionTests.kt +++ b/dgs-kotlin-co/src/test/kotlin/com/example/demo/SubscriptionTests.kt @@ -2,8 +2,9 @@ package com.example.demo import com.example.demo.SubscriptionTests.SubscriptionTestsConfig import com.example.demo.gql.datafetcher.AuthorsDataFetcher -import com.example.demo.gql.scalar.LocalDateTimeScalar import com.example.demo.gql.datafetcher.PostsDataFetcher +import com.example.demo.gql.scalar.LocalDateTimeScalar +import com.example.demo.gql.scalars.UUIDScalar import com.example.demo.gql.types.Comment import com.example.demo.gql.types.CommentInput import com.example.demo.model.CommentEntity @@ -13,12 +14,13 @@ import com.example.demo.service.AuthorService import com.example.demo.service.DefaultPostService import com.example.demo.service.PostService import com.fasterxml.jackson.databind.ObjectMapper -import com.netflix.graphql.dgs.autoconfig.DgsAutoConfiguration import com.netflix.graphql.dgs.reactive.DgsReactiveQueryExecutor -import com.netflix.graphql.dgs.webflux.autoconfiguration.DgsWebFluxAutoConfiguration +import com.netflix.graphql.dgs.test.EnableDgsTest import com.ninjasquad.springmockk.MockkBean import com.ninjasquad.springmockk.SpykBean import graphql.ExecutionResult +import graphql.schema.GraphQLScalarType +import graphql.schema.idl.RuntimeWiring import io.kotest.matchers.shouldBe import io.mockk.coEvery import io.mockk.coVerify @@ -34,14 +36,17 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.ImportAutoConfiguration import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Import +import org.springframework.graphql.execution.RuntimeWiringConfigurer import java.time.LocalDateTime import java.util.* import java.util.concurrent.CopyOnWriteArrayList @OptIn(ExperimentalCoroutinesApi::class) @SpringBootTest(classes = [SubscriptionTestsConfig::class]) +@EnableDgsTest class SubscriptionTests { companion object { private val log = LoggerFactory.getLogger(SubscriptionTests::class.java) @@ -58,12 +63,25 @@ class SubscriptionTests { ) @ImportAutoConfiguration( value = [ - DgsWebFluxAutoConfiguration::class, - DgsAutoConfiguration::class, WebFluxAutoConfiguration::class ] ) - class SubscriptionTestsConfig + class SubscriptionTestsConfig{ + @Bean + fun customRuntimeWiring(): RuntimeWiringConfigurer{ + return object: RuntimeWiringConfigurer { + override fun configure(builder: RuntimeWiring.Builder) { + builder.scalar( + GraphQLScalarType.newScalar() + .name("UUID") + .description("UUID type") + .coercing(UUIDScalar()) + .build() + ) + } + } + } + } @Autowired lateinit var dgsQueryExecutor: DgsReactiveQueryExecutor @@ -81,7 +99,7 @@ class SubscriptionTests { lateinit var postService: PostService @MockkBean - lateinit var authorService: AuthorService + lateinit var authorService: AuthorService @Test fun `test subscriptions`() = runTest { @@ -131,14 +149,14 @@ class SubscriptionTests { postService.addComment( CommentInput( - postId = UUID.randomUUID().toString(), + postId = UUID.randomUUID(), content = "Comment 1" ) ) postService.addComment( CommentInput( - postId = UUID.randomUUID().toString(), + postId = UUID.randomUUID(), content = "Comment 1" ) ) @@ -148,6 +166,6 @@ class SubscriptionTests { // verify mock callings coVerify(exactly = 2) { postRepository.existsById(any()) } coVerify(exactly = 2) { commentRepository.save(any()) } - coVerify(atLeast = 2) { postService.addComment(any())} + coVerify(atLeast = 2) { postService.addComment(any()) } } } \ No newline at end of file diff --git a/dgs-kotlin/build.gradle.kts b/dgs-kotlin/build.gradle.kts index 69d52d40a..9827af5d1 100644 --- a/dgs-kotlin/build.gradle.kts +++ b/dgs-kotlin/build.gradle.kts @@ -1,7 +1,4 @@ import com.netflix.graphql.dgs.codegen.gradle.GenerateJavaTask -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.jetbrains.kotlin.config.ApiVersion.Companion.KOTLIN_2_0 -import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { @@ -16,30 +13,37 @@ plugins { group = "com.example" version = "0.0.1-SNAPSHOT" -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + +kotlin { + jvmToolchain(21) + compilerOptions { + apiVersion.set(KotlinVersion.KOTLIN_2_0) + languageVersion.set(KotlinVersion.KOTLIN_2_0) + freeCompilerArgs.addAll( + "-Xjsr305=strict", + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + ) + } } repositories { mavenLocal() mavenCentral() - maven { url = uri( "https://repo.spring.io/milestone") } - maven { url = uri( "https://repo.spring.io/snapshot") } + maven { url = uri("https://repo.spring.io/milestone") } + maven { url = uri("https://repo.spring.io/snapshot") } } dependencyManagement { imports { - mavenBom("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:9.2.2") + mavenBom("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:10.0.1") } } dependencies { // dgs //implementation(platform("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:8.1.1")) - implementation("com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter") - implementation("com.netflix.graphql.dgs:graphql-dgs-subscriptions-websockets-autoconfigure") + implementation("com.netflix.graphql.dgs:dgs-starter") implementation("com.netflix.graphql.dgs:graphql-dgs-extended-scalars") //jdbc @@ -49,6 +53,7 @@ dependencies { // spring boot implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-websocket") + implementation("name.nkonev.multipart-spring-graphql:multipart-spring-graphql:1.5.3") // spring security implementation("org.springframework.boot:spring-boot-starter-security") @@ -70,6 +75,8 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") testImplementation("io.projectreactor:reactor-test") + + testImplementation("com.netflix.graphql.dgs:dgs-starter-test") } tasks.withType { @@ -80,19 +87,10 @@ tasks.withType { shortProjectionNames = false maxProjectionDepth = 2 snakeCaseConstantNames = true -} - -kotlin { - compilerOptions { - apiVersion.set(KotlinVersion.KOTLIN_2_0) - languageVersion.set(KotlinVersion.KOTLIN_2_0) - jvmTarget.set(JvmTarget.fromTarget("21")) - freeCompilerArgs.addAll( - "-Xjsr305=strict", - "-opt-in=kotlin.RequiresOptIn", - "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" - ) - } + typeMapping = mutableMapOf( + "UUID" to "java.util.UUID", + "Upload" to "org.springframework.web.multipart.MultipartFile" + ) } tasks.withType { diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/Extensions.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/Extensions.kt index 9b2ea1656..1c42cdeed 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/Extensions.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/Extensions.kt @@ -10,23 +10,23 @@ import com.example.demo.model.PostEntity import com.example.demo.model.ProfileEntity fun PostEntity.asGqlType(): Post = Post( - id = this.id!!.toString(), + id = this.id!!, title = this.title, content = this.content, createdAt = this.createdAt, updatedAt = this.updatedAt, - authorId = this.authorId.toString() + authorId = this.authorId ) fun CommentEntity.asGqlType(): Comment = Comment( - id = this.id!!.toString(), + id = this.id!!, content = this.content, createdAt = this.createdAt, - postId = this.postId!!.toString() + postId = this.postId!! ) fun AuthorEntity.asGqlType(): Author = Author( - id = this.id!!.toString(), + id = this.id!!, name = this.name, email = this.email, createdAt = this.createdAt 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 f6de7f3aa..926011981 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 @@ -1,13 +1,15 @@ 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.gql.types.Author +import com.example.demo.gql.types.Post +import com.example.demo.gql.types.Profile import com.example.demo.service.AuthorService import com.example.demo.service.PostService import com.netflix.graphql.dgs.* import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.multipart.MultipartFile +import java.util.* @DgsComponent class AuthorsDataFetcher( @@ -16,7 +18,7 @@ class AuthorsDataFetcher( ) { @DgsQuery - fun author(@InputArgument authorId: String) = authorService.getAuthorById(authorId) + fun author(@InputArgument authorId: UUID) = authorService.getAuthorById(authorId) @DgsData(parentType = DgsConstants.AUTHOR.TYPE_NAME, field = DgsConstants.AUTHOR.Posts) fun posts(dfe: DgsDataFetchingEnvironment): List { @@ -24,10 +26,17 @@ class AuthorsDataFetcher( return postService.getPostsByAuthorId(a.id) } - @DgsMutation - @PreAuthorize("isAuthenticated()") - fun updateProfile(@InputArgument("bio") bio: String, @InputArgument("coverImage") file: MultipartFile) = - authorService.updateProfile(bio, file) +// @DgsMutation +// @PreAuthorize("isAuthenticated()") +// fun updateProfile( +// @InputArgument("bio") bio: String,/* @InputArgument("coverImage") file: MultipartFile*/ +// env: DgsDataFetchingEnvironment +// ): Profile { +// val file = env.getArgument("coverImage") +// ?: throw IllegalArgumentException("Upload file is not valid") +// return authorService.updateProfile(bio, file) +// } + @DgsData(parentType = DgsConstants.AUTHOR.TYPE_NAME, field = DgsConstants.AUTHOR.Profile) fun profile(dfe: DgsDataFetchingEnvironment): Profile? { 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 ebb7afaee..1a51807e5 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 @@ -7,6 +7,7 @@ import com.example.demo.service.PostService import com.netflix.graphql.dgs.* import org.springframework.security.access.annotation.Secured import org.springframework.security.access.prepost.PreAuthorize +import java.util.UUID import java.util.concurrent.CompletableFuture @DgsComponent @@ -16,18 +17,18 @@ class PostsDataFetcher(val postService: PostService) { fun allPosts() = postService.allPosts() @DgsQuery - fun postById(@InputArgument postId: String) = postService.getPostById(postId) + fun postById(@InputArgument postId: UUID) = postService.getPostById(postId) @DgsData(parentType = DgsConstants.POST.TYPE_NAME, field = DgsConstants.POST.Author) fun author(dfe: DgsDataFetchingEnvironment): CompletableFuture { - val dataLoader = dfe.getDataLoader("authorsLoader") + val dataLoader = dfe.getDataLoader("authorsLoader") val post = dfe.getSource()!! return dataLoader!!.load(post.authorId) } @DgsData(parentType = DgsConstants.POST.TYPE_NAME, field = DgsConstants.POST.Comments) fun comments(dfe: DgsDataFetchingEnvironment): CompletableFuture> { - val dataLoader = dfe.getDataLoader>( + val dataLoader = dfe.getDataLoader>( CommentsDataLoader::class.java ) val (id) = dfe.getSource()!! 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 e261d191d..74385b5a0 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.UUID 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> = completedFuture( +class AuthorsDataLoader(val authorService: AuthorService) : BatchLoader { + 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 81bbb85da..b783e176f 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 @@ -6,15 +6,16 @@ import com.netflix.graphql.dgs.DgsDataLoader import org.dataloader.MappedBatchLoader import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.util.UUID import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionStage @DgsDataLoader(name = "comments") -class CommentsDataLoader(val postService: PostService) : MappedBatchLoader> { +class CommentsDataLoader(val postService: PostService) : MappedBatchLoader> { - override fun load(keys: Set): CompletionStage>> { - val comments = postService.getCommentsByPostIdIn(keys) - val mappedComments: MutableMap> = hashMapOf() + override fun load(keys: Set): CompletionStage>> { + val comments = postService.getCommentsByPostIdIn(keys.toList()) + val mappedComments: MutableMap> = hashMapOf() keys.forEach { mappedComments[it] = comments.filter { (_, _, postId) -> postId == it } } diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/gql/scalars/UUIDScalar.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/scalars/UUIDScalar.kt new file mode 100644 index 000000000..76b5fe674 --- /dev/null +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/gql/scalars/UUIDScalar.kt @@ -0,0 +1,52 @@ +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.CoercingSerializeException +import java.util.* + +@DgsScalar(name = "UUID") +class UUIDScalar : Coercing { + + override fun valueToLiteral( + input: Any, + graphQLContext: GraphQLContext, + locale: Locale + ): Value<*> = StringValue.of(input.toString()) + + override fun parseLiteral( + input: Value<*>, + variables: CoercedVariables, + graphQLContext: GraphQLContext, + locale: Locale + ): UUID? { + if (input is StringValue) { + return UUID.fromString(input.value); + } + + throw CoercingParseLiteralException("Value is not a valid UUID string"); + } + + override fun parseValue( + input: Any, + graphQLContext: GraphQLContext, + locale: Locale + ): UUID? = UUID.fromString(input.toString()) + + override fun serialize( + dataFetcherResult: Any, + graphQLContext: GraphQLContext, + locale: Locale + ): String? { + if (dataFetcherResult is UUID) { + return dataFetcherResult.toString(); + } + + throw CoercingSerializeException("Not a valid UUID"); + } +} \ No newline at end of file diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/service/AuthorNotFoundException.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/service/AuthorNotFoundException.kt index 31e0a8653..4ed0e9404 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/service/AuthorNotFoundException.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/service/AuthorNotFoundException.kt @@ -1,3 +1,5 @@ package com.example.demo.service -class AuthorNotFoundException(id: String) : RuntimeException("Author: $id was not found.") \ No newline at end of file +import java.util.UUID + +class AuthorNotFoundException(id: UUID) : RuntimeException("Author: $id was not found.") \ No newline at end of file diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/service/AuthorService.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/service/AuthorService.kt index 5063dd249..42b05581b 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/service/AuthorService.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/service/AuthorService.kt @@ -14,13 +14,12 @@ import java.util.* @Service class AuthorService(val authors: AuthorRepository, val profiles: ProfileRepository, val gridFsTemplate: GridFsTemplate) { - fun getAuthorById(id: String): Author = this.authors.findById(UUID.fromString(id)) + fun getAuthorById(id: UUID): Author = this.authors.findById(id) .map { it.asGqlType() } .orElseThrow { AuthorNotFoundException(id) } - fun getAuthorByIdIn(ids: List): List { - val uuids = ids.map { UUID.fromString(it) }; - val authorEntities = authors.findAllById(uuids) + fun getAuthorByIdIn(ids: List): List { + val authorEntities = authors.findAllById(ids) return authorEntities.map { it.asGqlType() } } @@ -30,7 +29,7 @@ class AuthorService(val authors: AuthorRepository, val profiles: ProfileReposito return profiles.save(ProfileEntity(coverImgId = objectId, bio = bio)).asGqlType() } - fun getProfileByUserId(id: String): Profile? { - return profiles.findByUserId(UUID.fromString(id))?.asGqlType() + fun getProfileByUserId(id: UUID): Profile? { + return profiles.findByUserId(id)?.asGqlType() } } \ No newline at end of file diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/service/PostNotFoundException.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/service/PostNotFoundException.kt index c56ff385e..95885c35f 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/service/PostNotFoundException.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/service/PostNotFoundException.kt @@ -1,4 +1,6 @@ package com.example.demo.service -class PostNotFoundException(id: String) : RuntimeException("Post: $id was not found.") +import java.util.UUID + +class PostNotFoundException(id: UUID) : RuntimeException("Post: $id was not found.") diff --git a/dgs-kotlin/src/main/kotlin/com/example/demo/service/PostService.kt b/dgs-kotlin/src/main/kotlin/com/example/demo/service/PostService.kt index 9b5400c61..3728f685e 100644 --- a/dgs-kotlin/src/main/kotlin/com/example/demo/service/PostService.kt +++ b/dgs-kotlin/src/main/kotlin/com/example/demo/service/PostService.kt @@ -26,11 +26,11 @@ class PostService( fun allPosts() = this.posts.findAll().map { it.asGqlType() } - fun getPostById(id: String): Post = this.posts.findById(UUID.fromString(id)) + fun getPostById(id: UUID): Post = this.posts.findById(id) .map { it.asGqlType() } .orElseThrow { PostNotFoundException(id) } - fun getPostsByAuthorId(id: String) = this.posts.findByAuthorId(UUID.fromString(id)).map { it.asGqlType() } + fun getPostsByAuthorId(id: UUID) = this.posts.findByAuthorId(id).map { it.asGqlType() } fun createPost(postInput: CreatePostInput): Post { val data = PostEntity(title = postInput.title, content = postInput.content) @@ -39,7 +39,7 @@ class PostService( } fun addComment(commentInput: CommentInput): Comment { - val postId = UUID.fromString(commentInput.postId) + val postId = commentInput.postId return this.posts.findById(postId) .map { val data = CommentEntity(content = commentInput.content, postId = postId) @@ -49,18 +49,17 @@ class PostService( sink.emitNext(comment, Sinks.EmitFailureHandler.FAIL_FAST) comment } - .orElseThrow { PostNotFoundException(postId.toString()) } + .orElseThrow { PostNotFoundException(postId) } } val sink = Sinks.many().replay().latest() fun commentAdded(): Publisher = sink.asFlux() - fun getCommentsByPostId(id: String): List = this.comments.findByPostId(UUID.fromString(id)) + fun getCommentsByPostId(id: UUID): List = this.comments.findByPostId(id) .map { it.asGqlType() } - fun getCommentsByPostIdIn(ids: Set): List { - val uuids = ids.map { UUID.fromString(it) }; - val authorEntities = comments.findByPostIdIn(uuids) + fun getCommentsByPostIdIn(ids: List): List { + val authorEntities = comments.findByPostIdIn(ids) return authorEntities.map { it.asGqlType() } } } \ No newline at end of file diff --git a/dgs-kotlin/src/main/resources/schema/schema.graphql b/dgs-kotlin/src/main/resources/schema/schema.graphql index c63fb9b9d..baac6f612 100644 --- a/dgs-kotlin/src/main/resources/schema/schema.graphql +++ b/dgs-kotlin/src/main/resources/schema/schema.graphql @@ -1,15 +1,15 @@ type Post{ - id: ID! + id: UUID! title: String! content: String comments: [Comment!] createdAt: LocalDateTime updatedAt: LocalDateTime - authorId:String + authorId:UUID author:Author } type Author{ - id:ID! + id:UUID! name:String! email:String! createdAt: LocalDateTime @@ -21,9 +21,9 @@ type Profile { coverImageUrl: String! } type Comment{ - id: ID! + id: UUID! content: String! - postId: String! + postId: UUID! createdAt: LocalDateTime } @@ -33,15 +33,14 @@ input CreatePostInput { } input CommentInput{ - postId: String! + postId: UUID! content: String! } type Query{ allPosts: [Post!]! - postById(postId: String!): Post - author(authorId: String!): Author - + postById(postId: UUID!): Post + author(authorId: UUID!): Author } type AuthResult { @@ -59,7 +58,7 @@ type Mutation { signIn(credentials: Credentials!): AuthResult logout:Boolean createPost(createPostInput: CreatePostInput!): Post! - updateProfile(bio:String, coverImage: Upload!): Profile! @skipcodegen + #updateProfile(bio:String, coverImage: Upload!): Profile! #@skipcodegen addComment(commentInput: CommentInput!): Comment! } @@ -68,5 +67,5 @@ type Subscription { } scalar LocalDateTime -scalar Upload -directive @skipcodegen on FIELD_DEFINITION \ No newline at end of file +scalar UUID +#directive @skipcodegen on FIELD_DEFINITION \ No newline at end of file diff --git a/dgs-kotlin/src/test/kotlin/com/example/demo/QueryTests.kt b/dgs-kotlin/src/test/kotlin/com/example/demo/QueryTests.kt index 5412c9880..8cf9f0ba3 100644 --- a/dgs-kotlin/src/test/kotlin/com/example/demo/QueryTests.kt +++ b/dgs-kotlin/src/test/kotlin/com/example/demo/QueryTests.kt @@ -39,7 +39,7 @@ class QueryTests { @Test fun `get an non-existing post should return errors NOT_FOUND`() { val query = """ - query notExisted(${'$'}id: String!){ + query notExisted(${'$'}id: UUID!){ postById(postId:${'$'}id){ id title @@ -47,7 +47,7 @@ class QueryTests { } """.trimIndent() - val result = dgsQueryExecutor.execute(query, mapOf("id" to UUID.randomUUID().toString())) + val result = dgsQueryExecutor.execute(query, mapOf("id" to UUID.randomUUID())) assertThat(result.errors).isNotNull assertThat(result.errors[0].extensions["errorType"]).isEqualTo("NOT_FOUND") } diff --git a/dgs-kotlin/src/test/kotlin/com/example/demo/SubscriptionTests.kt b/dgs-kotlin/src/test/kotlin/com/example/demo/SubscriptionTests.kt index e768f513e..9c90cf0db 100644 --- a/dgs-kotlin/src/test/kotlin/com/example/demo/SubscriptionTests.kt +++ b/dgs-kotlin/src/test/kotlin/com/example/demo/SubscriptionTests.kt @@ -26,6 +26,7 @@ import org.springframework.web.socket.* import org.springframework.web.socket.client.standard.StandardWebSocketClient import org.springframework.web.socket.handler.TextWebSocketHandler import java.net.URI +import java.util.UUID @SpringBootTest( @@ -150,7 +151,7 @@ class SubscriptionTests { assertThat(commentsReplay).isEqualTo(arrayListOf("comment2")) } - private fun addComment(postId: String, comment: String, token: String) { + private fun addComment(postId: UUID, comment: String, token: String) { log.debug("add comment:[$comment] to post:[$postId]") val query = """ mutation addComment(${'$'}input: CommentInput!) { @@ -182,7 +183,7 @@ class SubscriptionTests { assertThat(responseEntity.body!!["data"]!!["addComment"]!!.content).isEqualTo(comment) } - private fun createPost(token: String): String { + private fun createPost(token: String): UUID { val query = """ mutation createPost(${'$'}input: CreatePostInput!){ createPost(createPostInput:${'$'}input) { diff --git a/dgs-subscription-sse/build.gradle.kts b/dgs-subscription-sse/build.gradle.kts index a1a071c30..694a1ae8c 100644 --- a/dgs-subscription-sse/build.gradle.kts +++ b/dgs-subscription-sse/build.gradle.kts @@ -1,7 +1,4 @@ import com.netflix.graphql.dgs.codegen.gradle.GenerateJavaTask -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.jetbrains.kotlin.config.ApiVersion.Companion.KOTLIN_2_0 -import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { @@ -13,15 +10,20 @@ plugins { id("com.netflix.dgs.codegen") version "7.0.3" //https://plugins.gradle.org/plugin/com.netflix.dgs.codegen } -// extra["graphql-java.version"] = "19.2" - group = "com.example" version = "0.0.1-SNAPSHOT" -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } +kotlin { + jvmToolchain(21) + compilerOptions { + apiVersion.set(KotlinVersion.KOTLIN_2_0) + languageVersion.set(KotlinVersion.KOTLIN_2_0) + freeCompilerArgs.addAll( + "-Xjsr305=strict", + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + ) + } } repositories { @@ -32,30 +34,27 @@ repositories { dependencyManagement { imports { - mavenBom("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:9.2.2") + mavenBom("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:10.0.1") } } dependencies { // implementation(platform("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:8.1.1")) - implementation("com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter") { - exclude("org.yaml", "snakeyaml") - } - implementation("com.netflix.graphql.dgs:graphql-dgs-subscriptions-sse-autoconfigure") { - exclude("org.yaml", "snakeyaml") - } + implementation("com.netflix.graphql.dgs:dgs-starter") implementation("org.yaml:snakeyaml:2.3") //Spring and kotlin implementation("org.springframework.boot:spring-boot-starter-web") + implementation("io.projectreactor:reactor-core") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib") // test testImplementation("com.netflix.graphql.dgs:graphql-dgs-client") testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation("org.springframework:spring-webflux") + testImplementation("org.springframework:spring-webflux") // provides WebClient testImplementation("io.projectreactor.netty:reactor-netty") + testImplementation("com.netflix.graphql.dgs:dgs-starter-test") testImplementation("io.projectreactor:reactor-test") } @@ -69,19 +68,6 @@ tasks.withType { snakeCaseConstantNames = true } -kotlin { - compilerOptions { - apiVersion.set(KotlinVersion.KOTLIN_2_0) - languageVersion.set(KotlinVersion.KOTLIN_2_0) - jvmTarget.set(JvmTarget.fromTarget("21")) - freeCompilerArgs.addAll( - "-Xjsr305=strict", - "-opt-in=kotlin.RequiresOptIn", - "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" - ) - } -} - tasks.withType { useJUnitPlatform() } diff --git a/dgs-subscription-sse/src/main/kotlin/com/example/demo/Scalars.kt b/dgs-subscription-sse/src/main/kotlin/com/example/demo/Scalars.kt index 806089cdc..ed6e1b117 100644 --- a/dgs-subscription-sse/src/main/kotlin/com/example/demo/Scalars.kt +++ b/dgs-subscription-sse/src/main/kotlin/com/example/demo/Scalars.kt @@ -18,16 +18,19 @@ class LocalDateTimeScalar : Coercing { @Throws(CoercingSerializeException::class) 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") + if (dataFetcherResult is LocalDateTime) { + return dataFetcherResult.format(DateTimeFormatter.ISO_DATE_TIME) } + throw CoercingSerializeException("Not a valid DateTime") } @Throws(CoercingParseValueException::class) override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): LocalDateTime? { + if (input is String) { + return LocalDateTime.parse(input, DateTimeFormatter.ISO_DATE_TIME) + } - return LocalDateTime.parse(input.toString(), DateTimeFormatter.ISO_DATE_TIME) + throw CoercingParseLiteralException("Can not parse DateTime") } @Throws(CoercingParseLiteralException::class) @@ -36,14 +39,16 @@ class LocalDateTimeScalar : Coercing { 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") + ): LocalDateTime? { + if (input is StringValue) { + return LocalDateTime.parse(input.value, DateTimeFormatter.ISO_DATE_TIME) } + + throw CoercingParseLiteralException("Value is not a valid ISO date time") } override fun valueToLiteral(input: Any, graphQLContext: GraphQLContext, locale: Locale): Value<*> { - return StringValue.of(input.toString()) + val stringValue = serialize(input, graphQLContext, locale) + return StringValue.of(stringValue) } } \ No newline at end of file diff --git a/dgs-subscription-sse/src/main/resources/application.properties b/dgs-subscription-sse/src/main/resources/application.properties index ef1c4983f..a58dfdd68 100644 --- a/dgs-subscription-sse/src/main/resources/application.properties +++ b/dgs-subscription-sse/src/main/resources/application.properties @@ -3,4 +3,4 @@ logging.level.com.example=DEBUG logging.level.web=DEBUG logging.level.com.netflix.graphql.dgs=TRACE -spring.mvc.async.request-timeout=45000 +spring.mvc.async.request-timeout=45000 \ No newline at end of file diff --git a/dgs-subscription-sse/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt b/dgs-subscription-sse/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt deleted file mode 100644 index e71b18fd7..000000000 --- a/dgs-subscription-sse/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.example.demo - -import com.jayway.jsonpath.TypeRef -import com.netflix.graphql.dgs.DgsQueryExecutor -import com.netflix.graphql.dgs.client.SSESubscriptionGraphQLClient -import org.assertj.core.api.Assertions.assertThat -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 reactor.test.StepVerifier - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class DemoApplicationTestsWithGraphQLClient { - - @Autowired - lateinit var dgsQueryExecutor: DgsQueryExecutor - - @LocalServerPort - var port = 0; - - lateinit var client: SSESubscriptionGraphQLClient; - - @BeforeEach - fun setup() { - val baseUrl = "http://localhost:$port/subscriptions" - client = SSESubscriptionGraphQLClient( - baseUrl, - WebClient.create(baseUrl) - ) - } - - @Test - fun testMessages() { - //Hooks.onOperatorDebug(); - val query = "subscription { messageSent { body } }" - val variables = emptyMap() - val result = client.reactiveExecuteQuery(query, variables).map { - it.extractValueAsObject("data.messageSent", object : TypeRef>() {})["body"] as String - } - - val verifier = StepVerifier.create(result) - .consumeNextWith { assertThat(it).isEqualTo("text1 message") } - .consumeNextWith { assertThat(it).isEqualTo("text2 message") } - .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"); - - 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"); - - //verify it now. - verifier.verify(); - - val msgs = dgsQueryExecutor.executeAndExtractJsonPath>( - " { messages { body }}", - "data.messages[*].body" - ) - assertThat(msgs).allMatch { s: String -> - s.contains( - "message" - ) - } - } -} diff --git a/dgs-subscription-sse/src/test/kotlin/com/example/demo/IntegrationTests.kt b/dgs-subscription-sse/src/test/kotlin/com/example/demo/IntegrationTests.kt new file mode 100644 index 000000000..96a79faf1 --- /dev/null +++ b/dgs-subscription-sse/src/test/kotlin/com/example/demo/IntegrationTests.kt @@ -0,0 +1,92 @@ +package com.example.demo + +import com.jayway.jsonpath.TypeRef +import com.netflix.graphql.dgs.client.GraphqlSSESubscriptionGraphQLClient +import com.netflix.graphql.dgs.client.WebClientGraphQLClient +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.web.reactive.function.client.WebClient +import reactor.test.StepVerifier + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class IntegrationTests { + + @LocalServerPort + var port = 0; + + lateinit var sseClient: GraphqlSSESubscriptionGraphQLClient + lateinit var webClient: WebClientGraphQLClient + + @BeforeEach + fun setup() { + val baseUrl = "http://localhost:$port/graphql" + sseClient = GraphqlSSESubscriptionGraphQLClient( + baseUrl, + WebClient.create(baseUrl) + ) + webClient = WebClientGraphQLClient(WebClient.create(baseUrl)) + } + + @Test + fun testMessages() { + //Hooks.onOperatorDebug(); + val subscriptionQuery = "subscription { messageSent { body } }" + val variables = emptyMap() + val result = sseClient.reactiveExecuteQuery(subscriptionQuery, variables).map { + it.extractValueAsObject("data.messageSent", object : TypeRef>() {})["body"] as String + } + + val verifier = StepVerifier.create(result) + .consumeNextWith { assertThat(it).isEqualTo("text1 message") } + .consumeNextWith { assertThat(it).isEqualTo("text2 message") } + .thenCancel() + .verifyLater() + + val sendMessageQuery = "mutation sendMessage(\$msg: TextMessageInput!) { send(message:\$msg) { body}}" + webClient + .reactiveExecuteQuery( + sendMessageQuery, + mapOf("msg" to (mapOf("body" to "text1 message"))) + ) + .`as` { StepVerifier.create(it) } + .consumeNextWith { + assertThat(it.extractValueAsObject("data.send.body", String::class.java)) + .isEqualTo("text1 message") + } + .verifyComplete() + + webClient + .reactiveExecuteQuery( + sendMessageQuery, + mapOf("msg" to (mapOf("body" to "text2 message"))) + ) + .`as` { StepVerifier.create(it) } + .consumeNextWith { + assertThat(it.extractValueAsObject("data.send.body", String::class.java)) + .isEqualTo("text2 message") + } + .verifyComplete() + + //verify it now. + verifier.verify(); + + val allMessagesQuery = " { messages { body }}" + + webClient + .reactiveExecuteQuery( + allMessagesQuery, + emptyMap() + ) + .`as` { StepVerifier.create(it) } + .consumeNextWith { + assertThat(it.extractValueAsObject("data.messages[*].body", object : TypeRef>() {})) + .allMatch { s: String -> + s.contains("message") + } + } + .verifyComplete() + } +} diff --git a/dgs-subscription-sse/src/test/kotlin/com/example/demo/DemoApplicationTests.kt b/dgs-subscription-sse/src/test/kotlin/com/example/demo/SubscriptionTest.kt similarity index 83% rename from dgs-subscription-sse/src/test/kotlin/com/example/demo/DemoApplicationTests.kt rename to dgs-subscription-sse/src/test/kotlin/com/example/demo/SubscriptionTest.kt index 43c6363c9..21073a292 100644 --- a/dgs-subscription-sse/src/test/kotlin/com/example/demo/DemoApplicationTests.kt +++ b/dgs-subscription-sse/src/test/kotlin/com/example/demo/SubscriptionTest.kt @@ -1,6 +1,7 @@ package com.example.demo import com.netflix.graphql.dgs.DgsQueryExecutor +import com.netflix.graphql.dgs.test.EnableDgsTest import graphql.ExecutionResult import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -10,7 +11,8 @@ import org.springframework.boot.test.context.SpringBootTest import reactor.test.StepVerifier @SpringBootTest -class DemoApplicationTests { +@EnableDgsTest +class SubscriptionTest { @Autowired lateinit var dgsQueryExecutor: DgsQueryExecutor @@ -24,14 +26,14 @@ class DemoApplicationTests { val verifier = StepVerifier.create(publisher) .consumeNextWith { assertThat( - (it.getData>>()["messageSent"] - ?.get("body") as String) + it.getData>>()["messageSent"] + ?.get("body") as String ).contains("text1") } .consumeNextWith { assertThat( - (it.getData>>()["messageSent"] - ?.get("body") as String) + it.getData>>()["messageSent"] + ?.get("body") as String ).contains("text2") } .thenCancel() @@ -59,9 +61,7 @@ class DemoApplicationTests { "data.messages[*].body" ) assertThat(msgs).allMatch { s: String -> - s.contains( - "message" - ) + s.contains("message") } } } diff --git a/dgs-subscription-ws/build.gradle.kts b/dgs-subscription-ws/build.gradle.kts index 82e9b7b43..a0587e679 100644 --- a/dgs-subscription-ws/build.gradle.kts +++ b/dgs-subscription-ws/build.gradle.kts @@ -1,10 +1,6 @@ import com.netflix.graphql.dgs.codegen.gradle.GenerateJavaTask -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.jetbrains.kotlin.config.ApiVersion.Companion.KOTLIN_2_0 -import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion - plugins { id("org.springframework.boot") version "3.4.1" id("io.spring.dependency-management") version "1.1.7" @@ -14,15 +10,20 @@ plugins { id("com.netflix.dgs.codegen") version "7.0.3" //https://plugins.gradle.org/plugin/com.netflix.dgs.codegen } -// extra["graphql-java.version"] = "19.2" - group = "com.example" version = "0.0.1-SNAPSHOT" -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } +kotlin { + jvmToolchain(21) + compilerOptions { + apiVersion.set(KotlinVersion.KOTLIN_2_0) + languageVersion.set(KotlinVersion.KOTLIN_2_0) + freeCompilerArgs.addAll( + "-Xjsr305=strict", + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + ) + } } repositories { @@ -33,30 +34,28 @@ repositories { dependencyManagement { imports { - mavenBom("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:9.2.2") + mavenBom("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:10.0.1") } } dependencies { //implementation(platform("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:8.1.1")) - implementation("com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter") { - exclude("org.yaml", "snakeyaml") - } - implementation("com.netflix.graphql.dgs:graphql-dgs-subscriptions-websockets-autoconfigure") { - exclude("org.yaml", "snakeyaml") - } - implementation("org.yaml:snakeyaml:2.3") + implementation("com.netflix.graphql.dgs:dgs-starter") //Spring and kotlin implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-websocket") + implementation("io.projectreactor:reactor-core") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib") // test - testImplementation("com.netflix.graphql.dgs:graphql-dgs-client") - testImplementation("org.springframework:spring-webflux") - testImplementation("io.projectreactor.netty:reactor-netty") testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework:spring-webflux") // provides WebClient + testImplementation("io.projectreactor.netty:reactor-netty") + testImplementation("com.netflix.graphql.dgs:graphql-dgs-client") + testImplementation("com.netflix.graphql.dgs:dgs-starter-test") testImplementation("io.projectreactor:reactor-test") } @@ -70,19 +69,6 @@ tasks.withType { snakeCaseConstantNames = true } -kotlin { - compilerOptions { - apiVersion.set(KotlinVersion.KOTLIN_2_0) - languageVersion.set(KotlinVersion.KOTLIN_2_0) - jvmTarget.set(JvmTarget.fromTarget("21")) - freeCompilerArgs.addAll( - "-Xjsr305=strict", - "-opt-in=kotlin.RequiresOptIn", - "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" - ) - } -} - tasks.withType { useJUnitPlatform() } diff --git a/dgs-subscription-ws/src/main/resources/application.properties b/dgs-subscription-ws/src/main/resources/application.properties index 3a0443043..5c7e5ea79 100644 --- a/dgs-subscription-ws/src/main/resources/application.properties +++ b/dgs-subscription-ws/src/main/resources/application.properties @@ -3,4 +3,6 @@ logging.level.com.example=DEBUG logging.level.web=DEBUG logging.level.reactor.core.publisher=TRACE -spring.mvc.async.request-timeout=45000 +spring.graphql.websocket.path=/graphql +spring.graphql.websocket.connection-init-timeout=30s +spring.graphql.websocket.keep-alive=60s diff --git a/dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt b/dgs-subscription-ws/src/test/kotlin/com/example/demo/IntegrationTests.kt similarity index 56% rename from dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt rename to dgs-subscription-ws/src/test/kotlin/com/example/demo/IntegrationTests.kt index 6080f0808..c5280051c 100644 --- a/dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTestsWithGraphQLClient.kt +++ b/dgs-subscription-ws/src/test/kotlin/com/example/demo/IntegrationTests.kt @@ -2,31 +2,53 @@ package com.example.demo import com.jayway.jsonpath.TypeRef 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.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.platform.commons.logging.LoggerFactory import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.graphql.client.WebSocketGraphQlClient import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient import reactor.test.StepVerifier +import java.net.URI import java.time.Duration +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class DemoApplicationTestsWithGraphQLClient { - lateinit var webClientGraphQLClient: WebClientGraphQLClient - lateinit var socketGraphQLClient: WebSocketGraphQLClient +class IntegrationTests { + companion object { + private val log = LoggerFactory.getLogger(IntegrationTests::class.java) + } + lateinit var webClient: WebClientGraphQLClient + lateinit var socketClient: WebSocketGraphQlClient @LocalServerPort var port: Int = 0 @BeforeEach fun setup() { - this.webClientGraphQLClient = WebClientGraphQLClient(WebClient.create("http://localhost:$port/graphql")) - this.socketGraphQLClient = - WebSocketGraphQLClient("ws://localhost:$port/subscriptions", ReactorNettyWebSocketClient()) + val baseUrl = "http://localhost:$port/graphql" + this.webClient = WebClientGraphQLClient(WebClient.create(baseUrl)) + + val subscriptionUrl = "ws://localhost:$port/graphql" + this.socketClient = WebSocketGraphQlClient.create(URI.create(subscriptionUrl), ReactorNettyWebSocketClient()) + this.socketClient.start().subscribe() + // val latch = CountDownLatch(1) +// this.socketClient.start().doOnTerminate { latch.countDown() }.subscribe() +// latch.await(500, TimeUnit.MILLISECONDS) + } + + @AfterEach + fun teardown() { + this.socketClient.stop().subscribe() +// val latch = CountDownLatch(1) +// this.socketClient.stop().doOnTerminate { latch.countDown() }.subscribe() +// latch.await(500, TimeUnit.MILLISECONDS) } @Test @@ -38,19 +60,17 @@ class DemoApplicationTestsWithGraphQLClient { } } """.trimIndent() - val variables = emptyMap() - val executionResult = socketGraphQLClient.reactiveExecuteQuery(messageSentSubscriptionQuery, variables) + + val executionResult = socketClient.document(messageSentSubscriptionQuery).executeSubscription() .map { - it.extractValueAsObject( - "data.messageSent", - object : TypeRef>() {} - )["body"] as String + val messageSent = it.getData>()!!["messageSent"]!! as Map + messageSent["body"] as String } val message1 = "text1" val message2 = "text2" val verifier = StepVerifier.create(executionResult) - .thenAwait(Duration.ofMillis(1000)) // see: https://github.com/Netflix/dgs-framework/issues/657 + .thenAwait(Duration.ofMillis(1500)) // see: https://github.com/Netflix/dgs-framework/issues/657 .consumeNextWith { assertThat(it).isEqualTo(message1) } .consumeNextWith { assertThat(it).isEqualTo(message2) } .thenCancel() @@ -63,13 +83,13 @@ class DemoApplicationTestsWithGraphQLClient { } } """.trimIndent() - webClientGraphQLClient.reactiveExecuteQuery(sendMessageQuery, mapOf("msg" to (mapOf("body" to message1)))) + webClient.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() - webClientGraphQLClient.reactiveExecuteQuery(sendMessageQuery, mapOf("msg" to (mapOf("body" to message2)))) + webClient.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) } @@ -85,11 +105,10 @@ class DemoApplicationTestsWithGraphQLClient { } } """.trimIndent() - webClientGraphQLClient.reactiveExecuteQuery(allMessagesQuery) + webClient.reactiveExecuteQuery(allMessagesQuery) .map { it.extractValueAsObject("data.messages[*].body", object : TypeRef>() {}) } .`as` { StepVerifier.create(it) } - .consumeNextWith { assertThat(it).isEqualTo(message1) } - .consumeNextWith { assertThat(it).isEqualTo(message2) } + .consumeNextWith { assertThat(it).containsAll(listOf(message1, message2)) } .verifyComplete() } } diff --git a/dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTests.kt b/dgs-subscription-ws/src/test/kotlin/com/example/demo/SubscriptionTest.kt similarity index 77% rename from dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTests.kt rename to dgs-subscription-ws/src/test/kotlin/com/example/demo/SubscriptionTest.kt index 5414a1c25..c547b51e1 100644 --- a/dgs-subscription-ws/src/test/kotlin/com/example/demo/DemoApplicationTests.kt +++ b/dgs-subscription-ws/src/test/kotlin/com/example/demo/SubscriptionTest.kt @@ -1,18 +1,18 @@ package com.example.demo import com.netflix.graphql.dgs.DgsQueryExecutor +import com.netflix.graphql.dgs.test.EnableDgsTest import graphql.ExecutionResult -import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.reactivestreams.Publisher import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest -import reactor.core.publisher.Hooks import reactor.test.StepVerifier @SpringBootTest -class DemoApplicationTests { +@EnableDgsTest +class SubscriptionTest { @Autowired lateinit var dgsQueryExecutor: DgsQueryExecutor @@ -25,12 +25,12 @@ class DemoApplicationTests { val verifier = StepVerifier.create(publisher) .consumeNextWith { - assertThat((it.getData>>()["messageSent"] - ?.get("body") as String)).contains("text1") + assertThat(it.getData>>()["messageSent"] + ?.get("body") as String).contains("text1") } .consumeNextWith { - assertThat((it.getData>>()["messageSent"] - ?.get("body") as String)).contains("text2") + assertThat(it.getData>>()["messageSent"] + ?.get("body") as String).contains("text2") } .thenCancel() .verifyLater() @@ -56,10 +56,6 @@ class DemoApplicationTests { " { messages { body }}", "data.messages[*].body" ) - assertThat(msgs).allMatch { s: String -> - s.contains( - "message" - ) - } + assertThat(msgs).allMatch { s: String -> s.contains("message") } } } diff --git a/dgs-webflux/build.gradle b/dgs-webflux/build.gradle index 779060b35..b03647a06 100644 --- a/dgs-webflux/build.gradle +++ b/dgs-webflux/build.gradle @@ -18,21 +18,21 @@ java { repositories { mavenLocal() mavenCentral() - maven { url "https://repo.spring.io/milestone" } - maven { url "https://repo.spring.io/snapshot" } + maven { url 'https://repo.spring.io/milestone' } + maven { url 'https://repo.spring.io/snapshot' } } dependencyManagement { imports { - mavenBom("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:9.2.2") + mavenBom('com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:10.0.1') } } dependencies { //dgs - // implementation(platform("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:8.2.0")) - implementation "com.netflix.graphql.dgs:graphql-dgs-webflux-starter" - implementation "io.projectreactor:reactor-core:3.7.1" + // implementation(platform('com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:8.2.0')) + implementation 'com.netflix.graphql.dgs:dgs-starter' + implementation 'io.projectreactor:reactor-core' // spring boot implementation 'org.springframework.boot:spring-boot-starter-webflux' @@ -50,6 +50,7 @@ dependencies { //test testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'com.netflix.graphql.dgs:dgs-starter-test' testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.awaitility:awaitility:4.2.2' } diff --git a/dgs-webflux/src/main/resources/application.properties b/dgs-webflux/src/main/resources/application.properties index ec3862411..24e69c4c2 100644 --- a/dgs-webflux/src/main/resources/application.properties +++ b/dgs-webflux/src/main/resources/application.properties @@ -6,4 +6,7 @@ spring.sql.init.mode=always logging.level.root=INFO logging.level.sql=DEBUG logging.level.web=DEBUG -logging.level.com.example=DEBUG \ No newline at end of file +logging.level.com.example=DEBUG +logging.level.org.springframework.graphql=TRACE + +spring.graphql.websocket.path=/graphql diff --git a/dgs-webflux/src/main/resources/schema/schema.graphql b/dgs-webflux/src/main/resources/schema/schema.graphql index 430cace6b..e1e52c952 100644 --- a/dgs-webflux/src/main/resources/schema/schema.graphql +++ b/dgs-webflux/src/main/resources/schema/schema.graphql @@ -24,7 +24,7 @@ input CreatePostInput { } input CommentInput{ - postId: String! + postId: ID! content: String! } diff --git a/dgs-webflux/src/test/java/com/example/demo/SubscriptionTestsWithGraphQLClient.java b/dgs-webflux/src/test/java/com/example/demo/IntegrationTests.java similarity index 83% rename from dgs-webflux/src/test/java/com/example/demo/SubscriptionTestsWithGraphQLClient.java rename to dgs-webflux/src/test/java/com/example/demo/IntegrationTests.java index cdeb51522..010d3b483 100644 --- a/dgs-webflux/src/test/java/com/example/demo/SubscriptionTestsWithGraphQLClient.java +++ b/dgs-webflux/src/test/java/com/example/demo/IntegrationTests.java @@ -3,46 +3,53 @@ import com.example.demo.gql.types.Comment; import com.example.demo.gql.types.Post; import com.netflix.graphql.dgs.client.WebClientGraphQLClient; -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.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.graphql.GraphQlResponse; +import org.springframework.graphql.client.WebSocketGraphQlClient; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; import reactor.test.StepVerifier; +import java.net.URI; import java.time.Duration; -import java.util.Collections; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Slf4j //@Disabled //see: https://github.com/Netflix/dgs-framework/issues/689 -class SubscriptionTestsWithGraphQLClient { +class IntegrationTests { @LocalServerPort int port; private WebClientGraphQLClient client; - private WebSocketGraphQLClient socketClient; + private WebSocketGraphQlClient socketClient; @BeforeEach public void setup() { var webClient = WebClient.create("http://localhost:" + port + "/graphql"); this.client = new WebClientGraphQLClient(webClient); - this.socketClient = new WebSocketGraphQLClient("ws://localhost:" + port + "/subscriptions", new ReactorNettyWebSocketClient()); + this.socketClient = WebSocketGraphQlClient.create(URI.create("ws://localhost:" + port + "/graphql"), new ReactorNettyWebSocketClient()); + this.socketClient.start().subscribe(); + } + + @AfterEach + public void teardown() { + this.socketClient.stop().subscribe(); } @SneakyThrows @@ -74,7 +81,7 @@ mutation createPost($input: CreatePostInput!){ postIdHolder.set(id); countDownLatch.countDown(); }); - countDownLatch.await(5, SECONDS); + countDownLatch.await(1000, TimeUnit.MILLISECONDS); log.debug("created post:{}", createPostResult); Long postId = postIdHolder.get(); @@ -90,18 +97,18 @@ mutation createPost($input: CreatePostInput!){ } } """.stripIndent(); - var executionResultPublisher = this.socketClient - .reactiveExecuteQuery(subscriptionQuery, Collections.emptyMap()); - var commentAddedDataPublisher = executionResultPublisher - .map(it -> it.extractValueAsObject("commentAdded", Comment.class)); + var commentAddedDataPublisher = this.socketClient + .document(subscriptionQuery).executeSubscription() + .mapNotNull(GraphQlResponse::>>getData) + .map(it -> it.get("commentAdded")); // 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)) + .consumeNextWith(comment -> assertThat(comment.get("content")).isEqualTo(comment1)) + .consumeNextWith(comment -> assertThat(comment.get("content")).isEqualTo(comment2)) .thenCancel() .verifyLater(); 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 9d39fd373..e867f8a9e 100644 --- a/dgs-webflux/src/test/java/com/example/demo/MutationTests.java +++ b/dgs-webflux/src/test/java/com/example/demo/MutationTests.java @@ -5,9 +5,8 @@ import com.example.demo.gql.types.Post; import com.example.demo.service.AuthorService; import com.example.demo.service.PostService; -import com.netflix.graphql.dgs.autoconfig.DgsAutoConfiguration; import com.netflix.graphql.dgs.reactive.DgsReactiveQueryExecutor; -import com.netflix.graphql.dgs.webflux.autoconfiguration.DgsWebFluxAutoConfiguration; +import com.netflix.graphql.dgs.test.EnableDgsTest; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -27,15 +26,14 @@ import static org.mockito.Mockito.*; @SpringBootTest(classes = MutationTests.MutationTestsConfig.class) +@EnableDgsTest @Slf4j class MutationTests { @Configuration @Import(PostsDataFetcher.class) @ImportAutoConfiguration(classes = { - DgsWebFluxAutoConfiguration.class, WebFluxAutoConfiguration.class, - DgsAutoConfiguration.class, JacksonAutoConfiguration.class }) static class MutationTestsConfig { 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 c31c75da1..dac2bb787 100644 --- a/dgs-webflux/src/test/java/com/example/demo/QueryTests.java +++ b/dgs-webflux/src/test/java/com/example/demo/QueryTests.java @@ -5,9 +5,8 @@ import com.example.demo.service.AuthorService; import com.example.demo.service.PostNotFoundException; import com.example.demo.service.PostService; -import com.netflix.graphql.dgs.autoconfig.DgsAutoConfiguration; import com.netflix.graphql.dgs.reactive.DgsReactiveQueryExecutor; -import com.netflix.graphql.dgs.webflux.autoconfiguration.DgsWebFluxAutoConfiguration; +import com.netflix.graphql.dgs.test.EnableDgsTest; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -15,7 +14,6 @@ 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; @@ -30,6 +28,7 @@ import static org.mockito.Mockito.*; @SpringBootTest(classes = {QueryTests.QueryTestsConfig.class}) +@EnableDgsTest @Slf4j class QueryTests { @@ -45,9 +44,7 @@ class QueryTests { @Configuration @Import(PostsDataFetcher.class) @ImportAutoConfiguration(classes = { - DgsWebFluxAutoConfiguration.class, WebFluxAutoConfiguration.class, - DgsAutoConfiguration.class, JacksonAutoConfiguration.class }) static class QueryTestsConfig { 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 73396fdd7..cd57b27e1 100644 --- a/dgs-webflux/src/test/java/com/example/demo/SubscriptionTests.java +++ b/dgs-webflux/src/test/java/com/example/demo/SubscriptionTests.java @@ -11,9 +11,8 @@ import com.example.demo.service.AuthorService; import com.example.demo.service.PostService; import com.fasterxml.jackson.databind.ObjectMapper; -import com.netflix.graphql.dgs.autoconfig.DgsAutoConfiguration; import com.netflix.graphql.dgs.reactive.DgsReactiveQueryExecutor; -import com.netflix.graphql.dgs.webflux.autoconfiguration.DgsWebFluxAutoConfiguration; +import com.netflix.graphql.dgs.test.EnableDgsTest; import graphql.ExecutionResult; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -43,6 +42,7 @@ @SpringBootTest(classes = SubscriptionTests.SubscriptionTestsConfig.class) @Slf4j +@EnableDgsTest //@Disabled // see: https://github.com/Netflix/dgs-framework/discussions/605 class SubscriptionTests { @@ -50,9 +50,7 @@ class SubscriptionTests { @Configuration @Import({PostsDataFetcher.class, PostService.class}) @ImportAutoConfiguration(classes = { - DgsWebFluxAutoConfiguration.class, WebFluxAutoConfiguration.class, - DgsAutoConfiguration.class, JacksonAutoConfiguration.class }) static class SubscriptionTestsConfig { diff --git a/dgs/build.gradle b/dgs/build.gradle index 77ce5aacb..21c050d25 100644 --- a/dgs/build.gradle +++ b/dgs/build.gradle @@ -24,12 +24,12 @@ repositories { dependencyManagement { imports { - mavenBom('com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:9.2.2') + mavenBom('com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:10.0.1') } } dependencies { - implementation 'com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter' + implementation 'com.netflix.graphql.dgs:dgs-starter' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' @@ -42,7 +42,7 @@ dependencies { testCompileOnly 'org.projectlombok:lombok:1.18.36' testAnnotationProcessor 'org.projectlombok:lombok:1.18.36' testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'com.netflix.graphql.dgs:graphql-dgs-spring-graphql-starter-test' + testImplementation 'com.netflix.graphql.dgs:dgs-starter-test' } test { diff --git a/dgs/src/test/java/com/example/demo/MutationTests.java b/dgs/src/test/java/com/example/demo/MutationTests.java index 10f71cfc0..b78d5ea0e 100644 --- a/dgs/src/test/java/com/example/demo/MutationTests.java +++ b/dgs/src/test/java/com/example/demo/MutationTests.java @@ -6,32 +6,27 @@ import com.example.demo.service.AuthorService; import com.example.demo.service.PostService; import com.netflix.graphql.dgs.DgsQueryExecutor; -import com.netflix.graphql.dgs.autoconfig.DgsAutoConfiguration; +import com.netflix.graphql.dgs.test.EnableDgsTest; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; 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 java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; -@SpringBootTest(classes = MutationTests.MutationTestsConfig.class) +@EnableDgsTest +@SpringBootTest(classes = {MutationTests.MutationTestsConfig.class}) @Slf4j class MutationTests { @Configuration @Import(PostsDataFetcher.class) - @ImportAutoConfiguration(classes = { - DgsAutoConfiguration.class, - JacksonAutoConfiguration.class - }) static class MutationTestsConfig { } @@ -39,16 +34,16 @@ static class MutationTestsConfig { @Autowired DgsQueryExecutor dgsQueryExecutor; - @MockBean - PostService postService; + @MockitoBean + PostService mockedPostService; - @MockBean + @MockitoBean AuthorService authorService; @Test void createPosts() { - when(postService.createPost(any(CreatePostInput.class))).thenReturn(1L); - when(postService.getPostById(anyLong())).thenReturn( + when(mockedPostService.createPost(any(CreatePostInput.class))).thenReturn(1L); + when(mockedPostService.getPostById(anyLong())).thenReturn( Post.builder().id(1L) .title("test title") .content("test content") @@ -74,9 +69,9 @@ mutation createPost($input: CreatePostInput!){ assertThat(title).isEqualTo("test title"); - verify(postService, times(1)).createPost(any(CreatePostInput.class)); - verify(postService, times(1)).getPostById(anyLong()); - verifyNoMoreInteractions(postService); + verify(mockedPostService, times(1)).createPost(any(CreatePostInput.class)); + verify(mockedPostService, times(1)).getPostById(anyLong()); + verifyNoMoreInteractions(mockedPostService); } } diff --git a/dgs/src/test/java/com/example/demo/QueryTests.java b/dgs/src/test/java/com/example/demo/QueryTests.java index dd2bc0b99..70d12dbb7 100644 --- a/dgs/src/test/java/com/example/demo/QueryTests.java +++ b/dgs/src/test/java/com/example/demo/QueryTests.java @@ -6,16 +6,14 @@ import com.example.demo.service.PostNotFoundException; import com.example.demo.service.PostService; import com.netflix.graphql.dgs.DgsQueryExecutor; -import com.netflix.graphql.dgs.autoconfig.DgsAutoConfiguration; +import com.netflix.graphql.dgs.test.EnableDgsTest; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; 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 java.util.List; import java.util.Map; @@ -23,6 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; +@EnableDgsTest @SpringBootTest(classes = {QueryTests.QueryTestsConfig.class}) @Slf4j class QueryTests { @@ -30,18 +29,14 @@ class QueryTests { @Autowired DgsQueryExecutor dgsQueryExecutor; - @MockBean + @MockitoBean PostService postService; - @MockBean + @MockitoBean AuthorService authorService; @Configuration @Import(PostsDataFetcher.class) - @ImportAutoConfiguration(classes = { - DgsAutoConfiguration.class, - JacksonAutoConfiguration.class - }) static class QueryTestsConfig { } diff --git a/docker-compose.yml b/docker-compose.yml index 01e96e81c..49b93bad0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,3 @@ -# see https://docs.docker.com/compose/compose-file/compose-versioning/ -version: "3.5" # specify docker compose version, v3.5 is compatible with docker 17.12.0+ - # Define the services/containers to be run services: postgres: