diff --git a/.github/project.yml b/.github/project.yml index bfe950f37..0f7f3e0ca 100644 --- a/.github/project.yml +++ b/.github/project.yml @@ -1,4 +1,4 @@ name: SmallRye GraphQL release: - current-version: 2.0.0 - next-version: 2.0.1-SNAPSHOT + current-version: 2.0.1 + next-version: 2.0.2-SNAPSHOT diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 284918059..da9918465 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,6 +33,7 @@ jobs: with: java-version: ${{matrix.java}} distribution: 'temurin' + cache: 'maven' - name: Install graphviz run: sudo apt install graphviz @@ -58,19 +59,12 @@ jobs: ref: main path: wildfly-graphql-feature-pack - - name: Cache local Maven repository - uses: actions/cache@v2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - uses: actions/setup-java@v3.0.0 name: set up jdk ${{matrix.java}} with: java-version: ${{matrix.java}} distribution: 'temurin' + cache: 'maven' - name: build with maven run: mvn -B install -DskipTests --file pom.xml @@ -104,19 +98,12 @@ jobs: ref: jakarta-rewrite path: quarkus - - name: Cache local Maven repository - uses: actions/cache@v2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - uses: actions/setup-java@v3.0.0 name: set up jdk ${{matrix.java}} with: java-version: ${{matrix.java}} distribution: 'temurin' + cache: 'maven' - name: build with maven run: mvn -B install -DskipTests --file smallrye-graphql/pom.xml @@ -145,6 +132,7 @@ jobs: with: java-version: 11 distribution: 'temurin' + cache: 'maven' - name: sonar env: diff --git a/client/api/pom.xml b/client/api/pom.xml index 9e4dd282b..89512fdd6 100644 --- a/client/api/pom.xml +++ b/client/api/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-client-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-client-api diff --git a/client/generator-test/pom.xml b/client/generator-test/pom.xml index 2e5f11db0..429fa6f76 100644 --- a/client/generator-test/pom.xml +++ b/client/generator-test/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-client-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-client-generator-test diff --git a/client/generator/pom.xml b/client/generator/pom.xml index d8b8e4b86..5d84a79af 100644 --- a/client/generator/pom.xml +++ b/client/generator/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-client-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-client-generator diff --git a/client/implementation-vertx/pom.xml b/client/implementation-vertx/pom.xml index 9a2677773..f4943e0a5 100644 --- a/client/implementation-vertx/pom.xml +++ b/client/implementation-vertx/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-client-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-client-implementation-vertx diff --git a/client/implementation-vertx/src/test/java/io/smallrye/graphql/client/vertx/test/ssl/DynamicClientSSLTest.java b/client/implementation-vertx/src/test/java/io/smallrye/graphql/client/vertx/test/ssl/DynamicClientSSLTest.java index 78ba46ac0..ba46ca245 100644 --- a/client/implementation-vertx/src/test/java/io/smallrye/graphql/client/vertx/test/ssl/DynamicClientSSLTest.java +++ b/client/implementation-vertx/src/test/java/io/smallrye/graphql/client/vertx/test/ssl/DynamicClientSSLTest.java @@ -13,6 +13,7 @@ public class DynamicClientSSLTest { + public static final Duration TIMEOUT = Duration.ofSeconds(20); private SSLTestingTools tools = new SSLTestingTools(); /** @@ -25,12 +26,12 @@ public void serverAuthentication_correctTruststore() throws Exception { System.setProperty("myclient1/mp-graphql/truststore", "classpath:ssl/client.pkcs12.truststore"); System.setProperty("myclient1/mp-graphql/truststorePassword", "clienttruststorepassword"); System.setProperty("myclient1/mp-graphql/truststoreType", "PKCS12"); - DynamicGraphQLClient client = DynamicGraphQLClientBuilder.newBuilder() + try (DynamicGraphQLClient client = DynamicGraphQLClientBuilder.newBuilder() .configKey("myclient1") .url("https://127.0.0.1:" + server.actualPort()) - .build(); - client.executeAsync("asd").await().atMost(Duration.ofSeconds(1)); - client.close(); + .build()) { + client.executeAsync("asd").await().atMost(TIMEOUT); + } } finally { server.close(); } @@ -47,21 +48,15 @@ public void serverAuthentication_badKeystoreOnServer() throws Exception { System.setProperty("myclient2/mp-graphql/truststore", "classpath:ssl/client.pkcs12.truststore"); System.setProperty("myclient2/mp-graphql/truststorePassword", "clienttruststorepassword"); System.setProperty("myclient2/mp-graphql/truststoreType", "PKCS12"); - DynamicGraphQLClient client = null; - try { - client = DynamicGraphQLClientBuilder.newBuilder() - .configKey("myclient2") - .url("https://127.0.0.1:" + server.actualPort()) - .build(); - client.executeAsync("asd").await().atMost(Duration.ofSeconds(1)); + try (DynamicGraphQLClient client = DynamicGraphQLClientBuilder.newBuilder() + .configKey("myclient2") + .url("https://127.0.0.1:" + server.actualPort()) + .build()) { + client.executeAsync("asd").await().atMost(TIMEOUT); Assertions.fail("Connection to server should fail"); } catch (Exception e) { // verify that the client rejected the server's certificate assertHasCauseContainingMessage(e, "unable to find valid certification path to requested target"); - } finally { - if (client != null) { - client.close(); - } } } finally { server.close(); @@ -81,13 +76,13 @@ public void clientAuthentication_correctKeystore() throws Exception { System.setProperty("myclient3/mp-graphql/keystoreType", "PKCS12"); WebClientOptions options = new WebClientOptions(); options.setTrustAll(true); // don't require server auth - DynamicGraphQLClient client = new VertxDynamicGraphQLClientBuilder() + try (DynamicGraphQLClient client = new VertxDynamicGraphQLClientBuilder() .configKey("myclient3") .options(options) .url("https://127.0.0.1:" + server.actualPort()) - .build(); - client.executeAsync("asd").await().atMost(Duration.ofSeconds(1)); - client.close(); + .build()) { + client.executeAsync("asd").await().atMost(TIMEOUT); + } } finally { server.close(); } @@ -106,22 +101,16 @@ public void clientAuthentication_badKeystore() throws Exception { System.setProperty("myclient4/mp-graphql/keystoreType", "PKCS12"); WebClientOptions options = new WebClientOptions(); options.setTrustAll(true); // don't require server auth - DynamicGraphQLClient client = null; - try { - client = new VertxDynamicGraphQLClientBuilder() - .configKey("myclient4") - .options(options) - .url("https://127.0.0.1:" + server.actualPort()) - .build(); - client.executeAsync("asd").await().atMost(Duration.ofSeconds(1)); + try (DynamicGraphQLClient client = new VertxDynamicGraphQLClientBuilder() + .configKey("myclient4") + .options(options) + .url("https://127.0.0.1:" + server.actualPort()) + .build()) { + client.executeAsync("asd").await().atMost(TIMEOUT); Assertions.fail("Connection to server should fail"); } catch (Exception e) { // verify that the server rejected the client's certificate assertHasCauseContainingMessage(e, "Received fatal alert: bad_certificate"); - } finally { - if (client != null) { - client.close(); - } } } finally { server.close(); diff --git a/client/implementation/pom.xml b/client/implementation/pom.xml index b57b38cf7..10ef213bc 100644 --- a/client/implementation/pom.xml +++ b/client/implementation/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-client-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-client diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/core/utils/ValueFormatter.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/core/utils/ValueFormatter.java index e0d810cf3..924627bb8 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/core/utils/ValueFormatter.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/core/utils/ValueFormatter.java @@ -2,6 +2,7 @@ import java.lang.reflect.Array; import java.time.LocalDate; +import java.util.UUID; import io.smallrye.graphql.client.core.exceptions.BuildException; import io.smallrye.graphql.client.impl.core.EnumImpl; @@ -10,6 +11,18 @@ public class ValueFormatter { + private final static Class[] QUOTED_VALUES = new Class[] { String.class, Character.class, LocalDate.class, UUID.class }; + private final static Class[] UNQUOTED_VALUES = new Class[] { Number.class, Boolean.class }; + + public static boolean assignableFrom(Class clazz, Class[] candidates) { + for (Class candidate : candidates) { + if (candidate.isAssignableFrom(clazz)) { + return true; + } + } + return false; + } + public static String format(Object value) throws BuildException { if (value == null) { return "null"; @@ -24,17 +37,36 @@ public static String format(Object value) throws BuildException { return gqlEnum.build(); } else if (value.getClass().isArray()) { return _processArray(value); - } else if (value instanceof String) { - return _getAsQuotedString(String.valueOf(value)); - } else if (value instanceof Character) { - return _getAsQuotedString(String.valueOf(value)); - } else if (value instanceof LocalDate) { - return _getAsQuotedString(String.valueOf(value)); + } else if (value instanceof Iterable) { + return _processIterable((Iterable) value); } else { - return value.toString(); + if (assignableFrom(value.getClass(), QUOTED_VALUES)) { + return _getAsQuotedString(String.valueOf(value)); + } else if (assignableFrom(value.getClass(), UNQUOTED_VALUES)) { + return value.toString(); + } + throw new IllegalStateException("Could not format " + value.getClass() + ": Unsupported type."); } } + private static String _processIterable(Iterable iterable) throws BuildException { + StringBuilder builder = new StringBuilder(); + + boolean first = true; + builder.append("["); + for (Object v : iterable) { + if (first) { + first = false; + } else { + builder.append(","); + } + builder.append(format(v)); + } + builder.append("]"); + + return builder.toString(); + } + private static String _processArray(Object array) throws BuildException { StringBuilder builder = new StringBuilder(); diff --git a/client/implementation/src/test/java/io/smallrye/graphql/client/impl/core/utils/ValueFormatterTest.java b/client/implementation/src/test/java/io/smallrye/graphql/client/impl/core/utils/ValueFormatterTest.java new file mode 100644 index 000000000..2b5af4897 --- /dev/null +++ b/client/implementation/src/test/java/io/smallrye/graphql/client/impl/core/utils/ValueFormatterTest.java @@ -0,0 +1,28 @@ +package io.smallrye.graphql.client.impl.core.utils; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Date; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +class ValueFormatterTest { + + @Test + public void testUnsupportedInput() { + + assertThrows(IllegalStateException.class, () -> { + ValueFormatter.format(new Object()); + }); + + assertThrows(IllegalStateException.class, () -> { + ValueFormatter.format(Map.of("test", "value")); + }); + + assertThrows(IllegalStateException.class, () -> { + ValueFormatter.format(new Date()); + }); + } + +} diff --git a/client/pom.xml b/client/pom.xml index 51d56ece0..539af5262 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-client-parent diff --git a/client/tck/pom.xml b/client/tck/pom.xml index d6cc50617..ed1574d61 100644 --- a/client/tck/pom.xml +++ b/client/tck/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-client-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-client-tck diff --git a/client/tck/src/main/java/tck/graphql/dynamic/core/ArraysTest.java b/client/tck/src/main/java/tck/graphql/dynamic/core/ArraysTest.java index aeab064ee..2c93a7179 100644 --- a/client/tck/src/main/java/tck/graphql/dynamic/core/ArraysTest.java +++ b/client/tck/src/main/java/tck/graphql/dynamic/core/ArraysTest.java @@ -13,6 +13,7 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.net.URISyntaxException; +import java.util.UUID; import org.junit.jupiter.api.Test; @@ -61,7 +62,11 @@ public void arraysTest() throws IOException, URISyntaxException { prop("charPrimitiveArray", new char[] { 'f', 'o', 'o' }), prop("charObjectArray", new Character[] { 'f', 'o', 'o' }), - prop("stringArray", new String[] { "foo", "bar", "baz" })))), + prop("stringArray", new String[] { "foo", "bar", "baz" }), + + prop("uuidArray", + new UUID[] { UUID.fromString("fc4bb4f4-13fe-4908-8d6a-afa64f1b56c9"), + UUID.fromString("863c9e3c-7538-41b9-9d63-0852f6a50815") })))), field("boolPrimitiveArray"), field("boolObjectArray"), @@ -89,7 +94,9 @@ public void arraysTest() throws IOException, URISyntaxException { field("charPrimitiveArray"), field("charObjectArray"), - field("stringArray")))); + field("stringArray"), + + field("uuidArray")))); String generatedRequest = document.build(); AssertGraphQL.assertEquivalentGraphQLRequest(expectedRequest, generatedRequest); diff --git a/client/tck/src/main/java/tck/graphql/dynamic/core/IterableTest.java b/client/tck/src/main/java/tck/graphql/dynamic/core/IterableTest.java new file mode 100644 index 000000000..5a45dfcd0 --- /dev/null +++ b/client/tck/src/main/java/tck/graphql/dynamic/core/IterableTest.java @@ -0,0 +1,93 @@ +package tck.graphql.dynamic.core; + +import static io.smallrye.graphql.client.core.Argument.arg; +import static io.smallrye.graphql.client.core.Argument.args; +import static io.smallrye.graphql.client.core.Document.document; +import static io.smallrye.graphql.client.core.Field.field; +import static io.smallrye.graphql.client.core.InputObject.inputObject; +import static io.smallrye.graphql.client.core.InputObjectField.prop; +import static io.smallrye.graphql.client.core.Operation.operation; +import static io.smallrye.graphql.client.core.OperationType.QUERY; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URISyntaxException; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import io.smallrye.graphql.client.core.Document; +import tck.graphql.dynamic.helper.AssertGraphQL; +import tck.graphql.dynamic.helper.Utils; + +public class IterableTest { + + @Test + public void iterableTest() throws IOException, URISyntaxException { + String expectedRequest = Utils.getResourceFileContent("core/iterable.graphql"); + + Document document = document( + operation(QUERY, "iterableHolderQuery", + field("iterableHolder", + args( + arg("iterableHolder", inputObject( + + prop("boolObjectList", List.of(true, false)), + + prop("byteObjectList", List.of((byte) 0, (byte) 2, (byte) 3)), + + prop("shortObjectList", List.of((short) 78, (short) 789, (short) 645)), + + prop("intObjectList", List.of(78, 65, 12354)), + + prop("longObjectList", List.of(789L, 947894L, 1874448L)), + + prop("floatObjectList", List.of(1567.654f, 8765f, 123789456.1851f)), + + prop("doubleObjectList", List.of(789.3242d, 1815d, 98765421.654897d)), + + prop("bigIntegerList", + List.of(BigInteger.ZERO, BigInteger.ONE, BigInteger.TEN)), + + prop("bigDecimalList", + List.of(BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN)), + + prop("charObjectList", List.of('f', 'o', 'o')), + + prop("stringList", List.of("foo", "bar", "baz")), + + prop("uuidList", + List.of(UUID.fromString("fc4bb4f4-13fe-4908-8d6a-afa64f1b56c9"), + UUID.fromString("863c9e3c-7538-41b9-9d63-0852f6a50815"))) + + ))), + field("boolObjectList"), + + field("byteObjectList"), + + field("shortObjectList"), + + field("intObjectList"), + + field("longObjectList"), + + field("floatObjectList"), + + field("doubleObjectList"), + + field("bigIntegerList"), + + field("bigDecimalList"), + + field("charObjectList"), + + field("stringList"), + + field("uuidList")))); + + String generatedRequest = document.build(); + AssertGraphQL.assertEquivalentGraphQLRequest(expectedRequest, generatedRequest); + } +} diff --git a/client/tck/src/main/resources/core/arrays.graphql b/client/tck/src/main/resources/core/arrays.graphql index 4b306d724..c35c0e521 100644 --- a/client/tck/src/main/resources/core/arrays.graphql +++ b/client/tck/src/main/resources/core/arrays.graphql @@ -28,6 +28,8 @@ query arrayHolderQuery { charObjectArray: ["f", "o", "o"], stringArray: ["foo", "bar", "baz"], + + uuidArray: ["fc4bb4f4-13fe-4908-8d6a-afa64f1b56c9", "863c9e3c-7538-41b9-9d63-0852f6a50815"] }) { boolPrimitiveArray boolObjectArray @@ -57,5 +59,7 @@ query arrayHolderQuery { charObjectArray stringArray + + uuidArray } } \ No newline at end of file diff --git a/client/tck/src/main/resources/core/iterable.graphql b/client/tck/src/main/resources/core/iterable.graphql new file mode 100644 index 000000000..a1265df45 --- /dev/null +++ b/client/tck/src/main/resources/core/iterable.graphql @@ -0,0 +1,53 @@ +query iterableHolderQuery { + iterableHolder(iterableHolder:{ + + boolObjectList: [true, false] + + byteObjectList: [0, 2, 3] + + shortObjectList: [78, 789, 645] + + intObjectList: [78, 65, 12354] + + longObjectList: [789, 947894, 1874448] + + floatObjectList: [1567.654, 8765.0, 1.23789456E8] + + doubleObjectList: [789.3242, 1815.0, 9.8765421654897E7] + + bigIntegerList: [0, 1, 10] + + bigDecimalList: [0, 1, 10] + + charObjectList: ["f", "o", "o"] + + stringList: ["foo", "bar", "baz"] + + uuidList: ["fc4bb4f4-13fe-4908-8d6a-afa64f1b56c9", "863c9e3c-7538-41b9-9d63-0852f6a50815"] + + }) { + boolObjectList + + byteObjectList + + shortObjectList + + intObjectList + + longObjectList + + floatObjectList + + doubleObjectList + + bigIntegerList + + bigDecimalList + + charObjectList + + stringList + + uuidList + } +} \ No newline at end of file diff --git a/common/pom.xml b/common/pom.xml index aacab4f99..8dc9ad235 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-common-parent diff --git a/common/schema-builder/pom.xml b/common/schema-builder/pom.xml index e89f94568..86c222691 100644 --- a/common/schema-builder/pom.xml +++ b/common/schema-builder/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-common-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-schema-builder diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java index a0c88dc72..25f2636ff 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java @@ -9,6 +9,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; @@ -76,7 +77,7 @@ private static Map getParentAnnotations(FieldInfo f private static Map getParentAnnotations(ClassInfo classInfo) { Map parentAnnotations = new HashMap<>(); - for (AnnotationInstance classAnnotation : classInfo.classAnnotations()) { + for (AnnotationInstance classAnnotation : classInfo.declaredAnnotations()) { parentAnnotations.putIfAbsent(classAnnotation.name(), classAnnotation); } @@ -95,7 +96,7 @@ private static Map getPackageAnnotations(ClassInfo if (packageName != null) { ClassInfo packageInfo = ScanningContext.getIndex().getClassByName(packageName); if (packageInfo != null) { - for (AnnotationInstance packageAnnotation : packageInfo.classAnnotations()) { + for (AnnotationInstance packageAnnotation : packageInfo.declaredAnnotations()) { packageAnnotations.putIfAbsent(packageAnnotation.name(), packageAnnotation); } } @@ -178,7 +179,7 @@ public static Annotations getAnnotationsForClass(ClassInfo classInfo) { Map annotationMap = new HashMap<>(); - for (AnnotationInstance annotationInstance : classInfo.classAnnotations()) { + for (AnnotationInstance annotationInstance : classInfo.declaredAnnotations()) { DotName name = annotationInstance.name(); annotationMap.put(name, annotationInstance); } @@ -390,6 +391,25 @@ public Optional getOneOfTheseMethodParameterAnnotationsValue(DotName... return Optional.empty(); } + /** + * Get a stream of that annotation, maybe empty if not present, maybe a stream of one, or maybe several, if it's repeatable. + */ + public Stream resolve(DotName name) { + var annotationInstance = annotationsMap.get(name); + if (annotationInstance == null) { + var repeatableType = ScanningContext.getIndex().getClassByName(name); + if (repeatableType.hasAnnotation(REPEATABLE)) { + DotName containerName = repeatableType.annotation(REPEATABLE).value().asClass().name(); + AnnotationInstance containerAnnotation = annotationsMap.get(containerName); + if (containerAnnotation != null) { + return Stream.of(containerAnnotation.value().asNestedArray()); + } + } + return Stream.of(); + } + return Stream.of(annotationInstance); + } + @Override public String toString() { return annotationsMap.toString(); @@ -558,6 +578,8 @@ private static Map getAnnotationsWithFilter(org.jbo private static final short ZERO = 0; + public static final DotName REPEATABLE = DotName.createSimple("java.lang.annotation.Repeatable"); + // SmallRye Common Annotations public static final DotName BLOCKING = DotName.createSimple("io.smallrye.common.annotation.Blocking"); public static final DotName NON_BLOCKING = DotName.createSimple("io.smallrye.common.annotation.NonBlocking"); @@ -629,5 +651,4 @@ private static Map getAnnotationsWithFilter(org.jbo //Kotlin NotNull public static final DotName KOTLIN_NOT_NULL = DotName.createSimple("org.jetbrains.annotations.NotNull"); - } diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/DirectiveTypeCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/DirectiveTypeCreator.java index 630766edb..f60ceb07c 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/DirectiveTypeCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/DirectiveTypeCreator.java @@ -34,7 +34,8 @@ public DirectiveType create(ClassInfo classInfo) { directiveType.setClassName(classInfo.name().toString()); directiveType.setName(toDirectiveName(classInfo, annotations)); directiveType.setDescription(DescriptionHelper.getDescriptionForType(annotations).orElse(null)); - directiveType.setLocations(getLocations(classInfo.classAnnotation(DIRECTIVE))); + directiveType.setLocations(getLocations(classInfo.declaredAnnotation(DIRECTIVE))); + directiveType.setRepeatable(classInfo.hasAnnotation(Annotations.REPEATABLE)); for (MethodInfo method : classInfo.methods()) { DirectiveArgument argument = new DirectiveArgument(); diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ModelCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ModelCreator.java index 7b27d67e8..7c07851b8 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ModelCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ModelCreator.java @@ -53,10 +53,7 @@ protected static Type getReturnType(MethodInfo methodInfo) { } /** - * The the return type.This is usually the method return type, but can also be adapted to something else - * - * @param fieldInfo - * @return the return type + * The return type. This is usually the method return type, but can also be adapted to something else */ protected static Type getReturnType(FieldInfo fieldInfo) { return fieldInfo.type(); @@ -100,8 +97,7 @@ private void doPopulateField(Direction direction, Field field, Type type, Annota // Directives if (directives != null) { // this happens while scanning for the directive types - field.addDirectiveInstances( - directives.buildDirectiveInstances(name -> annotations.getOneOfTheseAnnotations(name).orElse(null))); + field.addDirectiveInstances(directives.buildDirectiveInstances(annotations)); } } } diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/AbstractCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/AbstractCreator.java index ce5a2e944..4d6e662b8 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/AbstractCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/AbstractCreator.java @@ -74,15 +74,15 @@ public Type create(ClassInfo classInfo, Reference reference) { addOperations(type, classInfo); // Directives - addDirectives(type, classInfo); + addDirectives(type, annotations); return type; } protected abstract void addFields(Type type, ClassInfo classInfo, Reference reference); - private void addDirectives(Type type, ClassInfo classInfo) { - type.setDirectiveInstances(directives.buildDirectiveInstances(classInfo::classAnnotation)); + private void addDirectives(Type type, Annotations annotations) { + type.setDirectiveInstances(directives.buildDirectiveInstances(annotations)); } private void addPolymorphicTypes(Type type, ClassInfo classInfo, Reference reference) { diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/EnumCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/EnumCreator.java index d352b1eec..fb8e854f9 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/EnumCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/EnumCreator.java @@ -78,7 +78,7 @@ public EnumType create(ClassInfo classInfo, Reference reference) { } private List getDirectiveInstances(Annotations annotations) { - return directives.buildDirectiveInstances(dotName -> annotations.getOneOfTheseAnnotations(dotName).orElse(null)); + return directives.buildDirectiveInstances(annotations); } } diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/InputTypeCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/InputTypeCreator.java index d8c3c80b0..f99358bd1 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/InputTypeCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/InputTypeCreator.java @@ -29,7 +29,7 @@ /** * This creates an input type object. - * + * * The input object has fields that might reference other types * that should still be created. * @@ -138,7 +138,7 @@ public void setDirectives(Directives directives) { } private List getDirectiveInstances(Annotations annotations) { - return directives.buildDirectiveInstances(dotName -> annotations.getOneOfTheseAnnotations(dotName).orElse(null)); + return directives.buildDirectiveInstances(annotations); } private void addFields(InputType inputType, ClassInfo classInfo, Reference reference) { diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/Directives.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/Directives.java index a008e1e4d..df781c129 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/Directives.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/Directives.java @@ -1,17 +1,18 @@ package io.smallrye.graphql.schema.helper; +import static java.util.stream.Collectors.toList; import static org.jboss.jandex.AnnotationValue.Kind.ARRAY; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.DotName; +import io.smallrye.graphql.schema.Annotations; import io.smallrye.graphql.schema.model.DirectiveInstance; import io.smallrye.graphql.schema.model.DirectiveType; @@ -37,21 +38,13 @@ public Directives(List directiveTypes) { } } - public List buildDirectiveInstances(Function getAnnotation) { - List result = null; + public List buildDirectiveInstances(Annotations annotations) { // only build directive instances from `@Directive` annotations here (that means the `directiveTypes` map), // because `directiveTypesOther` directives get their instances added on-the-go by classes that extend `ModelCreator` - for (DotName directiveTypeName : directiveTypes.keySet()) { - AnnotationInstance annotationInstance = getAnnotation.apply(directiveTypeName); - if (annotationInstance == null) { - continue; - } - if (result == null) { - result = new ArrayList<>(); - } - result.add(toDirectiveInstance(annotationInstance)); - } - return result; + return directiveTypes.keySet().stream() + .flatMap(annotations::resolve) + .map(this::toDirectiveInstance) + .collect(toList()); } private DirectiveInstance toDirectiveInstance(AnnotationInstance annotationInstance) { diff --git a/common/schema-model/pom.xml b/common/schema-model/pom.xml index 594afbb8a..e77045514 100644 --- a/common/schema-model/pom.xml +++ b/common/schema-model/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-common-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-schema-model diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/DirectiveType.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/DirectiveType.java index 0127e92c7..4b519b3a4 100644 --- a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/DirectiveType.java +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/DirectiveType.java @@ -69,6 +69,10 @@ public void setRepeatable(boolean repeatable) { this.repeatable = repeatable; } + public boolean isFederation() { + return className != null && className.startsWith("io.smallrye.graphql.api.federation"); + } + /** * Helper 'getter' methods, but DON'T add 'get' into their names, otherwise it breaks Quarkus bytecode recording, * because they would be detected as actual property getters while they are actually not diff --git a/docs/directives.md b/docs/directives.md index 139e1e2a4..8902f10e4 100644 --- a/docs/directives.md +++ b/docs/directives.md @@ -1,5 +1,20 @@ # Directives +## Custom Directives + +You can add your own [GraphQL Directives](https://spec.graphql.org/draft/#sec-Language.Directives) by writing +a corresponding Java Annotation and annotate it as `@Directive`, e.g.: + +```java +@Directive(on = { OBJECT, INTERFACE }) +@Description("Just a test") +@Retention(RUNTIME) +public @interface MyDirective { +} +``` + +Directives can be repeatable, see the `@Key` annotation for an example. + ## Directives generated from Bean Validation annotations If your project uses Bean Validation to validate fields on input types and operation arguments, and you enable @@ -23,4 +38,4 @@ BV annotations are listed here): Note: The `@NotNull` annotation does not map to a directive, instead it makes the GraphQL type non-nullable. -Constraints will only appear on fields of input types and operation arguments. \ No newline at end of file +Constraints will only appear on fields of input types and operation arguments. diff --git a/docs/federation.md b/docs/federation.md index e7ee83088..7fbee7f22 100644 --- a/docs/federation.md +++ b/docs/federation.md @@ -1,6 +1,6 @@ # Federation -To enable support for [GraphQL Federation](https://www.apollographql.com/docs/federation), simply set the `smallrye.graphql.federation.enabled` config key to `true`. +Support for [GraphQL Federation](https://www.apollographql.com/docs/federation) is enabled by default. If you add one of the federation annotations, the corresponding directives will be declared to your schema and the additional Federation queries will be added automatically. You can also disable Federation completely by setting the `smallrye.graphql.federation.enabled` config key to `false`. You can add the Federation directives by using the equivalent Java annotation, e.g. to extend a `Product` entity with a `price` field, you can write a class: @@ -37,7 +37,7 @@ import org.eclipse.microprofile.graphql.Query; public class Prices { @Query public Product product(@Id String id) { - return ...; + return ... } } ``` @@ -45,7 +45,7 @@ public class Prices { The GraphQL Schema then contains: ```graphql -type Product @extends @key(fields : ["id"]) { +type Product @extends @key(fields : "id") { id: ID price: Int } @@ -58,3 +58,5 @@ type Query { product(id: ID): Product } ``` + +If you can resolve, e.g., the product with different types of ids, you can add multiple `@Key` annotations. diff --git a/docs/pom.xml b/docs/pom.xml index 5f2e9a20a..bf7ae58f7 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-documentation diff --git a/pom.xml b/pom.xml index d840f671b..6fb121921 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ smallrye-graphql-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT pom SmallRye: GraphQL Parent diff --git a/release/pom.xml b/release/pom.xml index 464e3050b..fb61daa22 100644 --- a/release/pom.xml +++ b/release/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-release diff --git a/server/api/pom.xml b/server/api/pom.xml index 37e7ae851..09fc7d537 100644 --- a/server/api/pom.xml +++ b/server/api/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-server-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-api diff --git a/server/api/src/main/java/io/smallrye/graphql/api/federation/Extends.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/Extends.java index 2220567f0..49b3cdfa1 100644 --- a/server/api/src/main/java/io/smallrye/graphql/api/federation/Extends.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/Extends.java @@ -11,10 +11,15 @@ import io.smallrye.common.annotation.Experimental; import io.smallrye.graphql.api.Directive; -/** directive @extends on OBJECT | INTERFACE */ +/** + * directive @extends on OBJECT | INTERFACE + * + * @see federation + * spec + */ @Directive(on = { OBJECT, INTERFACE }) -@Description("Some libraries such as graphql-java don't have native support for type extensions in their printer. " + - "Apollo Federation supports using an @extends directive in place of extend type to annotate type references.") +@Description("Indicates that an object or interface definition is an extension of another definition of that same type.\n" + + "If your subgraph library supports GraphQL's built-in extend keyword, do not use this directive! Instead, use extend.") @Retention(RUNTIME) @Experimental("SmallRye GraphQL Federation is still subject to change. " + "Additionally, this annotation is currently only a directive without explicit support from the extension.") diff --git a/server/api/src/main/java/io/smallrye/graphql/api/federation/External.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/External.java index 431a906cd..0bd2d3fd8 100644 --- a/server/api/src/main/java/io/smallrye/graphql/api/federation/External.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/External.java @@ -1,6 +1,7 @@ package io.smallrye.graphql.api.federation; import static io.smallrye.graphql.api.DirectiveLocation.FIELD_DEFINITION; +import static io.smallrye.graphql.api.DirectiveLocation.OBJECT; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; @@ -10,10 +11,17 @@ import io.smallrye.common.annotation.Experimental; import io.smallrye.graphql.api.Directive; -/** directive @external on FIELD_DEFINITION */ -@Directive(on = FIELD_DEFINITION) -@Description("The @external directive is used to mark a field as owned by another service. " + - "This allows service A to use fields from service B while also knowing at runtime the types of that field.") +/** + * directive @external on FIELD_DEFINITION | OBJECT + * + * @see federation + * spec + */ +@Directive(on = { FIELD_DEFINITION, OBJECT }) +@Description("Indicates that this subgraph usually can't resolve a particular object field, but it still needs to define " + + "that field for other purposes.\n" + + "This directive is always used in combination with another directive that references object fields, " + + "such as @provides or @requires.") @Retention(RUNTIME) @Experimental("SmallRye GraphQL Federation is still subject to change.") public @interface External { diff --git a/server/api/src/main/java/io/smallrye/graphql/api/federation/Key.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/Key.java index 6ae31a6df..4015e642a 100644 --- a/server/api/src/main/java/io/smallrye/graphql/api/federation/Key.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/Key.java @@ -4,6 +4,7 @@ import static io.smallrye.graphql.api.DirectiveLocation.OBJECT; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import org.eclipse.microprofile.graphql.Description; @@ -11,14 +12,32 @@ import io.smallrye.common.annotation.Experimental; import io.smallrye.graphql.api.Directive; +import io.smallrye.graphql.api.federation.Key.Keys; -/** directive @key(fields: _FieldSet!) on OBJECT | INTERFACE */ +/** + * directive @key(fields: FieldSet!) repeatable on OBJECT | INTERFACE + * + * @see federation spec + */ @Directive(on = { OBJECT, INTERFACE }) -@Description("The @key directive is used to indicate a combination of fields that can be used to uniquely identify " + - "and fetch an object or interface.") +@Description("Designates an object type as an entity and specifies its key fields (a set of fields that the subgraph " + + "can use to uniquely identify any instance of the entity). You can apply multiple @key directives to " + + "a single entity (to specify multiple valid sets of key fields).") @Retention(RUNTIME) +@Repeatable(Keys.class) @Experimental("SmallRye GraphQL Federation is still subject to change.") public @interface Key { @NonNull - String[] fields(); + @Description("A GraphQL selection set (provided as a string) of fields and subfields that contribute " + + "to the entity's primary key.\n" + + "Examples:\n" + + "\"id\"\n" + + "\"username region\"\n" + + "\"name organization { id }\"") + String fields(); + + @Retention(RUNTIME) + @interface Keys { + Key[] value(); + } } diff --git a/server/api/src/main/java/io/smallrye/graphql/api/federation/Provides.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/Provides.java index df63a6d2d..458622e13 100644 --- a/server/api/src/main/java/io/smallrye/graphql/api/federation/Provides.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/Provides.java @@ -11,13 +11,27 @@ import io.smallrye.common.annotation.Experimental; import io.smallrye.graphql.api.Directive; -/** directive @provides(fields: _FieldSet!) on FIELD_DEFINITION */ +/** + * directive @provides(fields: FieldSet!) on FIELD_DEFINITION + * + * @see federation + * spec + */ @Directive(on = FIELD_DEFINITION) -@Description("When resolving the annotated field, this service can provide additional, normally `@external` fields.") +@Description("Specifies a set of entity fields that a subgraph can resolve, but only at a particular schema path " + + "(at other paths, the subgraph can't resolve those fields).\n" + + "If a subgraph can always resolve a particular entity field, do not apply this directive.\n" + + "Using this directive is always an optional optimization. It can reduce the total number of subgraphs " + + "that your graph router needs to communicate with to resolve certain operations, which can improve performance.") @Retention(RUNTIME) -@Experimental("SmallRye GraphQL Federation is still subject to change. " + - "Additionally, this annotation is currently only a directive without explicit support from the extension.") +@Experimental("SmallRye GraphQL Federation is still subject to change.") public @interface Provides { @NonNull - String[] fields(); + @Description("A GraphQL selection set (provided as a string) of object fields and subfields that the subgraph " + + "can resolve only at this query path.\n" + + "Examples:\n" + + "\"name\"\n" + + "\"name address\"\n" + + "\"... on Person { name address }\" (valid for fields that return a union or interface)") + String fields(); } diff --git a/server/api/src/main/java/io/smallrye/graphql/api/federation/Requires.java b/server/api/src/main/java/io/smallrye/graphql/api/federation/Requires.java index 90cce5924..a4e81b0fd 100644 --- a/server/api/src/main/java/io/smallrye/graphql/api/federation/Requires.java +++ b/server/api/src/main/java/io/smallrye/graphql/api/federation/Requires.java @@ -11,14 +11,20 @@ import io.smallrye.common.annotation.Experimental; import io.smallrye.graphql.api.Directive; -/** directive @requires(fields: _FieldSet!) on FIELD_DEFINITION */ +/** + * directive @requires(fields: FieldSet!) on FIELD_DEFINITION + * + * @see federation + * spec + */ @Directive(on = FIELD_DEFINITION) -@Description("In order to resolve the annotated field, this service needs these additional `@external` fields, " + - "even when the client didn't request them.") +@Description("Indicates that the resolver for a particular entity field depends on the values of other entity fields " + + "that are resolved by other subgraphs. This tells the graph router that it needs to fetch the values " + + "of those externally defined fields first, even if the original client query didn't request them.") @Retention(RUNTIME) @Experimental("SmallRye GraphQL Federation is still subject to change. " + "Additionally, this annotation is currently only a directive without explicit support from the extension.") public @interface Requires { @NonNull - String[] fields(); + String fields(); } diff --git a/server/implementation-cdi/pom.xml b/server/implementation-cdi/pom.xml index 40306e766..a524c4891 100644 --- a/server/implementation-cdi/pom.xml +++ b/server/implementation-cdi/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-server-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-cdi diff --git a/server/implementation-servlet/pom.xml b/server/implementation-servlet/pom.xml index 46f5ef775..cc8eb60be 100644 --- a/server/implementation-servlet/pom.xml +++ b/server/implementation-servlet/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-server-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-servlet diff --git a/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java b/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java index 6a360b923..f9f6f1093 100644 --- a/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java +++ b/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.InputStream; +import java.lang.annotation.Repeatable; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; @@ -78,6 +79,7 @@ private IndexView createCustomIndex() { try { indexer.index(convertClassToInputStream(Map.class)); indexer.index(convertClassToInputStream(Entry.class)); + indexer.index(convertClassToInputStream(Repeatable.class)); // things from the API module indexer.index(convertClassToInputStream(Extends.class)); diff --git a/server/implementation/pom.xml b/server/implementation/pom.xml index 53884db5a..7a320f613 100644 --- a/server/implementation/pom.xml +++ b/server/implementation/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-server-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql diff --git a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java index 912225a0d..e803acf85 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java @@ -220,11 +220,17 @@ private TypeResolver fetchEntityType() { private void createGraphQLDirectiveTypes() { if (schema.hasDirectiveTypes()) { for (DirectiveType directiveType : schema.getDirectiveTypes()) { - createGraphQLDirectiveType(directiveType); + if (enabled(directiveType)) { + createGraphQLDirectiveType(directiveType); + } } } } + private static boolean enabled(DirectiveType directiveType) { + return Config.get().isFederationEnabled() || !directiveType.isFederation(); + } + private void createGraphQLDirectiveType(DirectiveType directiveType) { GraphQLDirective.Builder directiveBuilder = GraphQLDirective.newDirective() .name(directiveType.getName()) @@ -372,7 +378,8 @@ private void createGraphQLEnumType(EnumType enumType) { .description(enumType.getDescription()); // Directives if (enumType.hasDirectiveInstances()) { - enumBuilder = enumBuilder.withDirectives(createGraphQLDirectives(enumType.getDirectiveInstances())); + enumBuilder = enumBuilder + .withDirectives(createGraphQLDirectives(enumType.getDirectiveInstances())); } // Values for (EnumValue value : enumType.getValues()) { @@ -381,7 +388,8 @@ private void createGraphQLEnumType(EnumType enumType) { .value(value.getValue()) .description(value.getDescription()); if (value.hasDirectiveInstances()) { - definitionBuilder = definitionBuilder.withDirectives(createGraphQLDirectives(value.getDirectiveInstances())); + definitionBuilder = definitionBuilder + .withDirectives(createGraphQLDirectives(value.getDirectiveInstances())); } enumBuilder = enumBuilder.value(definitionBuilder.build()); } @@ -411,9 +419,8 @@ private void createGraphQLInterfaceType(Type interfaceType) { // Directives if (interfaceType.hasDirectiveInstances()) { - for (DirectiveInstance directiveInstance : interfaceType.getDirectiveInstances()) { - interfaceTypeBuilder.withDirective(createGraphQLDirectiveFrom(directiveInstance)); - } + interfaceTypeBuilder = interfaceTypeBuilder + .withDirectives(createGraphQLDirectives(interfaceType.getDirectiveInstances())); } // Interfaces @@ -535,9 +542,8 @@ private void createGraphQLObjectType(Type type) { // Directives if (type.hasDirectiveInstances()) { - for (DirectiveInstance directiveInstance : type.getDirectiveInstances()) { - objectTypeBuilder.withDirective(createGraphQLDirectiveFrom(directiveInstance)); - } + objectTypeBuilder = objectTypeBuilder + .withDirectives(createGraphQLDirectives(type.getDirectiveInstances())); } // Fields @@ -657,7 +663,8 @@ private GraphQLFieldDefinition createGraphQLFieldDefinitionFromOperation(String // Directives if (operation.hasDirectiveInstances()) { - fieldBuilder = fieldBuilder.withDirectives(createGraphQLDirectives(operation.getDirectiveInstances())); + fieldBuilder = fieldBuilder + .withDirectives(createGraphQLDirectives(operation.getDirectiveInstances())); } GraphQLFieldDefinition graphQLFieldDefinition = fieldBuilder.build(); @@ -673,6 +680,7 @@ private GraphQLFieldDefinition createGraphQLFieldDefinitionFromOperation(String private GraphQLDirective[] createGraphQLDirectives(Collection directiveInstances) { return directiveInstances.stream() + .filter(directiveInstance -> enabled(directiveInstance.getType())) .map(this::createGraphQLDirectiveFrom) .toArray(GraphQLDirective[]::new); } @@ -695,9 +703,8 @@ private GraphQLFieldDefinition createGraphQLFieldDefinitionFromField(Reference o // Directives if (field.hasDirectiveInstances()) { - for (DirectiveInstance directiveInstance : field.getDirectiveInstances()) { - fieldBuilder.withDirective(createGraphQLDirectiveFrom(directiveInstance)); - } + fieldBuilder = fieldBuilder + .withDirectives(createGraphQLDirectives(field.getDirectiveInstances())); } // Auto Map argument @@ -770,10 +777,10 @@ private GraphQLInputObjectField createGraphQLInputObjectFieldFromField(Field fie // Type inputFieldBuilder = inputFieldBuilder.type(createGraphQLInputType(field)); + // Directives if (field.hasDirectiveInstances()) { - for (DirectiveInstance directiveInstance : field.getDirectiveInstances()) { - inputFieldBuilder.withDirective(createGraphQLDirectiveFrom(directiveInstance)); - } + inputFieldBuilder = inputFieldBuilder + .withDirectives(createGraphQLDirectives(field.getDirectiveInstances())); } // Default value (on method) @@ -942,12 +949,13 @@ private GraphQLArgument createGraphQLArgument(Argument argument) { graphQLInputType = GraphQLNonNull.nonNull(graphQLInputType); } + // Type argumentBuilder = argumentBuilder.type(graphQLInputType); + // Directives if (argument.hasDirectiveInstances()) { - for (DirectiveInstance directiveInstance : argument.getDirectiveInstances()) { - argumentBuilder.withDirective(createGraphQLDirectiveFrom(directiveInstance)); - } + argumentBuilder = argumentBuilder + .withDirectives(createGraphQLDirectives(argument.getDirectiveInstances())); } return argumentBuilder.build(); diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTest.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTest.java index becceb117..92611e17b 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTest.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTest.java @@ -1,5 +1,10 @@ package io.smallrye.graphql.schema; +import static graphql.Scalars.GraphQLString; +import static graphql.introspection.Introspection.DirectiveLocation.INTERFACE; +import static graphql.introspection.Introspection.DirectiveLocation.OBJECT; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toSet; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -9,8 +14,11 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.lang.annotation.Repeatable; import java.net.URISyntaxException; import java.nio.file.Files; +import java.util.EnumSet; +import java.util.Set; import java.util.stream.Stream; import org.jboss.jandex.IndexView; @@ -25,9 +33,12 @@ import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLUnionType; import io.smallrye.graphql.api.Directive; import io.smallrye.graphql.api.federation.Key; +import io.smallrye.graphql.api.federation.Key.Keys; import io.smallrye.graphql.bootstrap.Bootstrap; import io.smallrye.graphql.execution.SchemaPrinter; import io.smallrye.graphql.execution.TestConfig; @@ -76,8 +87,8 @@ void testSchemaWithDirectives() throws URISyntaxException, IOException { assertOperationWithDirectives(graphQLSchema.getSubscriptionType().getField("subscriptionWithDirectives")); String actualSchema = new SchemaPrinter().print(graphQLSchema); - String expectedSchema = Files - .readString(new File(SchemaTest.class.getResource("/schemaTest.graphql").toURI()).toPath()); + var schemaUri = requireNonNull(SchemaTest.class.getResource("/schemaTest.graphql")).toURI(); + String expectedSchema = Files.readString(new File(schemaUri).toPath()); Assertions.assertEquals(expectedSchema, actualSchema); } @@ -91,12 +102,8 @@ void schemaWithEnumDirectives() { "Enum EnumWithDirectives should have directive @enumDirective"); assertNotNull(enumWithDirectives.getValue("A").getDirective("enumDirective"), "Enum value EnumWithDirectives.A should have directive @enumDirective"); - - assertSchemaEndsWith(graphQLSchema, "" + - "enum EnumWithDirectives @enumDirective {\n" + - " A @enumDirective\n" + - " B\n" + - "}\n"); + assertNull(enumWithDirectives.getValue("B").getDirective("enumDirective"), + "Enum value EnumWithDirectives.B should not have directive @enumDirective"); } @Test @@ -111,53 +118,108 @@ void schemaWithInputDirectives() { "Input type field InputWithDirectivesInput.foo should have directive @inputDirective"); assertNotNull(inputWithDirectives.getField("bar").getDirective("inputDirective"), "Input type field InputWithDirectivesInput.bar should have directive @inputDirective"); - - assertSchemaEndsWith(graphQLSchema, "" + - "input InputWithDirectivesInput @inputDirective {\n" + - " bar: Int! @inputDirective\n" + - " foo: Int! @inputDirective\n" + - "}\n"); } @Test void testSchemaWithFederationDisabled() { - GraphQLSchema graphQLSchema = createGraphQLSchema(Directive.class, Key.class, TestTypeWithFederation.class, - FederationTestApi.class); - - assertSchemaEndsWith(graphQLSchema, "\n" + - "\"Query root\"\n" + - "type Query {\n" + - " testTypeWithFederation(arg: String): TestTypeWithFederation\n" + - "}\n" + - "\n" + - "type TestTypeWithFederation @key(fields : [\"id\"]) {\n" + - " id: String\n" + - "}\n"); + config.federationEnabled = false; + + GraphQLSchema graphQLSchema = createGraphQLSchema(Directive.class, Key.class, Keys.class, + TestTypeWithFederation.class, FederationTestApi.class); + + assertEquals(Set.of("include", "specifiedBy", "deprecated", "skip", "constraint"), + graphQLSchema.getDirectives().stream().map(GraphQLDirective::getName).collect(toSet())); + assertNull(graphQLSchema.getDirective("key")); // esp. NOT this one + assertNull(graphQLSchema.getType("_Entity")); + assertNull(graphQLSchema.getType("_Service")); + + GraphQLObjectType queryRoot = graphQLSchema.getQueryType(); + assertEquals(1, queryRoot.getFields().size()); + assertNull(queryRoot.getField("_entities")); + assertNull(queryRoot.getField("_service")); + + GraphQLFieldDefinition query = queryRoot.getField("testTypeWithFederation"); + assertEquals(1, query.getArguments().size()); + assertEquals(GraphQLString, query.getArgument("arg").getType()); + assertEquals("TestTypeWithFederation", ((GraphQLObjectType) query.getType()).getName()); + + GraphQLObjectType type = graphQLSchema.getObjectType("TestTypeWithFederation"); + assertEquals(0, type.getDirectives().size()); + assertEquals(3, type.getFields().size()); + assertEquals("id", type.getFields().get(0).getName()); + assertEquals(GraphQLString, type.getFields().get(0).getType()); + assertEquals("type", type.getFields().get(1).getName()); + assertEquals(GraphQLString, type.getFields().get(1).getType()); + assertEquals("value", type.getFields().get(2).getName()); + assertEquals(GraphQLString, type.getFields().get(2).getType()); + + assertNull(graphQLSchema.getObjectType("_Service")); } @Test - void testSchemaWithFederation() { + void testSchemaWithFederationEnabled() { config.federationEnabled = true; - GraphQLSchema graphQLSchema = createGraphQLSchema(Directive.class, Key.class, TestTypeWithFederation.class, - FederationTestApi.class); - - assertSchemaEndsWith(graphQLSchema, "\n" + - "union _Entity = TestTypeWithFederation\n" + - "\n" + - "\"Query root\"\n" + - "type Query {\n" + - " _entities(representations: [_Any!]!): [_Entity]!\n" + - " _service: _Service!\n" + - " testTypeWithFederation(arg: String): TestTypeWithFederation\n" + - "}\n" + - "\n" + - "type TestTypeWithFederation @key(fields : [\"id\"]) {\n" + - " id: String\n" + - "}\n" + - "\n" + - "type _Service {\n" + - " sdl: String!\n" + - "}\n"); + GraphQLSchema graphQLSchema = createGraphQLSchema(Repeatable.class, Directive.class, Key.class, Keys.class, + TestTypeWithFederation.class, FederationTestApi.class); + + GraphQLDirective keyDirective = graphQLSchema.getDirective("key"); + assertEquals("key", keyDirective.getName()); + assertTrue(keyDirective.isRepeatable()); + assertEquals( + "Designates an object type as an entity and specifies its key fields " + + "(a set of fields that the subgraph can use to uniquely identify any instance " + + "of the entity). You can apply multiple @key directives to a single entity " + + "(to specify multiple valid sets of key fields).", + keyDirective.getDescription()); + assertEquals(EnumSet.of(OBJECT, INTERFACE), keyDirective.validLocations()); + assertEquals(1, keyDirective.getArguments().size()); + assertEquals("String", ((GraphQLScalarType) keyDirective.getArgument("fields").getType()).getName()); + + GraphQLUnionType entityType = (GraphQLUnionType) graphQLSchema.getType("_Entity"); + assertNotNull(entityType); + assertEquals(1, entityType.getTypes().size()); + assertEquals(TestTypeWithFederation.class.getSimpleName(), entityType.getTypes().get(0).getName()); + + GraphQLObjectType queryRoot = graphQLSchema.getQueryType(); + assertEquals(3, queryRoot.getFields().size()); + + GraphQLFieldDefinition entities = queryRoot.getField("_entities"); + assertEquals(1, entities.getArguments().size()); + assertEquals("[_Any!]!", entities.getArgument("representations").getType().toString()); + assertEquals("[_Entity]!", entities.getType().toString()); + + GraphQLFieldDefinition service = queryRoot.getField("_service"); + assertEquals(0, service.getArguments().size()); + assertEquals("_Service!", service.getType().toString()); + + GraphQLFieldDefinition query = queryRoot.getField("testTypeWithFederation"); + assertEquals(1, query.getArguments().size()); + assertEquals(GraphQLString, query.getArgument("arg").getType()); + assertEquals("TestTypeWithFederation", ((GraphQLObjectType) query.getType()).getName()); + + GraphQLObjectType type = graphQLSchema.getObjectType("TestTypeWithFederation"); + assertEquals(2, type.getDirectives().size()); + assertKeyDirective(type.getDirectives().get(0), "id"); + assertKeyDirective(type.getDirectives().get(1), "type id"); + assertEquals(3, type.getFields().size()); + assertEquals("id", type.getFields().get(0).getName()); + assertEquals(GraphQLString, type.getFields().get(0).getType()); + assertEquals("type", type.getFields().get(1).getName()); + assertEquals(GraphQLString, type.getFields().get(1).getType()); + assertEquals("value", type.getFields().get(2).getName()); + assertEquals(GraphQLString, type.getFields().get(2).getType()); + + GraphQLObjectType serviceType = graphQLSchema.getObjectType("_Service"); + assertEquals(1, serviceType.getFields().size()); + assertEquals("sdl", serviceType.getFields().get(0).getName()); + assertEquals("String!", serviceType.getFields().get(0).getType().toString()); + } + + private static void assertKeyDirective(GraphQLDirective graphQLDirective, String value) { + assertEquals("key", graphQLDirective.getName()); + assertEquals(1, graphQLDirective.getArguments().size()); + assertEquals("fields", graphQLDirective.getArguments().get(0).getName()); + assertEquals(value, graphQLDirective.getArguments().get(0).toAppliedArgument().getArgumentValue().getValue()); } private GraphQLSchema createGraphQLSchema(Class... api) { @@ -168,16 +230,6 @@ private GraphQLSchema createGraphQLSchema(Class... api) { return graphQLSchema; } - private static void assertSchemaEndsWith(GraphQLSchema schema, String end) { - String schemaString = new SchemaPrinter().print(schema); - assertSchemaEndsWith(schemaString, end); - } - - private static void assertSchemaEndsWith(String schema, String end) { - // assertEquals(schema, end); // this is convenient for debugging, as the IDE can show the diff - assertTrue(schema.endsWith(end), () -> "<<<\n" + schema + "\n>>> does not end with <<<\n" + end + "\n>>>"); - } - private void assertOperationWithDirectives(GraphQLFieldDefinition operation) { String name = operation.getName(); GraphQLDirective operationDirective = operation.getDirective("operationDirective"); diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/TestTypeWithFederation.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/TestTypeWithFederation.java index a9d85d831..b7bad7272 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/schema/TestTypeWithFederation.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/schema/TestTypeWithFederation.java @@ -3,8 +3,19 @@ import io.smallrye.graphql.api.federation.Key; @Key(fields = "id") +@Key(fields = "type id") public class TestTypeWithFederation { + private String type; private String id; + private String value; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } public String getId() { return id; @@ -13,4 +24,12 @@ public String getId() { public void setId(String id) { this.id = id; } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } } diff --git a/server/integration-tests-jdk16/pom.xml b/server/integration-tests-jdk16/pom.xml index 4e1504083..87f62e920 100644 --- a/server/integration-tests-jdk16/pom.xml +++ b/server/integration-tests-jdk16/pom.xml @@ -3,7 +3,7 @@ smallrye-graphql-server-parent io.smallrye - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT 4.0.0 diff --git a/server/integration-tests/pom.xml b/server/integration-tests/pom.xml index ac18dd4ff..7dd6de23c 100644 --- a/server/integration-tests/pom.xml +++ b/server/integration-tests/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-server-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT 4.0.0 diff --git a/server/pom.xml b/server/pom.xml index 43705492a..95e24bd6e 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-server-parent diff --git a/server/runner/pom.xml b/server/runner/pom.xml index a3b3dda1c..700561cc5 100644 --- a/server/runner/pom.xml +++ b/server/runner/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-server-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-runner diff --git a/server/tck/pom.xml b/server/tck/pom.xml index 8fb7b9bb9..7255c8be4 100644 --- a/server/tck/pom.xml +++ b/server/tck/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-server-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-tck @@ -132,6 +132,9 @@ testng.xml + + true + diff --git a/tools/gradle-plugin/gradle.properties b/tools/gradle-plugin/gradle.properties index 97d695b3d..332be4061 100644 --- a/tools/gradle-plugin/gradle.properties +++ b/tools/gradle-plugin/gradle.properties @@ -1 +1 @@ -version=2.0.0 +version=2.0.1 diff --git a/tools/gradle-plugin/pom.xml b/tools/gradle-plugin/pom.xml index 3bcc811e7..32cac9835 100644 --- a/tools/gradle-plugin/pom.xml +++ b/tools/gradle-plugin/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-tools-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT 4.0.0 diff --git a/tools/maven-plugin-tests/pom.xml b/tools/maven-plugin-tests/pom.xml index 423c6e961..804ebd866 100644 --- a/tools/maven-plugin-tests/pom.xml +++ b/tools/maven-plugin-tests/pom.xml @@ -3,7 +3,7 @@ io.smallrye smallrye-graphql-tools-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT 4.0.0 diff --git a/tools/maven-plugin/pom.xml b/tools/maven-plugin/pom.xml index 997991e99..ab2309e79 100644 --- a/tools/maven-plugin/pom.xml +++ b/tools/maven-plugin/pom.xml @@ -3,7 +3,7 @@ io.smallrye smallrye-graphql-tools-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT 4.0.0 diff --git a/tools/pom.xml b/tools/pom.xml index 8d31e6b53..481898594 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-tools-parent diff --git a/ui/graphiql/pom.xml b/ui/graphiql/pom.xml index eb3dc14f9..b17eb4eb9 100644 --- a/ui/graphiql/pom.xml +++ b/ui/graphiql/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-ui-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-ui-graphiql diff --git a/ui/pom.xml b/ui/pom.xml index d56208918..344f77f86 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-parent - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT smallrye-graphql-ui-parent