diff --git a/.github/project.yml b/.github/project.yml index 3f3ffe844..24246efc7 100644 --- a/.github/project.yml +++ b/.github/project.yml @@ -1,4 +1,4 @@ name: SmallRye GraphQL release: - current-version: 2.8.1 - next-version: 2.8.2-SNAPSHOT + current-version: 2.9.1 + next-version: 2.9.2-SNAPSHOT diff --git a/.github/release/maven-settings.xml.gpg b/.github/release/maven-settings.xml.gpg deleted file mode 100644 index d25987610..000000000 Binary files a/.github/release/maven-settings.xml.gpg and /dev/null differ diff --git a/.github/release/smallrye-sign.asc.gpg b/.github/release/smallrye-sign.asc.gpg deleted file mode 100644 index ffd7b0f07..000000000 Binary files a/.github/release/smallrye-sign.asc.gpg and /dev/null differ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bab32ebb1..df8ecd023 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -96,8 +96,8 @@ jobs: - name: checkout Quarkus repository uses: actions/checkout@v2 with: - repository: quarkusio/quarkus - ref: main + repository: mskacelik/quarkus + ref: srgql-2.9.1 path: quarkus - uses: actions/setup-java@v3.10.0 @@ -120,7 +120,7 @@ jobs: cd quarkus && \ mvn install -Dsmallrye-graphql.version=$SMALLRYE_VERSION -Dquickly && \ mvn verify -Dsmallrye-graphql.version=$SMALLRYE_VERSION -Dnative \ - -pl extensions/smallrye-graphql-client/deployment,extensions/smallrye-graphql-client/runtime,extensions/smallrye-graphql/deployment,extensions/smallrye-graphql/runtime,integration-tests/smallrye-graphql,integration-tests/smallrye-graphql-client,integration-tests/hibernate-orm-graphql-panache,extensions/oidc-client-graphql/deployment,extensions/oidc-client-graphql/runtime + -pl extensions/smallrye-graphql-client/deployment,extensions/smallrye-graphql-client/runtime,extensions/smallrye-graphql/deployment,extensions/smallrye-graphql/runtime,integration-tests/smallrye-graphql,integration-tests/smallrye-graphql-client,integration-tests/hibernate-orm-graphql-panache,extensions/oidc-client-graphql/deployment,extensions/oidc-client-graphql/runtime,tcks/microprofile-graphql -Ptcks quality: needs: [build] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99198173d..d0528e6fa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,16 +29,22 @@ jobs: - uses: actions/setup-java@v1.4.3 with: java-version: 11 + server-id: 'oss.sonatype' + server-username: 'MAVEN_DEPLOY_USERNAME' + server-password: 'MAVEN_DEPLOY_TOKEN' + gpg-private-key: ${{secrets.MAVEN_GPG_PRIVATE_KEY}} + gpg-passphrase: 'MAVEN_GPG_PASSPHRASE' - name: Install graphviz run: sudo apt install graphviz - name: maven release ${{steps.metadata.outputs.current-version}} + env: + MAVEN_DEPLOY_USERNAME: ${{secrets.MAVEN_DEPLOY_USERNAME}} + MAVEN_DEPLOY_TOKEN: ${{secrets.MAVEN_DEPLOY_TOKEN}} + MAVEN_GPG_PASSPHRASE: ${{secrets.MAVEN_GPG_PASSPHRASE}} run: | java -version - gpg --quiet --batch --yes --decrypt --passphrase="${{secrets.SECRET_PASSPHRASE}}" --output smallrye-sign.asc .github/release/smallrye-sign.asc.gpg - gpg --quiet --batch --yes --decrypt --passphrase="${{secrets.SECRET_PASSPHRASE}}" --output maven-settings.xml .github/release/maven-settings.xml.gpg - gpg --fast-import --no-tty --batch --yes smallrye-sign.asc git config --global user.name "SmallRye CI" git config --global user.email "smallrye@googlegroups.com" git checkout -b release @@ -47,10 +53,10 @@ jobs: git add tools/gradle-plugin/gradle.properties git commit -m "Update Gradle plugin version" # make sure the server/integration-tests-jdk16 gets its version updated too: include the jdk16plus profile - mvn -X -e -B release:prepare -Prelease -Pjdk16plus -DreleaseVersion=${{steps.metadata.outputs.current-version}} -DdevelopmentVersion=${{steps.metadata.outputs.next-version}} -s maven-settings.xml + mvn -X -e -B release:prepare -Prelease -Pjdk16plus -DreleaseVersion=${{steps.metadata.outputs.current-version}} -DdevelopmentVersion=${{steps.metadata.outputs.next-version}} git checkout ${{github.base_ref}} git rebase release - mvn -X -e -B release:perform -Prelease -Pjdk16plus -s maven-settings.xml + mvn -X -e -B release:perform -Prelease -Pjdk16plus git push git push --tags diff --git a/README.adoc b/README.adoc index 8ea2ae909..c49434ca0 100644 --- a/README.adoc +++ b/README.adoc @@ -3,6 +3,7 @@ :subscriptions-transport-ws: https://github.com/apollographql/subscriptions-transport-ws :graphql-ws: https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md :graphql-federation: https://www.apollographql.com/docs/federation +:vertx: https://vertx.io/ image:https://github.com/smallrye/smallrye-graphql/workflows/SmallRye%20Build/badge.svg?branch=main[link=https://github.com/smallrye/smallrye-graphql/actions?query=workflow%3A%22SmallRye+Build%22] image:https://sonarcloud.io/api/project_badges/measure?project=smallrye_smallrye-graphql&metric=alert_status["Quality Gate Status", link="https://sonarcloud.io/dashboard?id=smallrye_smallrye-graphql"] @@ -62,17 +63,22 @@ The 1.5.x branch will be maintained for the `javax` namespace, and the main (2.x * link:server/api[API] (pulling in the MicroProfile API) and allowing us to experiment with api feature not yet in the spec. Code from here might move the the spec at some point. * link:server/implementation[Implementation] of the Eclipse MicroProfile GraphQL API. -* link:server/implementation-cdi[CDI] Module that allows lookup of GraphQL Endpoints via CDI -* link:server/implementation-servlet[Servlet] Making the implementation available via Servlet +* link:server/implementation-cdi[CDI] Module that allows lookup of GraphQL Endpoints via CDI. +* link:server/implementation-servlet[Servlet] Making the implementation available via Servlet. * link:server/tck[TCK] Test suite to run the implementation against the {microprofile-graphql}[Eclipse MicroProfile GraphQL] TCK. -* link:server/runner[Runner] Manual TCK testing with GraphiQL -* link:server/integration-tests[IT] To run some Integration tests +* link:server/runner[Runner] Manual TCK testing with GraphiQL. +* link:server/integration-tests[IT] To run some Integration tests. ==== Client -* link:client/api[Client API] (pulling in the MicroProfile Client API) and allowing us to experiment with api feature not yet in the spec. Code from here might move the the spec at some point. +* link:client/api[Client API] (pulling in the MicroProfile Client API) and allowing us to experiment with API feature not yet in the spec. Code from here might move the spec at some point. * link:client/implementation[Client Implementation] of the Eclipse MicroProfile GraphQL Client API. * link:client/tck[Client TCK] Test suite to run the client-implementation against the {microprofile-graphql}[Eclipse MicroProfile GraphQL] Client TCK. +* link:client/implementation-vertx[Client Implementation Vert.x] Module for {vertx}[Vert.x] handling for the client. +* link:client/generator[Client Generator] Module generating Type-Safe Client APIs with annotation processing. +* link:client/generator-test[Client Generator Test] Module containing tests for client generators. +* link:client/model[Client Model] defines Type-Safe's client model (GraphQL operations). +* link:client/model-builder[Client Model Builder] that generates Client Model from Jandex. ==== Tools diff --git a/client/api/pom.xml b/client/api/pom.xml index a6b1525ab..9b3429195 100644 --- a/client/api/pom.xml +++ b/client/api/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-client-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-client-api @@ -30,18 +30,12 @@ microprofile-config-api provided - io.smallrye.reactive mutiny provided - - io.smallrye - smallrye-graphql-client-model - - org.junit.jupiter junit-jupiter diff --git a/client/api/src/main/java/io/smallrye/graphql/client/core/utils/validation/NameValidation.java b/client/api/src/main/java/io/smallrye/graphql/client/core/utils/validation/NameValidation.java index 0c5747f6c..aff32c1c7 100644 --- a/client/api/src/main/java/io/smallrye/graphql/client/core/utils/validation/NameValidation.java +++ b/client/api/src/main/java/io/smallrye/graphql/client/core/utils/validation/NameValidation.java @@ -12,7 +12,8 @@ public class NameValidation { /** * The regular expression patterns for a valid GraphQL names. */ - private static final String _NAME_REGEX = "[a-zA-Z_][a-zA-Z0-9_]*"; + private static final String _IGNORED_TOKENS_REGEX = "[,\\s]*"; // for now, whitespaces and commas + private static final String _NAME_REGEX = _IGNORED_TOKENS_REGEX + "[a-zA-Z_][a-zA-Z0-9_]*" + _IGNORED_TOKENS_REGEX; private static final String _FIELD_NAME_REGEX = "^" + _NAME_REGEX + "(:" + _NAME_REGEX + ")?$"; private static final Pattern NAME_PATTERN = Pattern.compile(_NAME_REGEX); private static final Pattern FIELD_NAME_PATTERN = Pattern.compile(_FIELD_NAME_REGEX); diff --git a/client/generator-test/pom.xml b/client/generator-test/pom.xml index 39fb5bbd8..ebb5c2d95 100644 --- a/client/generator-test/pom.xml +++ b/client/generator-test/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-client-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-client-generator-test diff --git a/client/generator/pom.xml b/client/generator/pom.xml index acd3fd384..e51bd337e 100644 --- a/client/generator/pom.xml +++ b/client/generator/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-client-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-client-generator diff --git a/client/implementation-vertx/pom.xml b/client/implementation-vertx/pom.xml index e543ac90f..d243619ed 100644 --- a/client/implementation-vertx/pom.xml +++ b/client/implementation-vertx/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-client-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-client-implementation-vertx @@ -18,7 +18,7 @@ io.smallrye - smallrye-graphql-client-model-builder + smallrye-graphql-client-model io.smallrye.config @@ -29,6 +29,16 @@ microprofile-config-api provided + + jakarta.json + jakarta.json-api + provided + + + jakarta.json.bind + jakarta.json.bind-api + provided + @@ -50,5 +60,15 @@ mockito-junit-jupiter test + + io.smallrye + smallrye-graphql-client-model-builder + test + + + org.eclipse + yasson + test + diff --git a/client/implementation-vertx/src/test/java/test/tck/TypesafeTckClientModelSuite.java b/client/implementation-vertx/src/test/java/test/tck/TypesafeTckClientModelSuite.java index 010ae825a..4957dda6d 100644 --- a/client/implementation-vertx/src/test/java/test/tck/TypesafeTckClientModelSuite.java +++ b/client/implementation-vertx/src/test/java/test/tck/TypesafeTckClientModelSuite.java @@ -17,7 +17,6 @@ import org.junit.jupiter.api.Test; import org.junit.platform.suite.api.ExcludeClassNamePatterns; -import io.smallrye.graphql.client.GraphQLClient; import io.smallrye.graphql.client.model.ClientModelBuilder; import io.smallrye.graphql.client.model.ClientModels; import tck.graphql.typesafe.Animal; @@ -87,7 +86,6 @@ private static Index createIndexExcludingClasses(File directory, String classNam // SOME OTHER CLASSES TO BE ADDED TO INDEX try { indexer.indexClass(Input.class); - indexer.indexClass(GraphQLClient.class); indexer.indexClass(Closeable.class); indexer.indexClass(AutoCloseable.class); } catch (IOException e) { diff --git a/client/implementation/pom.xml b/client/implementation/pom.xml index 6a2765d59..d3da8003e 100644 --- a/client/implementation/pom.xml +++ b/client/implementation/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-client-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-client @@ -56,6 +56,11 @@ jakarta.json-api provided + + jakarta.json.bind + jakarta.json.bind-api + provided + io.smallrye.config smallrye-config @@ -66,18 +71,14 @@ jakarta.enterprise.cdi-api - - jakarta.json - jakarta.json-api - - - org.eclipse - yasson - io.smallrye.reactive mutiny + + io.smallrye + smallrye-graphql-client-model + @@ -118,5 +119,10 @@ mockito-junit-jupiter test + + org.eclipse + yasson + test + diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/RequestImpl.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/RequestImpl.java index 4085634a1..f9cddb323 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/RequestImpl.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/RequestImpl.java @@ -1,5 +1,6 @@ package io.smallrye.graphql.client.impl; +import java.io.StringReader; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -8,18 +9,15 @@ import jakarta.json.JsonBuilderFactory; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonStructure; import jakarta.json.JsonValue; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; -import org.eclipse.yasson.internal.JsonBinding; - import io.smallrye.graphql.client.Request; public class RequestImpl implements Request { - - private static final JsonBuilderFactory jsonBuilderFactory = Json.createBuilderFactory(null); + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(null); + private static final Jsonb JSONB = JsonbBuilder.create(); private final String document; private Map variables; @@ -32,7 +30,7 @@ public RequestImpl(String document) { @Override public String toJson() { - JsonObjectBuilder queryBuilder = jsonBuilderFactory.createObjectBuilder().add("query", document); + JsonObjectBuilder queryBuilder = JSON.createObjectBuilder().add("query", document); if (!variables.isEmpty()) { queryBuilder.add("variables", _formatJsonVariables()); } @@ -44,7 +42,7 @@ public String toJson() { @Override public JsonObject toJsonObject() { - JsonObjectBuilder queryBuilder = jsonBuilderFactory.createObjectBuilder().add("query", document); + JsonObjectBuilder queryBuilder = JSON.createObjectBuilder().add("query", document); if (!variables.isEmpty()) { queryBuilder.add("variables", _formatJsonVariables()); } @@ -55,7 +53,7 @@ public JsonObject toJsonObject() { } private JsonObject _formatJsonVariables() { - JsonObjectBuilder varBuilder = jsonBuilderFactory.createObjectBuilder(); + JsonObjectBuilder varBuilder = JSON.createObjectBuilder(); variables.forEach((k, v) -> { // Other types to process here @@ -69,14 +67,12 @@ private JsonObject _formatJsonVariables() { varBuilder.add(k, (Boolean) v); } else if (v instanceof Long) { varBuilder.add(k, (Long) v); + } else if (v instanceof Double) { + varBuilder.add(k, (Double) v); } else if (v == null) { varBuilder.addNull(k); } else { - try (Jsonb jsonb = JsonbBuilder.create()) { - JsonStructure struct = ((JsonBinding) jsonb).toJsonStructure(v); - varBuilder.add(k, struct); - } catch (Exception ignore) { - } + varBuilder.add(k, Json.createReader(new StringReader(JSONB.toJson(v))).read()); } }); diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/MethodInvocation.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/MethodInvocation.java index b5ec2ef80..b4e2f8485 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/MethodInvocation.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/reflection/MethodInvocation.java @@ -54,7 +54,7 @@ public String getKey() { } public MethodKey getMethodKey() { - return new MethodKey(method.getReturnType(), method.getName(), method.getParameterTypes()); + return new MethodKey(method.getName(), method.getParameterTypes()); } public OperationType getOperationType() { diff --git a/client/implementation/src/test/java/io/smallrye/graphql/client/RequestImplTest.java b/client/implementation/src/test/java/io/smallrye/graphql/client/RequestImplTest.java index 71b0cd443..6500ce896 100644 --- a/client/implementation/src/test/java/io/smallrye/graphql/client/RequestImplTest.java +++ b/client/implementation/src/test/java/io/smallrye/graphql/client/RequestImplTest.java @@ -29,6 +29,12 @@ public void testPrimitiveTypesToJson() { request.setVariable("key", "foo"); assertEquals("{\"query\":\"example\",\"variables\":{\"key\":\"foo\"}}", request.toJson()); + + request.setVariable("key", 5L); + assertEquals("{\"query\":\"example\",\"variables\":{\"key\":5}}", request.toJson()); + + request.setVariable("key", 5.5); + assertEquals("{\"query\":\"example\",\"variables\":{\"key\":5.5}}", request.toJson()); } @Test diff --git a/client/model-builder/pom.xml b/client/model-builder/pom.xml index ab7e3a064..de2a4900b 100644 --- a/client/model-builder/pom.xml +++ b/client/model-builder/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-client-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-client-model-builder @@ -26,6 +26,7 @@ jboss-logging provided + io.smallrye smallrye-graphql-client-api @@ -38,16 +39,17 @@ io.smallrye.reactive mutiny - - io.smallrye - smallrye-graphql-client - org.junit.jupiter junit-jupiter test + + io.smallrye + smallrye-graphql-api + test + diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ClientModelBuilder.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ClientModelBuilder.java index f639d4c62..ea7731e2b 100644 --- a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ClientModelBuilder.java +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/ClientModelBuilder.java @@ -22,6 +22,8 @@ * Builder class for generating client models from Jandex index. * It scans for classes annotated with {@link Annotations#GRAPHQL_CLIENT_API} and generates client models based on the annotated * methods. + * + * @author mskacelik */ public class ClientModelBuilder { @@ -60,9 +62,9 @@ private ClientModels generateClientModels() { List methods = getAllMethodsIncludingFromSuperClasses(apiClass); methods.stream().forEach(method -> { String query = new QueryBuilder(method).build(); - LOG.debugf("[%s] - Query created: %s", apiClass.name().toString(), query); + LOG.debugf("[%s] – Query created: %s", apiClass.name().toString(), query); operationMap.getOperationMap() - .put(OperationModel.of(method).getMethodKey(), query); + .putIfAbsent(OperationModel.of(method).getMethodKey(), query); }); clientModelMap.put( (graphQLApiAnnotation.value("configKey") == null) ? apiClass.name().toString() diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/QueryBuilder.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/QueryBuilder.java index a7bb86edf..791e27dc6 100644 --- a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/QueryBuilder.java +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/QueryBuilder.java @@ -11,6 +11,8 @@ /** * A utility class for building GraphQL queries based on a given {@link MethodInfo} which will be scanned thanks * to Jandex during build-time. + * + * @author mskacelik */ public class QueryBuilder { private final OperationModel method; diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/OperationModel.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/OperationModel.java index c9793ede2..4add9f429 100644 --- a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/OperationModel.java +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/OperationModel.java @@ -21,12 +21,13 @@ import org.jboss.jandex.MethodInfo; import io.smallrye.graphql.client.core.OperationType; -import io.smallrye.graphql.client.impl.SmallRyeGraphQLClientMessages; import io.smallrye.graphql.client.model.MethodKey; /** * Represents a model for a GraphQL operation method, providing methods to generate GraphQL query fields, * handle parameter bindings, and extract operation-related information. + * + * @author mskacelik */ public class OperationModel implements NamedElement { private final MethodInfo method; @@ -69,7 +70,7 @@ public static OperationModel of(MethodInfo method) { */ public String fields(TypeModel type) { if (typeStack.contains(type.getName())) - throw SmallRyeGraphQLClientMessages.msg.fieldRecursionFound(); + throw new IllegalStateException("field recursion found"); try { typeStack.push(type.getName()); return recursionCheckedFields(type); @@ -222,11 +223,10 @@ public Optional mutationName() { } /** - * Gets the name of the GraphQL subscription, considering any {@link io.smallrye.graphql.api.Subscription} annotation. + * Gets the name of the GraphQL subscription, considering any io.smallrye.graphql.api.Subscription annotation. * * @return An optional containing the subscription name if specified, otherwise empty. */ - public Optional subscriptionName() { Optional subscriptionAnnotation = getMethodAnnotation(SUBCRIPTION); if (subscriptionAnnotation.isPresent() && subscriptionAnnotation.orElseThrow().value() != null) @@ -343,10 +343,10 @@ public boolean isSingle() { /** * Gets the key for identifying the GraphQL operation method. * - * @return The {@link MethodKey} representing the key for the operation method (return type, name, parameters types). + * @return The {@link MethodKey} representing the key for the operation method (name, parameters types). */ public MethodKey getMethodKey() { - return new MethodKey(JandexReflection.loadRawType(method.returnType()), method.name(), method.parameters().stream() + return new MethodKey(method.name(), method.parameters().stream() .map(methodParameterInfo -> JandexReflection.loadRawType(methodParameterInfo.type())).toArray(Class[]::new)); } diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/ParameterModel.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/ParameterModel.java index 2b53a7f94..be3b24204 100644 --- a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/ParameterModel.java +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/ParameterModel.java @@ -27,6 +27,8 @@ public class ParameterModel implements NamedElement { private TypeModel type; private List directives; + private final static String PARAMETER_NAME_PLACEHOLDER = "arg"; + /** * Constructs a new {@code ParameterModel} instance based on the provided Jandex {@link MethodParameterInfo}. * @@ -107,7 +109,8 @@ public String getName() { } public String getRawName() { - return parameter.name(); + String rawName = parameter.name(); + return (rawName != null) ? rawName : PARAMETER_NAME_PLACEHOLDER + parameter.position(); } @Override diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeModel.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeModel.java index 2de3a784f..93bdd0c80 100644 --- a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeModel.java +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/helper/TypeModel.java @@ -7,9 +7,7 @@ import static io.smallrye.graphql.client.model.Classes.OBJECT; import static io.smallrye.graphql.client.model.Classes.OPTIONAL; import static io.smallrye.graphql.client.model.Classes.TYPESAFE_RESPONSE; -import static io.smallrye.graphql.client.model.Classes.isParameterized; import static io.smallrye.graphql.client.model.ScanningContext.getIndex; -import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import java.lang.reflect.Modifier; @@ -384,7 +382,7 @@ public Stream fields() { * @return A stream of {@link FieldModel} instances. */ private Stream fields(ClassInfo clazz) { - return (clazz == null) ? Stream.of() + return (clazz == null || clazz.superClassType() == null) ? Stream.of() : Stream.concat( fields(getIndex().getClassByName(clazz.superClassType().name())), // to superClass fieldsHelper(clazz) diff --git a/client/model-builder/src/test/java/io/smallrye/graphql/client/model/ClientModelBuilderTest.java b/client/model-builder/src/test/java/io/smallrye/graphql/client/model/ClientModelBuilderTest.java index fbbd65a7f..5b04a8a3d 100644 --- a/client/model-builder/src/test/java/io/smallrye/graphql/client/model/ClientModelBuilderTest.java +++ b/client/model-builder/src/test/java/io/smallrye/graphql/client/model/ClientModelBuilderTest.java @@ -19,6 +19,8 @@ import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; /** + * Testing query building using the client model implementation. + * * @author mskacelik */ public class ClientModelBuilderTest { @@ -43,15 +45,15 @@ void sclarClientModelTest() throws IOException { ClientModel clientModel = clientModels.getClientModelByConfigKey(configKey); assertEquals(3, clientModel.getOperationMap().size()); assertOperation(clientModel, - new MethodKey(Integer.class, "returnInteger", new Class[] { Integer.class }), + new MethodKey("returnInteger", new Class[] { Integer.class }), "query returnInteger($someNumber: Int) { returnInteger(someNumber: $someNumber) }"); assertOperation(clientModel, - new MethodKey(String.class, "returnString", new Class[] { String.class }), + new MethodKey("returnString", new Class[] { String.class }), "mutation returnString($someString: String) { returnString(someString: $someString) }"); assertOperation(clientModel, - new MethodKey(float.class, "returnNonNullFloat", new Class[] { float.class }), + new MethodKey("returnNonNullFloat", new Class[] { float.class }), "subscription returnNonNullFloat($someFloat: Float!) { returnNonNullFloat(someFloat: $someFloat) }"); } @@ -75,15 +77,15 @@ void collectionClientModelTest() throws IOException { ClientModel clientModel = clientModels.getClientModelByConfigKey(configKey); assertEquals(3, clientModel.getOperationMap().size()); assertOperation(clientModel, - new MethodKey(Collection.class, "returnIntegerCollection", new Class[] { Integer[].class }), + new MethodKey("returnIntegerCollection", new Class[] { Integer[].class }), "query returnIntegerCollection($someNumbers: [Int]) { returnIntegerCollection(someNumbers: $someNumbers) }"); assertOperation(clientModel, - new MethodKey(List.class, "returnStringList", new Class[] { Set.class }), + new MethodKey("returnStringList", new Class[] { Set.class }), "mutation returnStringList($someStrings: [String]) { returnStringList(someStrings: $someStrings) }"); assertOperation(clientModel, - new MethodKey(ArrayList.class, "returnFloatArrayList", new Class[] { HashSet.class }), + new MethodKey("returnFloatArrayList", new Class[] { HashSet.class }), "subscription returnFloatArrayList($someFloats: [Float]) { returnFloatArrayList(someFloats: $someFloats) }"); } @@ -96,7 +98,7 @@ void simpleObjectClientModelTest() throws IOException { ClientModel clientModel = clientModels.getClientModelByConfigKey(configKey); assertEquals(1, clientModel.getOperationMap().size()); assertOperation(clientModel, - new MethodKey(SimpleObjectClientApi.SomeObject.class, "returnSomeObject", + new MethodKey("returnSomeObject", new Class[] { SimpleObjectClientApi.SomeObject.class }), "query returnSomeObject($someObject: SomeObjectInput) { returnSomeObject(someObject: $someObject) {name innerObject {someInt}} }"); } @@ -130,7 +132,7 @@ void complexObjectClientModelTest() throws IOException { ClientModel clientModel = clientModels.getClientModelByConfigKey(configKey); assertEquals(1, clientModel.getOperationMap().size()); assertOperation(clientModel, - new MethodKey(List.class, "returnSomeGenericObject", new Class[] { List.class }), + new MethodKey("returnSomeGenericObject", new Class[] { List.class }), "query returnSomeGenericObject($someObject: [SomeGenericClassInput])" + " { returnSomeGenericObject(someObject: $someObject) {somethingParent {number}" + " field1 {number} field2 {innerField {number} something {someObject {someThingElse" + @@ -174,6 +176,40 @@ class SomeObjectFive { } } + @GraphQLClientApi(configKey = "string-api") + interface StringApiChild extends StringApiParent { + @Query("strings") + List allStrings(); + + List allStrings0(); + + } + + interface StringApiParent { + List allStrings(); + + List allStrings2(); + } + + @Test + void inheritedOperationsClientModelTest() throws IOException { + String configKey = "string-api"; + ClientModels clientModels = ClientModelBuilder + .build(Index.of(StringApiChild.class, StringApiParent.class)); + assertNotNull(clientModels.getClientModelByConfigKey(configKey)); + ClientModel clientModel = clientModels.getClientModelByConfigKey(configKey); + assertEquals(3, clientModel.getOperationMap().size()); + assertOperation(clientModel, + new MethodKey("allStrings", new Class[0]), + "query strings { strings }"); + assertOperation(clientModel, + new MethodKey("allStrings2", new Class[0]), + "query allStrings2 { allStrings2 }"); + assertOperation(clientModel, + new MethodKey("allStrings0", new Class[0]), + "query allStrings0 { allStrings0 }"); + } + private void assertOperation(ClientModel clientModel, MethodKey methodKey, String expectedQuery) { String actualQuery = clientModel.getOperationMap().get(methodKey); assertEquals(expectedQuery, actualQuery); diff --git a/client/model/pom.xml b/client/model/pom.xml index 1c0bc5b96..62e646d51 100644 --- a/client/model/pom.xml +++ b/client/model/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-client-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-client-model diff --git a/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModel.java b/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModel.java index de8a49886..fb485c539 100644 --- a/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModel.java +++ b/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModel.java @@ -3,6 +3,12 @@ import java.util.HashMap; import java.util.Map; +/** + * Represents a model object used by Quarkus during both build-time and runtime. + * This class encapsulates all of the operation queries for a single client API. + * + * @author mskacelik + */ public class ClientModel { private Map operationQueryMap; @@ -11,6 +17,7 @@ public ClientModel() { operationQueryMap = new HashMap<>(); }; + // bytecode recording public Map getOperationMap() { return operationQueryMap; } @@ -19,6 +26,7 @@ public void setOperationMap(Map operationQueryMap) { this.operationQueryMap = operationQueryMap; } + // for testing purposes... @Override public String toString() { return "ClientModel{" + diff --git a/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModels.java b/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModels.java index 5430fa33f..35a13a30b 100644 --- a/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModels.java +++ b/client/model/src/main/java/io/smallrye/graphql/client/model/ClientModels.java @@ -3,6 +3,13 @@ import java.util.HashMap; import java.util.Map; +/** + * Represents an object for all `ClientModel` (based on their configuration keys) + * instances within Quarkus application. + * + * @author mskacelik + */ + public class ClientModels { private Map clientModelMap; @@ -10,11 +17,11 @@ public ClientModel getClientModelByConfigKey(String configKey) { return clientModelMap.get(configKey); } - // bytecode recording public ClientModels() { clientModelMap = new HashMap<>(); } + // bytecode recording public void setClientModelMap(Map clientModelMap) { this.clientModelMap = clientModelMap; } diff --git a/client/model/src/main/java/io/smallrye/graphql/client/model/MethodKey.java b/client/model/src/main/java/io/smallrye/graphql/client/model/MethodKey.java index 3dbc06c20..b420c202c 100644 --- a/client/model/src/main/java/io/smallrye/graphql/client/model/MethodKey.java +++ b/client/model/src/main/java/io/smallrye/graphql/client/model/MethodKey.java @@ -3,13 +3,18 @@ import java.util.Arrays; import java.util.Objects; +/** + * Represents a unique identifier for an Client API operation (method). This class + * combines the method name and its parameter types to create a key that can + * be identified by both Jandex and Java reflection. + * + * @author mskacelik + */ public class MethodKey { - private Class declaringClass; private String methodName; private Class[] parameterTypes; - public MethodKey(Class declaringClass, String methodName, Class[] parameterTypes) { - this.declaringClass = declaringClass; + public MethodKey(String methodName, Class[] parameterTypes) { this.methodName = methodName; this.parameterTypes = parameterTypes; } @@ -24,25 +29,17 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; MethodKey methodKey = (MethodKey) o; - return Objects.equals(declaringClass, methodKey.declaringClass) && Objects.equals(methodName, methodKey.methodName) - && Arrays.equals(parameterTypes, methodKey.parameterTypes); + return Objects.equals(methodName, methodKey.methodName) && Arrays.equals(parameterTypes, methodKey.parameterTypes); } @Override public int hashCode() { - int result = Objects.hash(declaringClass, methodName); + int result = Objects.hash(methodName); result = 31 * result + Arrays.hashCode(parameterTypes); return result; } - public Class getDeclaringClass() { - return declaringClass; - } - - public void setDeclaringClass(Class declaringClass) { - this.declaringClass = declaringClass; - } - + // set methods for bytecode recording public String getMethodName() { return methodName; } @@ -62,8 +59,7 @@ public void setParameterTypes(Class[] parameterTypes) { @Override public String toString() { return "MethodKey{" + - "declaringClass=" + declaringClass + - ", methodName='" + methodName + '\'' + + "methodName='" + methodName + '\'' + ", parameterTypes=" + Arrays.toString(parameterTypes) + '}'; } diff --git a/client/pom.xml b/client/pom.xml index 50bab80fe..66f3f0bb4 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-client-parent diff --git a/client/tck/pom.xml b/client/tck/pom.xml index 77ab94267..b4220a7ec 100644 --- a/client/tck/pom.xml +++ b/client/tck/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-client-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-client-tck @@ -39,6 +39,14 @@ jakarta.enterprise jakarta.enterprise.cdi-api + + jakarta.json + jakarta.json-api + + + jakarta.json.bind + jakarta.json.bind-api + com.graphql-java @@ -79,5 +87,10 @@ io.smallrye smallrye-graphql-client + + org.eclipse + yasson + compile + diff --git a/client/tck/src/main/java/tck/graphql/dynamic/core/FieldsTest.java b/client/tck/src/main/java/tck/graphql/dynamic/core/FieldsTest.java index cf987d021..dd0d57b29 100644 --- a/client/tck/src/main/java/tck/graphql/dynamic/core/FieldsTest.java +++ b/client/tck/src/main/java/tck/graphql/dynamic/core/FieldsTest.java @@ -58,6 +58,11 @@ public void fieldShouldNotThrowExceptionForValidNameTest() { assertDoesNotThrow(() -> field("_:v1")); assertDoesNotThrow(() -> field("___")); assertDoesNotThrow(() -> field("o")); + assertDoesNotThrow(() -> field(" valid_name ")); + assertDoesNotThrow(() -> field("o1: b")); + assertDoesNotThrow(() -> field("o12 ,,, : ,,, bbbee1")); + assertDoesNotThrow(() -> field(",,,,valid_name,,,,")); + assertDoesNotThrow(() -> field("foo:\tbar")); } @Test @@ -66,7 +71,6 @@ public void fieldShouldThrowExceptionForInvalidNameTest() { assertThrows(IllegalArgumentException.class, () -> field("")); assertThrows(IllegalArgumentException.class, () -> field("invalid_nam&e")); assertThrows(IllegalArgumentException.class, () -> field("1invalid_name")); - assertThrows(IllegalArgumentException.class, () -> field(" invalid_name")); assertThrows(IllegalArgumentException.class, () -> field(":invalid_name")); assertThrows(IllegalArgumentException.class, () -> field("invalid name")); assertThrows(IllegalArgumentException.class, () -> field("invalid_name:")); @@ -74,5 +78,7 @@ public void fieldShouldThrowExceptionForInvalidNameTest() { assertThrows(IllegalArgumentException.class, () -> field("a:1")); assertThrows(IllegalArgumentException.class, () -> field("invalid::name")); assertThrows(IllegalArgumentException.class, () -> field("field:name:name2")); + assertThrows(IllegalArgumentException.class, () -> field(" invalid,name")); + assertThrows(IllegalArgumentException.class, () -> field("invalid\tname")); } } diff --git a/client/tck/src/main/java/tck/graphql/dynamic/core/FragmentReferencesTest.java b/client/tck/src/main/java/tck/graphql/dynamic/core/FragmentReferencesTest.java index 0c23fe069..ed59834be 100644 --- a/client/tck/src/main/java/tck/graphql/dynamic/core/FragmentReferencesTest.java +++ b/client/tck/src/main/java/tck/graphql/dynamic/core/FragmentReferencesTest.java @@ -20,6 +20,7 @@ public void fragmentReferencesShouldNotThrowExceptionForValidName() { assertDoesNotThrow(() -> fragmentRef("_")); assertDoesNotThrow(() -> fragmentRef("frag_ment")); assertDoesNotThrow(() -> fragmentRef("one")); + assertDoesNotThrow(() -> fragmentRef(" two,, ,")); } @Test @@ -33,5 +34,6 @@ public void fragmentReferencesShouldThrowExceptionForInvalidName() { assertThrows(IllegalArgumentException.class, () -> fragmentRef("in:valid")); assertThrows(IllegalArgumentException.class, () -> fragmentRef("inv@lid")); assertThrows(IllegalArgumentException.class, () -> fragmentRef("on")); + assertThrows(IllegalArgumentException.class, () -> fragmentRef(" invalid_frag,ment")); } } diff --git a/client/tck/src/main/java/tck/graphql/dynamic/core/FragmentsTest.java b/client/tck/src/main/java/tck/graphql/dynamic/core/FragmentsTest.java index 8cd3f6048..ae20b3c17 100644 --- a/client/tck/src/main/java/tck/graphql/dynamic/core/FragmentsTest.java +++ b/client/tck/src/main/java/tck/graphql/dynamic/core/FragmentsTest.java @@ -46,6 +46,7 @@ public void fragmentsShouldNotThrowExceptionForValidNameTest() { assertDoesNotThrow(() -> fragment("_")); assertDoesNotThrow(() -> fragment("frag_ment")); assertDoesNotThrow(() -> fragment("one")); + assertDoesNotThrow(() -> fragment(",,,two ")); } @Test @@ -60,5 +61,6 @@ public void fragmentsShouldThrowExceptionForInvalidNameTest() { assertThrows(IllegalArgumentException.class, () -> fragment("...fragmentinvalid")); assertThrows(IllegalArgumentException.class, () -> fragment("inv@lid")); assertThrows(IllegalArgumentException.class, () -> fragment("on")); + assertThrows(IllegalArgumentException.class, () -> fragment("frag,ment")); } } diff --git a/client/tck/src/main/java/tck/graphql/dynamic/core/OperationsTest.java b/client/tck/src/main/java/tck/graphql/dynamic/core/OperationsTest.java index 7899c587b..f09f320b3 100644 --- a/client/tck/src/main/java/tck/graphql/dynamic/core/OperationsTest.java +++ b/client/tck/src/main/java/tck/graphql/dynamic/core/OperationsTest.java @@ -16,6 +16,7 @@ public void operationsShouldNotThrowExceptionForValidNameTest() { assertDoesNotThrow(() -> operation("_myOperation")); assertDoesNotThrow(() -> operation("my_operation")); assertDoesNotThrow(() -> operation("my123Operation")); + assertDoesNotThrow(() -> operation(",, ,myOperation ,")); assertDoesNotThrow(() -> operation("o")); assertDoesNotThrow(() -> operation("_")); assertDoesNotThrow(() -> operation("op_eration")); @@ -35,6 +36,7 @@ public void operationsShouldThrowExceptionForInvalidNameTest() { assertThrows(IllegalArgumentException.class, () -> operation("InvalidName::")); assertThrows(IllegalArgumentException.class, () -> operation("@InvalidName")); assertThrows(IllegalArgumentException.class, () -> operation("my.Operation")); + assertThrows(IllegalArgumentException.class, () -> operation("my,Operation")); assertThrows(IllegalArgumentException.class, () -> operation("my-Operation")); } } diff --git a/client/tck/src/main/java/tck/graphql/dynamic/core/VariableTypesTest.java b/client/tck/src/main/java/tck/graphql/dynamic/core/VariableTypesTest.java index 894b6b6bc..76b2f9310 100644 --- a/client/tck/src/main/java/tck/graphql/dynamic/core/VariableTypesTest.java +++ b/client/tck/src/main/java/tck/graphql/dynamic/core/VariableTypesTest.java @@ -18,12 +18,14 @@ public void varTypesShouldNotThrowExceptionForValidName() { assertDoesNotThrow(() -> varType("v")); assertDoesNotThrow(() -> varType("_")); assertDoesNotThrow(() -> varType("va_lid")); + assertDoesNotThrow(() -> varType(" ,va_lid,, ")); assertDoesNotThrow(() -> varType("_valid")); } @Test public void varTypesShouldThrowExceptionForInvalidName() { assertThrows(IllegalArgumentException.class, () -> varType("Invalid:Name")); + assertThrows(IllegalArgumentException.class, () -> varType("Invalid,Name")); assertThrows(IllegalArgumentException.class, () -> varType("123InvalidName")); assertThrows(IllegalArgumentException.class, () -> varType("invalid-Name")); assertThrows(IllegalArgumentException.class, () -> varType("invalid name")); @@ -39,12 +41,14 @@ public void nonNullsShouldNotThrowExceptionForValidName() { assertDoesNotThrow(() -> nonNull("v")); assertDoesNotThrow(() -> nonNull("_")); assertDoesNotThrow(() -> nonNull("va_lid")); + assertDoesNotThrow(() -> nonNull(" ,va_lid,, ")); assertDoesNotThrow(() -> nonNull("_valid")); } @Test public void nonNullsShouldThrowExceptionForInvalidName() { assertThrows(IllegalArgumentException.class, () -> nonNull("Invalid:Name")); + assertThrows(IllegalArgumentException.class, () -> nonNull("Invalid,Name")); assertThrows(IllegalArgumentException.class, () -> nonNull("123InvalidName")); assertThrows(IllegalArgumentException.class, () -> nonNull("invalid-Name")); assertThrows(IllegalArgumentException.class, () -> nonNull("invalid name")); @@ -60,12 +64,14 @@ public void listsShouldNotThrowExceptionForValidName() { assertDoesNotThrow(() -> list("v")); assertDoesNotThrow(() -> list("_")); assertDoesNotThrow(() -> list("va_lid")); + assertDoesNotThrow(() -> list(" ,va_lid,, ")); assertDoesNotThrow(() -> list("_valid")); } @Test public void listsShouldThrowExceptionForInvalidName() { assertThrows(IllegalArgumentException.class, () -> list("Invalid:Name")); + assertThrows(IllegalArgumentException.class, () -> list("Invalid,Name")); assertThrows(IllegalArgumentException.class, () -> list("123InvalidName")); assertThrows(IllegalArgumentException.class, () -> list("invalidName-")); assertThrows(IllegalArgumentException.class, () -> list("invalid name")); diff --git a/common/pom.xml b/common/pom.xml index d5104a18e..032b6d736 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-common-parent diff --git a/common/schema-builder/pom.xml b/common/schema-builder/pom.xml index adc385a6b..f6c96bdd3 100644 --- a/common/schema-builder/pom.xml +++ b/common/schema-builder/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-common-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-schema-builder @@ -19,6 +19,10 @@ ${project.groupId} smallrye-graphql-api + + com.graphql-java + graphql-java + 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 5dbce5505..3123a0d64 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 @@ -512,7 +512,6 @@ private static Map getTypeUseAnnotations(org.jboss. private static Map getAnnotations(org.jboss.jandex.Type type) { Map annotationMap = new HashMap<>(); - if (type.kind().equals(org.jboss.jandex.Type.Kind.PARAMETERIZED_TYPE)) { org.jboss.jandex.Type typeInCollection = type.asParameterizedType().arguments().get(0); annotationMap.putAll(getAnnotations(typeInCollection)); @@ -522,7 +521,6 @@ private static Map getAnnotations(org.jboss.jandex. annotationMap.put(annotationInstance.name(), annotationInstance); } } - return annotationMap; } diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/WrapperCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/WrapperCreator.java index 08743268d..2ad27f988 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/WrapperCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/WrapperCreator.java @@ -86,18 +86,16 @@ private static boolean markParameterizedTypeNonNull(Type fieldType, Type methodT Annotations annotationsInParameterizedType = Annotations.getAnnotationsForArray(typeInCollection, methodTypeInCollection); - return NonNullHelper.markAsNonNull(typeInCollection, annotationsInParameterizedType, true); + return NonNullHelper.markAsNonNull(typeInCollection, annotationsInParameterizedType, false); } return false; } private static Type getTypeInCollection(Type type) { if (Classes.isArray(type)) { - Type typeInArray = type.asArrayType().component(); - return getTypeInCollection(typeInArray); + return type.asArrayType().componentType(); } else if (Classes.isParameterized(type)) { - Type typeInCollection = type.asParameterizedType().arguments().get(0); - return getTypeInCollection(typeInCollection); + return type.asParameterizedType().arguments().get(0); } return type; } 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 b4e6ccac7..fe4209572 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 @@ -12,6 +12,7 @@ import io.smallrye.graphql.schema.Annotations; import io.smallrye.graphql.schema.ScanningContext; +import io.smallrye.graphql.schema.SchemaBuilderException; import io.smallrye.graphql.schema.creator.OperationCreator; import io.smallrye.graphql.schema.creator.ReferenceCreator; import io.smallrye.graphql.schema.helper.DescriptionHelper; @@ -112,8 +113,8 @@ protected void addOperations(Type type, ClassInfo classInfo) { SourceOperationHelper sourceOperationHelper = new SourceOperationHelper(); Map> sourceFields = sourceOperationHelper.getSourceAnnotations(); Map> batchedFields = sourceOperationHelper.getSourceListAnnotations(); - type.setOperations(toOperations(sourceFields, type, classInfo)); - type.setBatchOperations(toOperations(batchedFields, type, classInfo)); + type.setOperations(validateOperationNames(toOperations(sourceFields, type, classInfo), type)); + type.setBatchOperations(validateOperationNames(toOperations(batchedFields, type, classInfo), type)); } protected Map toOperations(Map> sourceFields, Type type, @@ -152,4 +153,21 @@ protected Map toOperations(Map validateOperationNames(Map operations, Type type) { + operations.keySet().forEach(fieldName -> { + if (type.getFields().keySet().contains(fieldName)) { + throw new SchemaBuilderException(String.format("Type '%s' already contains field named '%s'" + + " so source field, with the same name, cannot be applied. You can resolve this conflict using @Ignore on the type's field.", + type.getName(), + fieldName)); + } + }); + return operations; + } } diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/TypeCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/TypeCreator.java index 46f36b86f..df97861cd 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/TypeCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/type/TypeCreator.java @@ -1,11 +1,6 @@ package io.smallrye.graphql.schema.creator.type; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -45,28 +40,34 @@ public TypeCreator(ReferenceCreator referenceCreator, FieldCreator fieldCreator, @Override protected void addFields(Type type, ClassInfo classInfo, Reference reference) { // Fields - List allMethods = new ArrayList<>(); + List> allMethods = new LinkedList<>(); Map allFields = new HashMap<>(); // Find all methods and properties up the tree for (ClassInfo c = classInfo; c != null; c = ScanningContext.getIndex().getClassByName(c.superName())) { + List listMethods = new ArrayList<>(); + if (InterfaceCreator.canAddInterfaceIntoScheme(c.toString())) { // Not java objects List classMethods = filterOutBridgeMethod(c.methods()); - allMethods.addAll(classMethods); - allMethods.addAll(getAllInterfaceMethods(c, classMethods + listMethods.addAll(getAllInterfaceMethods(c, classMethods .stream() .map(MethodInfo::toString) .collect(Collectors.toSet()))); + listMethods.addAll(classMethods); for (FieldInfo fieldInfo : c.fields()) { allFields.putIfAbsent(fieldInfo.name(), fieldInfo); } } + + allMethods.add(0, listMethods); } - for (MethodInfo methodInfo : allMethods) { - if (MethodHelper.isPropertyMethod(Direction.OUT, methodInfo)) { - String fieldName = MethodHelper.getPropertyName(Direction.OUT, methodInfo.name()); - FieldInfo fieldInfo = allFields.remove(fieldName); - fieldCreator.createFieldForPojo(Direction.OUT, fieldInfo, methodInfo, reference).ifPresent(type::addField); + for (List listMethods : allMethods) { + for (MethodInfo methodInfo : listMethods) { + if (MethodHelper.isPropertyMethod(Direction.OUT, methodInfo)) { + String fieldName = MethodHelper.getPropertyName(Direction.OUT, methodInfo.name()); + FieldInfo fieldInfo = allFields.remove(fieldName); + fieldCreator.createFieldForPojo(Direction.OUT, fieldInfo, methodInfo, reference).ifPresent(type::addField); + } } } 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 e8c43cf0b..aeeccbedd 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 @@ -18,10 +18,13 @@ import org.jboss.jandex.MethodInfo; import org.jboss.logging.Logger; +import graphql.language.StringValue; +import io.smallrye.graphql.api.federation.Key; import io.smallrye.graphql.api.federation.policy.Policy; import io.smallrye.graphql.api.federation.requiresscopes.RequiresScopes; import io.smallrye.graphql.schema.Annotations; import io.smallrye.graphql.schema.ScanningContext; +import io.smallrye.graphql.schema.SchemaBuilderException; import io.smallrye.graphql.schema.model.DirectiveInstance; import io.smallrye.graphql.schema.model.DirectiveType; @@ -56,16 +59,14 @@ public List buildDirectiveInstances(Annotations annotations, return directiveTypes.keySet().stream() .flatMap(annotations::resolve) .map(this::toDirectiveInstance) - .filter(directiveInstance -> { + .peek(directiveInstance -> { if (!directiveInstance.getType().getLocations().contains(directiveLocation)) { - LOG.warnf( + throw new SchemaBuilderException(String.format( "Directive instance: '%s' assigned to '%s' cannot be applied." + " The directive is allowed on locations '%s' but on '%s'", directiveInstance.getType().getClassName(), referenceName, - directiveInstance.getType().getLocations(), directiveLocation); - return false; + directiveInstance.getType().getLocations(), directiveLocation)); } - return true; }) .collect(toList()); } @@ -82,7 +83,12 @@ private DirectiveInstance toDirectiveInstance(AnnotationInstance annotationInsta // For both of these directives, we need to process the annotation values as nested arrays of strings directiveInstance.setValue(annotationValueName, valueObjectNestedList(annotationValue)); } else { - directiveInstance.setValue(annotationValueName, valueObject(annotationValue)); + if (directiveType.getClassName().equals(Key.class.getName()) && annotationValueName.equals("fields")) { + directiveInstance.setValue(annotationValueName, + new StringValue((String) valueObject(annotationValue.asNested().value()))); + } else { + directiveInstance.setValue(annotationValueName, valueObject(annotationValue)); + } } } diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/SchemaBuilderTest.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/SchemaBuilderTest.java index 693ff0f28..a12a058cf 100644 --- a/common/schema-builder/src/test/java/io/smallrye/graphql/index/SchemaBuilderTest.java +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/SchemaBuilderTest.java @@ -118,20 +118,81 @@ public void testConcurrentSchemaBuilding() throws Exception { * Test a schema where two Java classes map to the same GraphQL type. Such schema should not be allowed to create. */ @Test - public void testSchemaWithDuplicates() { + public void testSchemaWithTypeNameDuplicates() { try { Indexer indexer = new Indexer(); - indexDirectory(indexer, "io/smallrye/graphql/index/duplicates"); - indexDirectory(indexer, "io/smallrye/graphql/index/duplicates/a"); - indexDirectory(indexer, "io/smallrye/graphql/index/duplicates/b"); + indexDirectory(indexer, "io/smallrye/graphql/index/duplicates/typename"); + indexDirectory(indexer, "io/smallrye/graphql/index/duplicates/typename/a"); + indexDirectory(indexer, "io/smallrye/graphql/index/duplicates/typename/b"); IndexView index = indexer.complete(); SchemaBuilder.build(index); Assertions.fail("Schema should not build when there are multiple classes mapped to the same type"); } catch (SchemaBuilderException e) { // ok + assertEquals("Classes io.smallrye.graphql.index.duplicates.typename.a.Animal " + + "and io.smallrye.graphql.index.duplicates.typename.b.Animal map to the same GraphQL type " + + "'Animal', consider using the @Name annotation or a different naming strategy to " + + "distinguish between them", + e.getMessage()); } } + @Test + public void testSchemaWithSourceFieldNameDuplicates() { + try { + Indexer indexer = new Indexer(); + indexDirectory(indexer, "io/smallrye/graphql/index/duplicates/source"); + indexDirectory(indexer, "io/smallrye/graphql/index/duplicates/source/sourcefield"); + IndexView index = indexer.complete(); + SchemaBuilder.build(index); + Assertions.fail("Schema should not build when there are both field and source field with the same defined name"); + } catch (SchemaBuilderException e) { + // ok + assertEquals("Type 'SomeClass' already contains field named 'password' so source field, " + + "with the same name, cannot be applied. You can resolve this conflict using @Ignore on the type's field.", + e.getMessage()); + } + } + + @Test + public void testSchemaWithBatchSourceFieldNameDuplicates() { + try { + Indexer indexer = new Indexer(); + indexDirectory(indexer, "io/smallrye/graphql/index/duplicates/source"); + indexDirectory(indexer, "io/smallrye/graphql/index/duplicates/source/batch"); + IndexView index = indexer.complete(); + SchemaBuilder.build(index); + Assertions.fail("Schema should not build when there are both field and source field with the same defined name"); + } catch (SchemaBuilderException e) { + // ok + assertEquals( + "Type 'SomeClass' already contains field named 'password' so source field, with the same name, cannot be applied. You can resolve this conflict using @Ignore on the type's field.", + e.getMessage()); + } + } + + @Test + public void testSchemaWithInheritFieldBySubtype() { + Indexer indexer = new Indexer(); + indexDirectory(indexer, "io/smallrye/graphql/index/inherit/"); + + IndexView index = indexer.complete(); + Schema schema = SchemaBuilder.build(index); + // Get Types + Map types = schema.getTypes(); + Map interfaces = schema.getInterfaces(); + Type containerType = types.get("ContainerType"); + Field containerTypeField = containerType.getFields().get("inheritField"); + Type containerInterface = interfaces.get("ContainerInterface"); + Field containerInterfaceField = containerInterface.getFields().get("inheritField"); + + // Should be FieldType according to GraphQL Spec: https://spec.graphql.org/October2021/#sec-Objects.Type-Validation + assertEquals("io.smallrye.graphql.index.inherit.FieldType", + containerTypeField.getReference().getClassName()); + assertEquals("io.smallrye.graphql.index.inherit.FieldInterface", + containerInterfaceField.getReference().getClassName()); + } + @Test public void testSchemaWithDirectives() throws IOException { Indexer indexer = new Indexer(); @@ -155,7 +216,8 @@ public void testSchemaWithDirectives() throws IOException { assertEquals("someDirective", someDirective.getName()); assertEquals(SomeDirective.class.getName(), someDirective.getClassName()); assertEquals(singleton("value"), someDirective.argumentNames()); - assertEquals(new HashSet<>(asList("INTERFACE", "FIELD_DEFINITION", "OBJECT")), someDirective.getLocations()); + assertEquals(new HashSet<>(asList("INTERFACE", "FIELD_DEFINITION", "OBJECT", "INPUT_OBJECT", "INPUT_FIELD_DEFINITION")), + someDirective.getLocations()); // check directive instances on type Type movie = schema.getTypes().get("Movie"); diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/app/SomeDirective.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/app/SomeDirective.java index e25826c0a..1695e268a 100644 --- a/common/schema-builder/src/test/java/io/smallrye/graphql/index/app/SomeDirective.java +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/app/SomeDirective.java @@ -1,6 +1,8 @@ package io.smallrye.graphql.index.app; import static io.smallrye.graphql.api.DirectiveLocation.FIELD_DEFINITION; +import static io.smallrye.graphql.api.DirectiveLocation.INPUT_FIELD_DEFINITION; +import static io.smallrye.graphql.api.DirectiveLocation.INPUT_OBJECT; import static io.smallrye.graphql.api.DirectiveLocation.INTERFACE; import static io.smallrye.graphql.api.DirectiveLocation.OBJECT; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -9,7 +11,7 @@ import io.smallrye.graphql.api.Directive; -@Directive(on = { OBJECT, INTERFACE, FIELD_DEFINITION }) +@Directive(on = { OBJECT, INTERFACE, FIELD_DEFINITION, INPUT_OBJECT, INPUT_FIELD_DEFINITION }) @Retention(RUNTIME) public @interface SomeDirective { String[] value(); diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/ApiWithDuplicates.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/ApiWithDuplicates.java deleted file mode 100644 index 4e95b788f..000000000 --- a/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/ApiWithDuplicates.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.smallrye.graphql.index.duplicates; - -import org.eclipse.microprofile.graphql.GraphQLApi; -import org.eclipse.microprofile.graphql.Query; - -@GraphQLApi -public class ApiWithDuplicates { - - @Query - public io.smallrye.graphql.index.duplicates.a.Animal queryA() { - return null; - } - - @Query - public io.smallrye.graphql.index.duplicates.b.Animal queryB() { - return null; - } -} diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/source/SomeClass.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/source/SomeClass.java new file mode 100644 index 000000000..36206151e --- /dev/null +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/source/SomeClass.java @@ -0,0 +1,30 @@ +package io.smallrye.graphql.index.duplicates.source; + +public class SomeClass { + private String name; + private String password; + + public SomeClass() { + } + + public SomeClass(String name, String password) { + this.name = name; + this.password = password; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/source/batch/ApiWithBatchSourceFieldNameDuplicates.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/source/batch/ApiWithBatchSourceFieldNameDuplicates.java new file mode 100644 index 000000000..249598fb7 --- /dev/null +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/source/batch/ApiWithBatchSourceFieldNameDuplicates.java @@ -0,0 +1,23 @@ +package io.smallrye.graphql.index.duplicates.source.batch; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.eclipse.microprofile.graphql.Source; + +import io.smallrye.graphql.index.duplicates.source.SomeClass; + +@GraphQLApi +public class ApiWithBatchSourceFieldNameDuplicates { + + public Collection getPassword(@Source Collection someClasses) { + return List.of("my new password 1", "my new password 2"); + } + + @Query + public Collection getSomeClass() { + return List.of(new SomeClass("hello1", "password1"), new SomeClass("hello2", "password2")); + } +} diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/source/sourcefield/ApiWithSourceFieldNameDuplicates.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/source/sourcefield/ApiWithSourceFieldNameDuplicates.java new file mode 100644 index 000000000..7728af2c5 --- /dev/null +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/source/sourcefield/ApiWithSourceFieldNameDuplicates.java @@ -0,0 +1,20 @@ +package io.smallrye.graphql.index.duplicates.source.sourcefield; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.eclipse.microprofile.graphql.Source; + +import io.smallrye.graphql.index.duplicates.source.SomeClass; + +@GraphQLApi +public class ApiWithSourceFieldNameDuplicates { + + public String getPassword(@Source SomeClass someClass) { + return "new Password"; + } + + @Query + public SomeClass getSomeClass() { + return new SomeClass("hello", "password"); + } +} diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/typename/ApiWithTypeNameDuplicates.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/typename/ApiWithTypeNameDuplicates.java new file mode 100644 index 000000000..57b86248f --- /dev/null +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/typename/ApiWithTypeNameDuplicates.java @@ -0,0 +1,18 @@ +package io.smallrye.graphql.index.duplicates.typename; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; + +@GraphQLApi +public class ApiWithTypeNameDuplicates { + + @Query + public io.smallrye.graphql.index.duplicates.typename.a.Animal queryA() { + return null; + } + + @Query + public io.smallrye.graphql.index.duplicates.typename.b.Animal queryB() { + return null; + } +} diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/b/Animal.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/typename/a/Animal.java similarity index 76% rename from common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/b/Animal.java rename to common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/typename/a/Animal.java index c7f67c6eb..1ff16f9a6 100644 --- a/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/b/Animal.java +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/typename/a/Animal.java @@ -1,4 +1,4 @@ -package io.smallrye.graphql.index.duplicates.b; +package io.smallrye.graphql.index.duplicates.typename.a; public class Animal { diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/a/Animal.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/typename/b/Animal.java similarity index 76% rename from common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/a/Animal.java rename to common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/typename/b/Animal.java index e15f71741..5a9a5a8ba 100644 --- a/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/a/Animal.java +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/duplicates/typename/b/Animal.java @@ -1,4 +1,4 @@ -package io.smallrye.graphql.index.duplicates.a; +package io.smallrye.graphql.index.duplicates.typename.b; public class Animal { diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/ContainerInterface.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/ContainerInterface.java new file mode 100644 index 000000000..0305f0b49 --- /dev/null +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/ContainerInterface.java @@ -0,0 +1,5 @@ +package io.smallrye.graphql.index.inherit; + +public interface ContainerInterface { + FieldInterface getInheritField(); +} diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/ContainerType.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/ContainerType.java new file mode 100644 index 000000000..a418fe7c9 --- /dev/null +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/ContainerType.java @@ -0,0 +1,8 @@ +package io.smallrye.graphql.index.inherit; + +public class ContainerType implements ContainerInterface { + + public FieldType getInheritField() { + return new FieldType(); + } +} diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/FieldInterface.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/FieldInterface.java new file mode 100644 index 000000000..573debaa4 --- /dev/null +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/FieldInterface.java @@ -0,0 +1,5 @@ +package io.smallrye.graphql.index.inherit; + +public interface FieldInterface { + String getVal(); +} diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/FieldType.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/FieldType.java new file mode 100644 index 000000000..57bdbae01 --- /dev/null +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/FieldType.java @@ -0,0 +1,7 @@ +package io.smallrye.graphql.index.inherit; + +public class FieldType implements FieldInterface { + public String getVal() { + return ""; + } +} diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/InheritAPI.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/InheritAPI.java new file mode 100644 index 000000000..d3cb21a94 --- /dev/null +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/inherit/InheritAPI.java @@ -0,0 +1,12 @@ +package io.smallrye.graphql.index.inherit; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; + +@GraphQLApi +public class InheritAPI { + @Query + public ContainerInterface getContainer() { + return null; + } +} diff --git a/common/schema-model/pom.xml b/common/schema-model/pom.xml index 04900be50..fbea1478d 100644 --- a/common/schema-model/pom.xml +++ b/common/schema-model/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-common-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-schema-model diff --git a/docs/dynamic-client-usage.md b/docs/dynamic-client-usage.md index 96fcb8d26..8def9a784 100644 --- a/docs/dynamic-client-usage.md +++ b/docs/dynamic-client-usage.md @@ -31,7 +31,7 @@ DynamicGraphQLClient client; ``` The above example assumes that configuration for the client is present in system properties. For a full list of -supported configuration properties, see [Client configuration reference](/client_configuration) +supported configuration properties, see [Client configuration reference](client_configuration.md) The other way to build a client is programmatically using a builder: diff --git a/docs/pom.xml b/docs/pom.xml index d71f0a00b..efaf44d67 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-documentation diff --git a/mkdocs.yml b/mkdocs.yml index 7520e2998..8f27a0029 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,7 +5,7 @@ edit_uri: edit/main/docs/ nav: - Overview: 'index.md' - Server side features: - - Customizing JSON deserializers: 'custom-json-deserializers.md' + - Customizing JSON (de)serializers: 'custom-json-serializers-deserializers.md' - Directives: 'directives.md' - Federation: 'federation.md' - Custom error extensions: 'custom-error-extensions.md' diff --git a/pom.xml b/pom.xml index c06e8105b..1d2c65f95 100644 --- a/pom.xml +++ b/pom.xml @@ -5,11 +5,11 @@ io.smallrye smallrye-parent - 42 + 43 smallrye-graphql-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT pom SmallRye: GraphQL Parent @@ -37,7 +37,7 @@ 6.0.0 2.0.0 4.4.0 - 21.3 + 22.1 21.0 1.11.3 4.5.4 diff --git a/release/pom.xml b/release/pom.xml index b61b13987..8c882471e 100644 --- a/release/pom.xml +++ b/release/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-release diff --git a/server/api/pom.xml b/server/api/pom.xml index 7aebf7b4a..5d29354f9 100644 --- a/server/api/pom.xml +++ b/server/api/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-server-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-api diff --git a/server/implementation-cdi/pom.xml b/server/implementation-cdi/pom.xml index e21c4a71d..89ac1b3bb 100644 --- a/server/implementation-cdi/pom.xml +++ b/server/implementation-cdi/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-server-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-cdi diff --git a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/context/CDISmallRyeContext.java b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/context/CDISmallRyeContext.java index 1cb692e60..0dea8aca6 100644 --- a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/context/CDISmallRyeContext.java +++ b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/context/CDISmallRyeContext.java @@ -262,11 +262,6 @@ public Map getAddedExtensions() { return SmallRyeContextManager.getCurrentSmallRyeContext().getAddedExtensions(); } - @Override - public void setAddedExtensions(Map addedExtensions) { - SmallRyeContextManager.getCurrentSmallRyeContext().setAddedExtensions(addedExtensions); - } - @Override public void addExtension(String key, Object value) { SmallRyeContextManager.getCurrentSmallRyeContext().addExtension(key, value); diff --git a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/metrics/MPMetricsService.java b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/metrics/MPMetricsService.java index b1246e902..dafa255e3 100644 --- a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/metrics/MPMetricsService.java +++ b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/metrics/MPMetricsService.java @@ -2,7 +2,9 @@ import java.time.Duration; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; import jakarta.enterprise.inject.spi.CDI; import jakarta.enterprise.util.AnnotationLiteral; @@ -13,6 +15,7 @@ import org.jboss.logging.Logger; import io.smallrye.graphql.api.Context; +import io.smallrye.graphql.schema.model.OperationType; import io.smallrye.graphql.spi.MetricsService; /** @@ -24,8 +27,10 @@ public class MPMetricsService implements MetricsService { private MetricRegistry metricRegistry; - private final Map metricsMemory = new ConcurrentHashMap<>(); + private final Map> metricsMemory = new ConcurrentHashMap<>(); + private final Map subscriptions = new ConcurrentHashMap<>(); private static final String METRIC_NAME = "mp_graphql"; + private static final String METRIC_SUBSCRIPTIONS = "mp_graphql_subscription"; private Logger LOG = Logger.getLogger(MPMetricsService.class); public MPMetricsService() { @@ -41,7 +46,7 @@ private MetricRegistry getMetricRegistry() { return metricRegistry; } - private Tag[] getTags(MetricMeasurement metricMeasurement) { + private Tag[] getTags(MetricMeasurement metricMeasurement) { return new Tag[] { new Tag("name", metricMeasurement.getName()), new Tag("type", metricMeasurement.getOperationType()), @@ -51,7 +56,7 @@ private Tag[] getTags(MetricMeasurement metricMeasurement) { @Override public void start(Long measurementId, Context context) { - metricsMemory.put(measurementId, new MetricMeasurement(context.getFieldName(), + metricsMemory.put(measurementId, new MetricMeasurement<>(context.getFieldName(), context.hasSource(), context.getOperationType(), System.nanoTime())); @@ -60,13 +65,34 @@ public void start(Long measurementId, Context context) { @Override public void end(Long measurementId) { - MetricMeasurement metricMeasurement = metricsMemory.remove(measurementId); - long duration = System.nanoTime() - metricMeasurement.getTimeStarted(); + MetricMeasurement metricMeasurement = metricsMemory.remove(measurementId); + long duration = System.nanoTime() - metricMeasurement.getMetric(); getMetricRegistry().simpleTimer(METRIC_NAME, getTags(metricMeasurement)) .update(Duration.ofNanos(duration)); LOG.tracef("(" + measurementId + ") Finished recording metrics for: %s", metricMeasurement.getName()); } + @Override + public void subscriptionStart(Context context) { + if (!OperationType.SUBSCRIPTION.name().equals(context.getOperationType())) { + return; + } + subscriptions.computeIfAbsent(context.getFieldName(), k -> new AtomicLong(0)); + subscriptions.get(context.getFieldName()).incrementAndGet(); + getMetricRegistry().gauge( + METRIC_SUBSCRIPTIONS, + () -> subscriptions.get(context.getFieldName()).get(), + new Tag("name", context.getFieldName())); + } + + @Override + public void subscriptionEnd(Context context) { + if (!OperationType.SUBSCRIPTION.name().equals(context.getOperationType())) { + return; + } + Optional.ofNullable(subscriptions.get(context.getFieldName())).ifPresent(AtomicLong::decrementAndGet); + } + class VendorType extends AnnotationLiteral implements RegistryType { @Override public MetricRegistry.Type type() { diff --git a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/metrics/MetricMeasurement.java b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/metrics/MetricMeasurement.java index b531e1d0b..385b90e48 100644 --- a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/metrics/MetricMeasurement.java +++ b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/metrics/MetricMeasurement.java @@ -1,16 +1,16 @@ package io.smallrye.graphql.cdi.metrics; -public class MetricMeasurement { +public class MetricMeasurement { private String name; private boolean source; private String operationType; - private long timeStarted; + private M metric; - public MetricMeasurement(String name, boolean source, String operationType, long timeStarted) { + public MetricMeasurement(String name, boolean source, String operationType, M metric) { this.name = name; this.source = source; this.operationType = operationType; - this.timeStarted = timeStarted; + this.metric = metric; } public String getName() { @@ -25,7 +25,7 @@ public String getOperationType() { return operationType; } - public long getTimeStarted() { - return timeStarted; + public M getMetric() { + return metric; } } diff --git a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/metrics/MicrometerMetricsService.java b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/metrics/MicrometerMetricsService.java index f9e8487c8..6cfc2b4cd 100644 --- a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/metrics/MicrometerMetricsService.java +++ b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/metrics/MicrometerMetricsService.java @@ -1,21 +1,27 @@ package io.smallrye.graphql.cdi.metrics; -import java.time.Duration; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; import org.jboss.logging.Logger; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; import io.smallrye.graphql.api.Context; +import io.smallrye.graphql.schema.model.OperationType; import io.smallrye.graphql.spi.MetricsService; public class MicrometerMetricsService implements MetricsService { private final MeterRegistry meterRegistry = Metrics.globalRegistry; - private final Map metricsMemory = new ConcurrentHashMap<>(); + private final Map> metricsMemory = new ConcurrentHashMap<>(); + private final Map subscriptions = new ConcurrentHashMap<>(); + private static final String METRIC_NAME = "mp_graphql"; + private static final String METRIC_SUBSCRIPTIONS = "mp_graphql_subscription"; private Logger LOG = Logger.getLogger(MicrometerMetricsService.class); public MicrometerMetricsService() { @@ -24,7 +30,7 @@ public MicrometerMetricsService() { meterRegistry.getMeters(); } - private Tags getTags(MetricMeasurement metricMeasurement) { + private Tags getTags(MetricMeasurement metricMeasurement) { return Tags.of("name", metricMeasurement.getName()) .and("type", metricMeasurement.getOperationType()) .and("source", String.valueOf(metricMeasurement.isSource())); @@ -33,20 +39,41 @@ private Tags getTags(MetricMeasurement metricMeasurement) { @Override public void start(Long measurementId, Context context) { metricsMemory.put(measurementId, - new MetricMeasurement( + new MetricMeasurement<>( context.getFieldName(), context.hasSource(), context.getOperationType(), - System.nanoTime())); + Timer.start())); LOG.tracef("(" + measurementId + ") Started recording metrics for: %s", context.getFieldName()); } @Override public void end(Long measurementId) { - MetricMeasurement metricMeasurement = metricsMemory.remove(measurementId); - long duration = System.nanoTime() - metricMeasurement.getTimeStarted(); - meterRegistry.timer(METRIC_NAME, getTags(metricMeasurement)) - .record(Duration.ofNanos(duration)); + MetricMeasurement metricMeasurement = metricsMemory.remove(measurementId); + Timer timer = meterRegistry.timer(METRIC_NAME, getTags(metricMeasurement)); + metricMeasurement.getMetric().stop(timer); LOG.tracef("(" + measurementId + ") Finished recording metrics for: %s", metricMeasurement.getName()); } + + @Override + public void subscriptionStart(Context context) { + if (!OperationType.SUBSCRIPTION.name().equals(context.getOperationType())) { + return; + } + subscriptions.computeIfAbsent(context.getFieldName(), k -> new AtomicLong(0)); + subscriptions.get(context.getFieldName()).incrementAndGet(); + meterRegistry.gauge( + METRIC_SUBSCRIPTIONS, + Tags.of("name", context.getFieldName()), + subscriptions.get(context.getFieldName()), + AtomicLong::get); + } + + @Override + public void subscriptionEnd(Context context) { + if (!OperationType.SUBSCRIPTION.name().equals(context.getOperationType())) { + return; + } + Optional.ofNullable(subscriptions.get(context.getFieldName())).ifPresent(AtomicLong::decrementAndGet); + } } diff --git a/server/implementation-servlet/pom.xml b/server/implementation-servlet/pom.xml index ea2f9661a..9b0393bcb 100644 --- a/server/implementation-servlet/pom.xml +++ b/server/implementation-servlet/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-server-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-servlet diff --git a/server/implementation/pom.xml b/server/implementation/pom.xml index 7aee194fe..a89c420b6 100644 --- a/server/implementation/pom.xml +++ b/server/implementation/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-server-parent - 2.8.2-SNAPSHOT + 2.9.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 bc3dcfe95..5d50df3e6 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 @@ -18,6 +18,7 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.Set; +import java.util.Stack; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -727,6 +728,12 @@ private GraphQLDirective createGraphQLDirectiveFrom(DirectiveInstance directiveI directiveBuilder.argument(argumentBuilder.build()); } } + directiveBuilder.validLocations(directiveInstance + .getType() + .getLocations() + .stream() + .map(location -> DirectiveLocation.valueOf(DirectiveLocation.class, location)) + .toArray(DirectiveLocation[]::new)); return directiveBuilder.build(); } @@ -904,23 +911,25 @@ private GraphQLInputObjectField createGraphQLInputObjectFieldFromField(Field fie } private GraphQLInputType createGraphQLInputType(Field field) { - GraphQLInputType graphQLInputType = referenceGraphQLInputType(field); + GraphQLInputType graphQLInputType = getGraphQLInputType(field.getReference()); Wrapper wrapper = dataFetcherFactory.unwrap(field, false); // Field can have a wrapper, like List if (wrapper != null && wrapper.isCollectionOrArrayOrMap()) { + Stack stackOfWrappers = new Stack<>(); + for (Wrapper currentWrapper = wrapper; currentWrapper != null; currentWrapper = currentWrapper.getWrapper()) { + stackOfWrappers.add(currentWrapper); + } // Loop as long as there is a wrapper do { + wrapper = stackOfWrappers.pop(); if (wrapper.isCollectionOrArrayOrMap()) { if (wrapper.isWrappedTypeNotNull()) { graphQLInputType = GraphQLNonNull.nonNull(graphQLInputType); } graphQLInputType = list(graphQLInputType); - wrapper = wrapper.getWrapper(); - } else { - wrapper = null; } - } while (wrapper != null); + } while (!stackOfWrappers.empty()); } // Check if field is mandatory @@ -937,18 +946,20 @@ private GraphQLOutputType createGraphQLOutputType(Field field, boolean isBatch) Wrapper wrapper = dataFetcherFactory.unwrap(field, isBatch); // Field can have a wrapper, like List if (wrapper != null && wrapper.isCollectionOrArrayOrMap()) { + Stack stackOfWrappers = new Stack<>(); + for (Wrapper currentWrapper = wrapper; currentWrapper != null; currentWrapper = currentWrapper.getWrapper()) { + stackOfWrappers.add(currentWrapper); + } // Loop as long as there is a wrapper do { + wrapper = stackOfWrappers.pop(); if (wrapper.isCollectionOrArrayOrMap()) { if (wrapper.isWrappedTypeNotNull()) { graphQLOutputType = GraphQLNonNull.nonNull(graphQLOutputType); } graphQLOutputType = list(graphQLOutputType); - wrapper = wrapper.getWrapper(); - } else { - wrapper = null; } - } while (wrapper != null); + } while (!stackOfWrappers.empty()); } // Check if field is mandatory diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionService.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionService.java index 3963b2007..db07c72df 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionService.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionService.java @@ -265,8 +265,7 @@ private void notifyAndWrite(SmallRyeContext smallRyeContext, smallRyeContext.setExecutionResult(executionResult); // Notify after eventEmitter.fireAfterExecute(smallRyeContext); - - ExecutionResponse executionResponse = new ExecutionResponse(executionResult, + ExecutionResponse executionResponse = new ExecutionResponse(smallRyeContext.unwrap(ExecutionResult.class), smallRyeContext.getAddedExtensions()); if (!payloadOption.equals(LogPayloadOption.off)) { log.payloadOut(executionResponse.toString()); diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/QueryCache.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/QueryCache.java index 1afec0ca4..93041094f 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/QueryCache.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/QueryCache.java @@ -10,6 +10,7 @@ import graphql.ExecutionInput; import graphql.execution.instrumentation.InstrumentationContext; +import graphql.execution.instrumentation.InstrumentationState; import graphql.execution.instrumentation.SimpleInstrumentation; import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters; import graphql.execution.preparsed.PreparsedDocumentEntry; @@ -26,7 +27,7 @@ public class QueryCache extends SimpleInstrumentation implements PreparsedDocume private final LRUCache cache = new LRUCache<>(MAX_CACHE_SIZE); @Override - public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, + public CompletableFuture getDocumentAsync(ExecutionInput executionInput, Function computeFunction) { String query = executionInput.getQuery(); PreparsedDocumentEntry entry = cache.get(query); @@ -37,19 +38,19 @@ public PreparsedDocumentEntry getDocument(ExecutionInput executionInput, } else { log.retrievedFromCache(query); } - return entry; + return CompletableFuture.completedFuture(entry); } @Override public InstrumentationContext> beginValidation( - InstrumentationValidationParameters parameters) { + InstrumentationValidationParameters parameters, InstrumentationState state) { ExecutionFunction executionFunction = executionFunctionTL.get(); executionFunctionTL.remove(); if (executionFunction != null) { return new ValidationInstrumentationContext(executionFunction); } - return super.beginValidation(parameters); + return super.beginValidation(parameters, state); } private static class ExecutionFunction implements Function { @@ -79,7 +80,7 @@ private class ValidationInstrumentationContext implements InstrumentationContext } @Override - public void onDispatched(CompletableFuture> result) { + public void onDispatched() { // no-op } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/context/DocumentSupplier.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/context/DocumentSupplier.java index 7d19cb422..274df7fa4 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/context/DocumentSupplier.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/context/DocumentSupplier.java @@ -1,5 +1,6 @@ package io.smallrye.graphql.execution.context; +import java.util.concurrent.ExecutionException; import java.util.function.Supplier; import graphql.ExecutionInput; @@ -25,12 +26,17 @@ public Document get() { ParseAndValidateResult parse = ParseAndValidate.parse(executionInput); return parse.isFailure() ? null : parse.getDocument(); } else { - PreparsedDocumentEntry documentEntry = queryCache.getDocument(executionInput, ei -> { - ParseAndValidateResult parse = ParseAndValidate.parse(ei); - return parse.isFailure() ? new PreparsedDocumentEntry(parse.getErrors()) - : new PreparsedDocumentEntry(parse.getDocument()); - }); + PreparsedDocumentEntry documentEntry = null; + try { + documentEntry = queryCache.getDocumentAsync(executionInput, ei -> { + ParseAndValidateResult parse = ParseAndValidate.parse(ei); + return parse.isFailure() ? new PreparsedDocumentEntry(parse.getErrors()) + : new PreparsedDocumentEntry(parse.getDocument()); + }).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } return documentEntry.hasErrors() ? null : documentEntry.getDocument(); } } -} \ No newline at end of file +} diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/context/SmallRyeContext.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/context/SmallRyeContext.java index 49188a66a..fe26aac8c 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/context/SmallRyeContext.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/context/SmallRyeContext.java @@ -2,10 +2,10 @@ import static io.smallrye.graphql.SmallRyeGraphQLServerMessages.msg; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import jakarta.json.JsonArray; import jakarta.json.JsonObject; @@ -21,6 +21,17 @@ /** * Implements the Context from MicroProfile API. * + * WARNING: This class has to be used as semi-immutable. + * When propagating this to a new execution, it has to be cloned. + * + * A clone is a deep copy WITH THE EXCEPTION OF: + * - Added extensions + * - ExecutionResult + * - DataFetchingEnvironment (this actually should be rewritten after cloning for each new fetcher) + * + * These above things get shared between all clones to enable applications to write their own data + * into them. + * * @author Phillip Kruger (phillip.kruger@redhat.com) */ public class SmallRyeContext implements Context { @@ -44,7 +55,32 @@ public class SmallRyeContext implements Context { private QueryCache queryCache; private DocumentSupplier documentSupplier; private ExecutionResult executionResult; - private Map addedExtensions = new HashMap<>(); + private Map addedExtensions = new ConcurrentHashMap<>(); + + public SmallRyeContext clone() { + SmallRyeContext clone = new SmallRyeContext(createdBy); + clone.fetchId = fetchId; + clone.request = request; + clone.executionId = executionId; + clone.field = field; + clone.fieldName = fieldName; + clone.arguments = arguments; + clone.source = source; + clone.path = path; + clone.selectedFields = selectedFields; + clone.selectedAndSourceFields = selectedAndSourceFields; + clone.operationType = operationType; + clone.requestedOperationTypes = requestedOperationTypes; + clone.parentTypeName = parentTypeName; + clone.operationName = operationName; + clone.dataFetchingEnvironment = dataFetchingEnvironment; + clone.executionInput = executionInput; + clone.queryCache = queryCache; + clone.documentSupplier = documentSupplier; + clone.executionResult = executionResult; + clone.addedExtensions = addedExtensions; + return clone; + } public Map getAddedExtensions() { return addedExtensions; @@ -52,10 +88,12 @@ public Map getAddedExtensions() { /** * Sets the entire map of extension(s) into the context. + * Note: this is private, for adding extensions, it is necessary to use getAddedExtensions().put(...) + * to avoid changing the map reference. * * @param addedExtensions The Map object containing extension(s). */ - public void setAddedExtensions(Map addedExtensions) { + private void setAddedExtensions(Map addedExtensions) { this.addedExtensions = addedExtensions; } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/context/SmallRyeContextManager.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/context/SmallRyeContextManager.java index 09ed035f7..334decdb6 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/context/SmallRyeContextManager.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/context/SmallRyeContextManager.java @@ -125,25 +125,26 @@ public static SmallRyeContext populateFromDataFetchingEnvironment( smallRyeContext = restoreSmallRyeContext(dataFetchingEnvironment); if (!smallRyeContext.hasRequest()) throw new RuntimeException("Invalid context provided, can not populate data from Data Fetching Environment"); - smallRyeContext.setDataFetchingEnvironment(dataFetchingEnvironment); - smallRyeContext.setField(field); - smallRyeContext.setArguments(dataFetchingEnvironment.getArguments()); - smallRyeContext.setPath(dataFetchingEnvironment.getExecutionStepInfo().getPath().toString()); - smallRyeContext.setExecutionId(dataFetchingEnvironment.getExecutionId().toString()); - smallRyeContext.setFieldName(dataFetchingEnvironment.getField().getName()); - smallRyeContext.setSource(dataFetchingEnvironment.getSource()); - smallRyeContext.setSelectedFields(buildSelectedFields(type, dataFetchingEnvironment, field, false)); - smallRyeContext.setSelectedAndSourceFields(buildSelectedFields(type, dataFetchingEnvironment, field, true)); - smallRyeContext.setOperationType(getOperationTypeFromDefinition(dataFetchingEnvironment.getOperationDefinition())); - smallRyeContext.setParentTypeName(getGraphQLTypeName(dataFetchingEnvironment.getParentType()).orElse(null)); - if (smallRyeContext.getOperationName().isEmpty()) { - smallRyeContext.setOperationName(getOperationName(dataFetchingEnvironment)); + SmallRyeContext clone = smallRyeContext.clone(); + clone.setDataFetchingEnvironment(dataFetchingEnvironment); + clone.setField(field); + clone.setArguments(dataFetchingEnvironment.getArguments()); + clone.setPath(dataFetchingEnvironment.getExecutionStepInfo().getPath().toString()); + clone.setExecutionId(dataFetchingEnvironment.getExecutionId().toString()); + clone.setFieldName(dataFetchingEnvironment.getField().getName()); + clone.setSource(dataFetchingEnvironment.getSource()); + clone.setSelectedFields(buildSelectedFields(type, dataFetchingEnvironment, field, false)); + clone.setSelectedAndSourceFields(buildSelectedFields(type, dataFetchingEnvironment, field, true)); + clone.setOperationType(getOperationTypeFromDefinition(dataFetchingEnvironment.getOperationDefinition())); + clone.setParentTypeName(getGraphQLTypeName(dataFetchingEnvironment.getParentType()).orElse(null)); + if (clone.getOperationName().isEmpty()) { + clone.setOperationName(getOperationName(dataFetchingEnvironment)); } GraphQLContext graphQLContext = dataFetchingEnvironment.getGraphQlContext(); - graphQLContext.put(CONTEXT, smallRyeContext); + graphQLContext.put(CONTEXT, clone); - current.set(smallRyeContext); - return smallRyeContext; + current.set(clone); + return clone; } private static Optional getGraphQLTypeName(GraphQLType graphQLType) { diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/AbstractStreamingDataFetcher.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/AbstractStreamingDataFetcher.java index f319e4749..7cc6670a3 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/AbstractStreamingDataFetcher.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/datafetcher/AbstractStreamingDataFetcher.java @@ -51,6 +51,8 @@ protected O invokeAndTransform( return (O) resultBuilder.build(); } }) + .onSubscription().invoke(() -> metricsEmitter.subscriptionStart(context)) + .onTermination().invoke(() -> metricsEmitter.subscriptionEnd(context)) .onFailure().recoverWithItem(new Function() { @Override public O apply(Throwable throwable) { diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/event/EventEmitter.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/event/EventEmitter.java index 551da1fc5..ba8992284 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/event/EventEmitter.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/event/EventEmitter.java @@ -1,6 +1,7 @@ package io.smallrye.graphql.execution.event; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; @@ -37,11 +38,13 @@ */ public class EventEmitter { private static final Logger LOG = Logger.getLogger(EventEmitter.class); - private static final ThreadLocal eventEmitters = ThreadLocal.withInitial(EventEmitter::new); + + private static final EventEmitter INSTANCE = new EventEmitter(); + private final List enabledServices; public static EventEmitter getInstance() { - return eventEmitters.get(); + return INSTANCE; } private EventEmitter() { @@ -70,7 +73,7 @@ private EventEmitter() { } } enabledServices.sort(Comparator.comparing(this::getPriority)); - this.enabledServices = enabledServices; + this.enabledServices = Collections.unmodifiableList(enabledServices); } private int getPriority(EventingService es) { diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/metrics/MetricsEmitter.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/metrics/MetricsEmitter.java index 7f7755089..11900c5dd 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/metrics/MetricsEmitter.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/metrics/MetricsEmitter.java @@ -16,11 +16,11 @@ public class MetricsEmitter { private static final Logger LOG = Logger.getLogger(MetricsEmitter.class); - private static final ThreadLocal metricsEmitters = ThreadLocal.withInitial(MetricsEmitter::new); + private static final MetricsEmitter METRICS_EMITTER = new MetricsEmitter(); private final List enabledServices; public static MetricsEmitter getInstance() { - return metricsEmitters.get(); + return METRICS_EMITTER; } private MetricsEmitter() { @@ -43,6 +43,14 @@ private MetricsEmitter() { this.enabledServices = enabledServices; } + public void subscriptionStart(Context context) { + enabledServices.forEach(metricsService -> metricsService.subscriptionStart(context)); + } + + public void subscriptionEnd(Context context) { + enabledServices.forEach(metricsService -> metricsService.subscriptionEnd(context)); + } + public Long start(Context context) { Long measurementId = ThreadLocalRandom.current().nextLong(); enabledServices.forEach(metricsService -> metricsService.start(measurementId, context)); diff --git a/server/implementation/src/main/java/io/smallrye/graphql/scalar/federation/FieldSetCoercing.java b/server/implementation/src/main/java/io/smallrye/graphql/scalar/federation/FieldSetCoercing.java index 4397dc8d4..901232982 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/scalar/federation/FieldSetCoercing.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/scalar/federation/FieldSetCoercing.java @@ -26,6 +26,10 @@ private String convertImpl(Object input) { } else { throw new RuntimeException("Can not parse a String from [" + typeName(value) + "]"); } + } else if (input instanceof String) { + return (String) input; + } else if (input instanceof StringValue) { + return ((StringValue) input).getValue(); } else { throw new RuntimeException("Can not parse a FieldSet from [" + typeName(input) + "]"); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/spi/ClassloadingService.java b/server/implementation/src/main/java/io/smallrye/graphql/spi/ClassloadingService.java index 80221e225..b091678f0 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/spi/ClassloadingService.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/spi/ClassloadingService.java @@ -19,8 +19,6 @@ */ public interface ClassloadingService { - ServiceLoader classloadingServices = ServiceLoader.load(ClassloadingService.class); - ClassloadingService classloadingService = load(); static ClassloadingService get() { @@ -30,7 +28,7 @@ static ClassloadingService get() { static ClassloadingService load() { ClassloadingService cls; try { - cls = classloadingServices.iterator().next(); + cls = ServiceLoader.load(ClassloadingService.class).iterator().next(); } catch (Exception ex) { cls = new DefaultClassloadingService(); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/spi/MetricsService.java b/server/implementation/src/main/java/io/smallrye/graphql/spi/MetricsService.java index 1e1cb2393..9587d3205 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/spi/MetricsService.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/spi/MetricsService.java @@ -6,4 +6,20 @@ public interface MetricsService { void start(Long measurementId, Context context); void end(Long measurementId); + + /** + * Tracks the number of active connections to GraphQL Subscriptions + * Indicates to backing Metrics Service that a new connection has been established. + * + * @param context GraphQL Subscription Context + */ + void subscriptionStart(Context context); + + /** + * Tracks the number of active connections to GraphQL Subscriptions + * Indicates to backing Metrics Service that a connection has been terminated + * + * @param context GraphQL Subscription Context + */ + void subscriptionEnd(Context context); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/spi/config/Config.java b/server/implementation/src/main/java/io/smallrye/graphql/spi/config/Config.java index 2e8ac467c..7de1a0584 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/spi/config/Config.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/spi/config/Config.java @@ -18,13 +18,12 @@ */ public interface Config { static Logger LOG = Logger.getLogger(Config.class); - ServiceLoader configs = ServiceLoader.load(Config.class); Config config = init(); static Config init() { Config c; try { - c = configs.iterator().next(); + c = ServiceLoader.load(Config.class).iterator().next(); } catch (Exception ex) { c = new Config() { @Override diff --git a/server/implementation/src/main/java/io/smallrye/graphql/transformation/TransformException.java b/server/implementation/src/main/java/io/smallrye/graphql/transformation/TransformException.java index 4ad317cf8..3a3b8bac6 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/transformation/TransformException.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/transformation/TransformException.java @@ -47,10 +47,13 @@ public DataFetcherResult.Builder appendDataFetcherResult(DataFetcherResu List paths = toPathList(handlerParameters.getPath()); - ValidationError error = new ValidationError(ValidationErrorType.WrongType, - sourceLocation, "argument '" + field.getName() + "' with value 'StringValue{value='" + parameterValue - + "'}' is not a valid '" + getScalarTypeName() + "'", - paths); + ValidationError error = new ValidationError.Builder() + .validationErrorType(ValidationErrorType.WrongType) + .sourceLocation(sourceLocation) + .description("argument '" + field.getName() + "' with value 'StringValue{value='" + parameterValue + + "'}' is not a valid '" + getScalarTypeName() + "'") + .queryPath(paths) + .build(); return builder.error(error); } diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/FederationTestApi.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/FederationTestApi.java index 34494cc37..c6c450c4f 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/schema/FederationTestApi.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/schema/FederationTestApi.java @@ -9,4 +9,9 @@ public class FederationTestApi { public TestTypeWithFederation testTypeWithFederation(String arg) { return null; } + + @Query + public TestInterfaceWitFederation testInterfaceWitFederation(String arg) { + return null; + } } diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/InputTestApi.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/InputTestApi.java index 75acad594..6cea9a039 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/schema/InputTestApi.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/schema/InputTestApi.java @@ -4,8 +4,6 @@ import org.eclipse.microprofile.graphql.GraphQLApi; import org.eclipse.microprofile.graphql.Query; -import io.smallrye.graphql.schema.schemadirectives.OutputDirective; - @GraphQLApi public class InputTestApi { @@ -14,11 +12,6 @@ public int query(InputWithDirectives input) { return 0; } - @Query - public int someQuery(SomeObject someObject) { - return 1; - } - @InputDirective @Description("InputType description") public static class InputWithDirectives { @@ -30,14 +23,4 @@ public static class InputWithDirectives { public void setBar(int bar) { } } - - @OutputDirective - public static class SomeObject { - int boo; - - public void setBoo(int boo) { - this.boo = boo; - } - } - } diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/OneOfSchema.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/OneOfSchema.java index b970f1331..f8533f51c 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/schema/OneOfSchema.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/schema/OneOfSchema.java @@ -24,7 +24,7 @@ public void setField(Integer field) { } @Query - public SomeClass someQuery(SomeClass someClass) { + public String someQuery(SomeClass someClass) { return null; } } diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaFederationEnabledTest.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaFederationEnabledTest.java index c0c53f952..c94a37b71 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaFederationEnabledTest.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaFederationEnabledTest.java @@ -4,6 +4,7 @@ import static graphql.introspection.Introspection.DirectiveLocation.INTERFACE; import static graphql.introspection.Introspection.DirectiveLocation.OBJECT; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -12,12 +13,16 @@ import org.junit.jupiter.api.Test; +import graphql.language.StringValue; +import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLDirective; import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNonNull; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLType; import graphql.schema.GraphQLUnionType; import io.smallrye.graphql.api.Directive; import io.smallrye.graphql.api.federation.FieldSet; @@ -33,7 +38,7 @@ void testSchemaWithFederationEnabled() { System.setProperty("smallrye.graphql.federation.enabled", "true"); try { GraphQLSchema graphQLSchema = createGraphQLSchema(Repeatable.class, Directive.class, Key.class, Keys.class, - FieldSet.class, TestTypeWithFederation.class, FederationTestApi.class); + FieldSet.class, TestTypeWithFederation.class, FederationTestApi.class, TestInterfaceWitFederation.class); GraphQLDirective keyDirective = graphQLSchema.getDirective("key"); assertEquals("key", keyDirective.getName()); @@ -58,7 +63,7 @@ void testSchemaWithFederationEnabled() { assertEquals(TestTypeWithFederation.class.getSimpleName(), entityType.getTypes().get(0).getName()); GraphQLObjectType queryRoot = graphQLSchema.getQueryType(); - assertEquals(3, queryRoot.getFields().size()); + assertEquals(4, queryRoot.getFields().size()); GraphQLFieldDefinition entities = queryRoot.getField("_entities"); assertEquals(1, entities.getArguments().size()); @@ -90,6 +95,15 @@ void testSchemaWithFederationEnabled() { assertEquals(1, serviceType.getFields().size()); assertEquals("sdl", serviceType.getFields().get(0).getName()); assertEquals("String!", serviceType.getFields().get(0).getType().toString()); + + GraphQLType interfaceType = graphQLSchema.getType("TestInterfaceWitFederation"); + assertNotNull(interfaceType); + assertInstanceOf(GraphQLInterfaceType.class, interfaceType); + GraphQLDirective interfaceDirective = ((GraphQLInterfaceType) interfaceType).getDirectives().get(0); + assertEquals("key", interfaceDirective.getName()); + GraphQLArgument interfaceFieldsArgument = interfaceDirective.getArgument("fields"); + assertNotNull(interfaceFieldsArgument); + assertEquals("id", ((StringValue) interfaceFieldsArgument.getArgumentValue().getValue()).getValue()); } finally { System.clearProperty("smallrye.graphql.federation.enabled"); } 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 44e8fe222..2c8d4e7a9 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 @@ -15,6 +15,7 @@ import java.net.URISyntaxException; import java.nio.file.Files; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -23,7 +24,11 @@ import jakarta.annotation.security.RolesAllowed; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.NonNull; +import org.eclipse.microprofile.graphql.Query; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import graphql.schema.GraphQLArgument; @@ -48,7 +53,6 @@ import io.smallrye.graphql.schema.rolesallowedschemas.RolesSchema2; import io.smallrye.graphql.schema.rolesallowedschemas.RolesSchema3; import io.smallrye.graphql.schema.schemadirectives.NonRepeatableSchemaDirective; -import io.smallrye.graphql.schema.schemadirectives.OutputDirective; import io.smallrye.graphql.schema.schemadirectives.RepeatableSchemaDirective; import io.smallrye.graphql.schema.schemadirectives.Schema1; import io.smallrye.graphql.schema.schemadirectives.Schema2; @@ -68,7 +72,7 @@ void testSchemaWithDirectives() throws URISyntaxException, IOException { assertEquals("intArrayTestDirective", typeDirective.getName()); assertEquals("test-description", typeDirective.getDescription()); assertEquals(1, typeDirective.getArguments().size()); - assertEquals("[Int]", typeDirective.getArgument("value").getType().toString()); + assertEquals("[Int!]", typeDirective.getArgument("value").getType().toString()); GraphQLDirective fieldDirective = graphQLSchema.getDirective("fieldDirective"); assertEquals("fieldDirective", fieldDirective.getName()); @@ -160,8 +164,8 @@ void schemaWithUnionDirectives() { @Test void schemaWithInputDirectives() { - GraphQLSchema graphQLSchema = createGraphQLSchema(InputDirective.class, OutputDirective.class, - InputTestApi.class, InputTestApi.InputWithDirectives.class, InputTestApi.SomeObject.class); + GraphQLSchema graphQLSchema = createGraphQLSchema(InputDirective.class, + InputTestApi.class, InputTestApi.InputWithDirectives.class); GraphQLInputObjectType inputWithDirectives = graphQLSchema.getTypeAs("InputWithDirectivesInput"); assertNotNull(inputWithDirectives.getDirective("inputDirective"), @@ -172,10 +176,6 @@ void schemaWithInputDirectives() { assertEquals("InputTypeField description", inputWithDirectives.getField("foo").getDescription()); assertNotNull(inputWithDirectives.getField("bar").getDirective("inputDirective"), "Input type field InputWithDirectivesInput.bar should have directive @inputDirective"); - - GraphQLInputObjectType outputAsInputWithDirectives = graphQLSchema.getTypeAs("SomeObjectInput"); - assertNull(outputAsInputWithDirectives.getDirective("outputDirective"), - "Input type SomeObject should not have directive @outputDirective"); } @Test @@ -185,13 +185,13 @@ void testSchemaWithFederationDisabled() { System.setProperty("smallrye.graphql.federation.enabled", "false"); GraphQLSchema graphQLSchema = createGraphQLSchema(Directive.class, Key.class, Keys.class, - TestTypeWithFederation.class, FederationTestApi.class); + TestTypeWithFederation.class, FederationTestApi.class, TestInterfaceWitFederation.class); assertNull(graphQLSchema.getDirective("key")); assertNull(graphQLSchema.getType("_Entity")); GraphQLObjectType queryRoot = graphQLSchema.getQueryType(); - assertEquals(1, queryRoot.getFields().size()); + assertEquals(2, queryRoot.getFields().size()); assertNull(queryRoot.getField("_entities")); assertNull(queryRoot.getField("_service")); @@ -319,10 +319,6 @@ void testSchemasWithOneOfDirective() { GraphQLFieldDefinition someQuery = queryRoot.getField("someQuery"); assertNotNull(someQuery); - GraphQLObjectType someClassOutput = graphQLSchema.getTypeAs("SomeClass"); - assertNotNull(someClassOutput); - assertEquals(0, someClassOutput.getDirectives().size()); - GraphQLInputObjectType someClassInput = graphQLSchema.getTypeAs("SomeClassInput"); assertNotNull(someClassInput); assertEquals(1, someClassInput.getDirectives().size()); @@ -337,6 +333,188 @@ void testSchemasWithOneOfDirective() { assertEquals("Indicates an Input Object is a OneOf Input Object.", oneOfDirective.getDescription()); } + @GraphQLApi + static class SchemaWithWrongAppliedDirective1 { + @InputDirective + static class SomeObject { + private String field; + + public SomeObject() { + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + } + + @Query + public SomeObject someOperation(SomeObject someObject) { + return null; + } + } + + @GraphQLApi + static class SchemaWithWrongAppliedDirective2 { + + static class SomeObject { + @InputDirective + private String field; + + public SomeObject() { + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + } + + @Query + public SomeObject someOperation(SomeObject someObject) { + return null; + } + } + + @GraphQLApi + static class SchemaWithWrongAppliedDirective3 { + + @ArgumentDirective + @Query + public String someOperation() { + return null; + } + } + + @Nested + class WrongAppliedDirectiveTests { + @Test + void inputDirectiveOnAOutputObjectTest() { + Throwable throwable = assertThrows(SchemaBuilderException.class, + () -> createGraphQLSchema(SchemaWithWrongAppliedDirective1.class, + SchemaWithWrongAppliedDirective1.SomeObject.class, InputDirective.class)); + assertEquals("Directive instance: 'io.smallrye.graphql.schema.InputDirective' assigned to 'io.s" + + "mallrye.graphql.schema.SchemaTest$SchemaWithWrongAppliedDirective1$SomeObject' cannot be applie" + + "d. The directive is allowed on locations '[INPUT_FIELD_DEFINITION, INPUT_OBJECT]' but on 'OBJECT'", + throwable.getMessage()); + } + + @Test + void inputFieldDirectiveOnAOutputFieldTest() { + Throwable throwable = assertThrows(SchemaBuilderException.class, + () -> createGraphQLSchema(SchemaWithWrongAppliedDirective2.class, + SchemaWithWrongAppliedDirective2.SomeObject.class, InputDirective.class)); + assertEquals("Directive instance: 'io.smallrye.graphql.schema.InputDirective' assigned to 'fiel" + + "d' cannot be applied. The directive is allowed on locations '[INPUT_FIELD_DEFINITION, IN" + + "PUT_OBJECT]' but on 'FIELD_DEFINITION'", + throwable.getMessage()); + } + + @Test + void wrongDirectiveLocationInGeneralTest() { // ARGUMENT_DEFINITION -> FIELD_DEFINITION + Throwable throwable = assertThrows(SchemaBuilderException.class, + () -> createGraphQLSchema(SchemaWithWrongAppliedDirective3.class, ArgumentDirective.class)); + assertEquals("Directive instance: 'io.smallrye.graphql.schema.ArgumentDirective' assigned to 's" + + "omeOperation' cannot be applied. The directive is allowed on locations '[ARGUMENT_DEFIN" + + "ITION]' but on 'FIELD_DEFINITION'", + throwable.getMessage()); + } + } + + @GraphQLApi + static class SomeNonNulLWrapperApi { + @Query + public @NonNull Set[]> someOperation(@NonNull Set[]> sio) { + return null; + } + } + + static class SomeObject { + public Set> a; + public Set> b; + public Set<@NonNull Collection> c; + @NonNull + public Set> d; + public Set<@NonNull Collection<@NonNull Long>> e; + @NonNull + public Set> f; + @NonNull + public Set<@NonNull Collection> g; + @NonNull + public Set<@NonNull Collection<@NonNull Long>> h; + + public String[] aArray; + public @NonNull String[] bArray; + public List cArray; + public Set dArray; + + public SomeObject() { + } + } + + @Test + void nonNullWrapperTest() { + GraphQLSchema graphQLSchema = createGraphQLSchema(SomeObject.class, SomeNonNulLWrapperApi.class); + + GraphQLFieldDefinition someOperation = graphQLSchema.getQueryType().getField("someOperation"); + assertNotNull(someOperation); + assertEquals("[[[SomeObject!]]]!", someOperation.getType().toString()); + assertEquals("[[[SomeObjectInput!]]]!", someOperation.getArgument("sio").getType().toString()); + + GraphQLObjectType graphQLObjectType = graphQLSchema.getTypeAs("SomeObject"); + assertNotNull(graphQLObjectType); + + GraphQLInputObjectType graphQLInputObjectType = graphQLSchema.getTypeAs("SomeObjectInput"); + assertNotNull(graphQLInputObjectType); + + assertEquals(12, graphQLObjectType.getFields().size()); + assertEquals(12, graphQLInputObjectType.getFields().size()); + + assertEquals("[[BigInteger]]", graphQLObjectType.getField("a").getType().toString()); + assertEquals("[[BigInteger]]", graphQLInputObjectType.getField("a").getType().toString()); + + assertEquals("[[BigInteger!]]", graphQLObjectType.getField("b").getType().toString()); + assertEquals("[[BigInteger!]]", graphQLInputObjectType.getField("b").getType().toString()); + + assertEquals("[[BigInteger]!]", graphQLObjectType.getField("c").getType().toString()); + assertEquals("[[BigInteger]!]", graphQLInputObjectType.getField("c").getType().toString()); + + assertEquals("[[BigInteger]]!", graphQLObjectType.getField("d").getType().toString()); + assertEquals("[[BigInteger]]!", graphQLInputObjectType.getField("d").getType().toString()); + + assertEquals("[[BigInteger!]!]", graphQLObjectType.getField("e").getType().toString()); + assertEquals("[[BigInteger!]!]", graphQLInputObjectType.getField("e").getType().toString()); + + assertEquals("[[BigInteger!]]!", graphQLObjectType.getField("f").getType().toString()); + assertEquals("[[BigInteger!]]!", graphQLInputObjectType.getField("f").getType().toString()); + + assertEquals("[[BigInteger]!]!", graphQLObjectType.getField("g").getType().toString()); + assertEquals("[[BigInteger]!]!", graphQLInputObjectType.getField("g").getType().toString()); + + assertEquals("[[BigInteger!]!]!", graphQLObjectType.getField("h").getType().toString()); + assertEquals("[[BigInteger!]!]!", graphQLInputObjectType.getField("h").getType().toString()); + + assertEquals("[String]", graphQLObjectType.getField("aArray").getType().toString()); + assertEquals("[String]", graphQLInputObjectType.getField("aArray").getType().toString()); + + // should be `[String]!` + assertEquals("[String!]!", graphQLObjectType.getField("bArray").getType().toString()); + assertEquals("[String!]!", graphQLInputObjectType.getField("bArray").getType().toString()); + + assertEquals("[[String]]", graphQLObjectType.getField("cArray").getType().toString()); + assertEquals("[[String]]", graphQLInputObjectType.getField("cArray").getType().toString()); + + assertEquals("[[Int!]]", graphQLObjectType.getField("dArray").getType().toString()); + assertEquals("[[Int!]]", graphQLInputObjectType.getField("dArray").getType().toString()); + + } + private void assertRolesAllowedDirective(GraphQLFieldDefinition field, String roleValue) { assertNotNull(field); diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTestBase.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTestBase.java index 4c05be928..7e995e108 100644 --- a/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTestBase.java +++ b/server/implementation/src/test/java/io/smallrye/graphql/schema/SchemaTestBase.java @@ -5,13 +5,13 @@ import java.io.IOException; import java.io.InputStream; -import java.util.Map; import java.util.stream.Stream; import org.jboss.jandex.IndexView; import org.jboss.jandex.Indexer; import org.junit.jupiter.api.AfterEach; +import graphql.language.StringValue; import graphql.schema.GraphQLDirective; import graphql.schema.GraphQLSchema; import io.smallrye.graphql.bootstrap.Bootstrap; @@ -33,8 +33,8 @@ protected void assertKeyDirective(GraphQLDirective graphQLDirective, String fiel assertEquals("fields", graphQLDirective.getArguments().get(0).getName()); assertEquals("resolvable", graphQLDirective.getArguments().get(1).getName()); assertEquals(fieldsValue, - ((Map) graphQLDirective.getArguments().get(0).toAppliedArgument().getArgumentValue().getValue()) - .get("value")); + ((StringValue) graphQLDirective.getArguments().get(0).toAppliedArgument().getArgumentValue().getValue()) + .getValue()); assertEquals(resolvableValue, graphQLDirective.getArguments().get(1).toAppliedArgument().getArgumentValue().getValue()); assertEquals(true, graphQLDirective.getArguments().get(1).getArgumentDefaultValue().getValue()); } diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/TestInterfaceWitFederation.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/TestInterfaceWitFederation.java new file mode 100644 index 000000000..8627252af --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/schema/TestInterfaceWitFederation.java @@ -0,0 +1,12 @@ +package io.smallrye.graphql.schema; + +import io.smallrye.graphql.api.federation.External; +import io.smallrye.graphql.api.federation.FieldSet; +import io.smallrye.graphql.api.federation.Key; + +@Key(fields = @FieldSet("id")) + +public interface TestInterfaceWitFederation { + @External + String getId(); +} 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 caa2b9199..b0181341b 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 @@ -1,11 +1,12 @@ package io.smallrye.graphql.schema; +import io.smallrye.graphql.api.federation.External; import io.smallrye.graphql.api.federation.FieldSet; import io.smallrye.graphql.api.federation.Key; @Key(fields = @FieldSet("id")) @Key(fields = @FieldSet("type id"), resolvable = true) -public class TestTypeWithFederation { +public class TestTypeWithFederation implements TestInterfaceWitFederation { private String type; private String id; private String value; @@ -18,6 +19,8 @@ public void setType(String type) { this.type = type; } + @External + @Override public String getId() { return id; } diff --git a/server/implementation/src/test/java/io/smallrye/graphql/schema/schemadirectives/OutputDirective.java b/server/implementation/src/test/java/io/smallrye/graphql/schema/schemadirectives/OutputDirective.java deleted file mode 100644 index 7fe1248a6..000000000 --- a/server/implementation/src/test/java/io/smallrye/graphql/schema/schemadirectives/OutputDirective.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.smallrye.graphql.schema.schemadirectives; - -import static io.smallrye.graphql.api.DirectiveLocation.OBJECT; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Retention; - -import io.smallrye.graphql.api.Directive; - -@Retention(RUNTIME) -@Directive(on = { OBJECT }) -public @interface OutputDirective { -} diff --git a/server/implementation/src/test/resources/schemaTest.graphql b/server/implementation/src/test/resources/schemaTest.graphql index ef7edb46c..e68013f85 100644 --- a/server/implementation/src/test/resources/schemaTest.graphql +++ b/server/implementation/src/test/resources/schemaTest.graphql @@ -18,7 +18,7 @@ directive @include( ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT "test-description" -directive @intArrayTestDirective(value: [Int]) on OBJECT | INTERFACE +directive @intArrayTestDirective(value: [Int!]) on OBJECT | INTERFACE "Indicates an Input Object is a OneOf Input Object." directive @oneOf on INPUT_OBJECT diff --git a/server/integration-tests-jdk16/pom.xml b/server/integration-tests-jdk16/pom.xml index 3168555ae..762d6a26a 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.8.2-SNAPSHOT + 2.9.2-SNAPSHOT 4.0.0 diff --git a/server/integration-tests/pom.xml b/server/integration-tests/pom.xml index 1080090de..d361382b1 100644 --- a/server/integration-tests/pom.xml +++ b/server/integration-tests/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-server-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT 4.0.0 @@ -138,6 +138,11 @@ smallrye-metrics test + + io.micrometer + micrometer-core + test + org.eclipse.microprofile.context-propagation microprofile-context-propagation-api diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/extensions/DynamicClientExtensionsTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/extensions/DynamicClientExtensionsTest.java index 42b77830e..de6026a05 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/extensions/DynamicClientExtensionsTest.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/dynamic/extensions/DynamicClientExtensionsTest.java @@ -86,7 +86,7 @@ public Pool poolWithoutExtensions() { @Query public Pool poolWithExtensions() { - smallRyeContext.setAddedExtensions(getMap()); + smallRyeContext.getAddedExtensions().putAll(getMap()); return new Pool(23); } } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/ClientApi.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/ClientApi.java index b3fb0df1a..1147b602f 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/ClientApi.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/ClientApi.java @@ -3,7 +3,7 @@ import org.eclipse.microprofile.graphql.Query; import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; -import io.smallrye.graphql.tests.client.typesafe.directives.model.SomeClass; +import io.smallrye.graphql.tests.client.typesafe.directives.model.SomeClassClient; @GraphQLClientApi public interface ClientApi { @@ -12,7 +12,8 @@ public interface ClientApi { @FieldDirective(fields = 1) @FieldDirective(fields = { 2, 3 }) @VariableDefinitionDirective(fields = "should ignore") - SomeClass getQuerySomeClass(@FieldDirective(fields = 999) /* will ignore */ SomeClass someObject, + SomeClassClient getQuerySomeClass( + @FieldDirective(fields = 999) /* will ignore */ SomeClassClient someObject, @VariableDefinitionDirective @VariableDefinitionDirective(fields = "a") boolean simpleType); } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/ServerApi.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/ServerApi.java index 544e3cbb9..143b48299 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/ServerApi.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/ServerApi.java @@ -6,17 +6,17 @@ import org.eclipse.microprofile.graphql.Query; import io.smallrye.graphql.execution.context.SmallRyeContext; -import io.smallrye.graphql.tests.client.typesafe.directives.model.SomeClass; +import io.smallrye.graphql.tests.client.typesafe.directives.model.SomeClassServer; @GraphQLApi public class ServerApi { - private static String EXPECTED_QUERY = "query querySomeClass($someObject: SomeClassInput, $simpleType: Boolean! @variableDefinitionDirective @variableDefinitionDirective(fields: \"a\")) { querySomeClass(someObject: $someObject, simpleType: $simpleType) @fieldDirective(fields: [1]) @fieldDirective(fields: [2, 3]) {id @fieldDirective(fields: [4]) number} }"; + private final static String EXPECTED_QUERY = "query querySomeClass($someObject: SomeClassServerInput, $simpleType: Boolean! @variableDefinitionDirective @variableDefinitionDirective(fields: \"a\")) { querySomeClass(someObject: $someObject, simpleType: $simpleType) @fieldDirective(fields: [1]) @fieldDirective(fields: [2, 3]) {id @fieldDirective(fields: [4]) number} }"; @Inject SmallRyeContext context; @Query - public SomeClass getQuerySomeClass(SomeClass someObject, boolean simpleType) { + public SomeClassServer getQuerySomeClass(SomeClassServer someObject, boolean simpleType) { if (!context.getQuery().equals(EXPECTED_QUERY)) { throw new RuntimeException("Queries do not match"); } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/TypesafeStaticDirectivesClientModelTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/TypesafeStaticDirectivesClientModelTest.java index 624bad092..88b80bd7d 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/TypesafeStaticDirectivesClientModelTest.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/TypesafeStaticDirectivesClientModelTest.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.net.URL; +import org.eclipse.microprofile.graphql.Name; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.container.test.api.RunAsClient; import org.jboss.arquillian.junit.Arquillian; @@ -17,9 +18,9 @@ import org.junit.Test; import org.junit.runner.RunWith; -import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; import io.smallrye.graphql.client.vertx.typesafe.VertxTypesafeGraphQLClientBuilder; -import io.smallrye.graphql.tests.client.typesafe.directives.model.SomeClass; +import io.smallrye.graphql.tests.client.typesafe.directives.model.SomeClassClient; +import io.smallrye.graphql.tests.client.typesafe.directives.model.SomeClassServer; @RunWith(Arquillian.class) @RunAsClient @@ -28,8 +29,8 @@ public class TypesafeStaticDirectivesClientModelTest { @Deployment public static WebArchive deployment() { return ShrinkWrap.create(WebArchive.class, "typesafe-directive-client-model.war") - .addClasses(SomeClass.class, ServerApi.class, - // needed for the server-side (java-graphql) validation + .addClasses(SomeClassServer.class, ServerApi.class, + // needed for the server-side (graphql-java) validation FieldDirective.class, VariableDefinitionDirective.class); } @@ -45,12 +46,12 @@ public void prepare() { if (!onlyOnce) { Index index = null; try { - index = Index.of(SomeClass.class, ClientApi.class, ServerApi.class, + index = Index.of(SomeClassClient.class, ClientApi.class, + Name.class, FieldDirective.class, FieldDirective.FieldDirectives.class, VariableDefinitionDirective.class, - VariableDefinitionDirective.VariableDefinitionDirectives.class, - GraphQLClientApi.class); + VariableDefinitionDirective.VariableDefinitionDirectives.class); } catch (IOException e) { throw new RuntimeException(e); } @@ -64,9 +65,10 @@ public void prepare() { @Test public void singleQueryDirectiveTest() { - SomeClass queryInput = new SomeClass("a", 1); + final SomeClassClient queryInput = new SomeClassClient("a", 1); // query checking is on the server side API - assertEquals(queryInput, client.getQuerySomeClass(queryInput, false)); + final SomeClassClient queryResult = client.getQuerySomeClass(queryInput, false); + assertEquals(queryInput.getId(), queryResult.getId()); + assertEquals(queryInput.getNumber(), queryResult.getNumber()); } - } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/model/SomeClassClient.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/model/SomeClassClient.java new file mode 100644 index 000000000..4c16fa719 --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/model/SomeClassClient.java @@ -0,0 +1,36 @@ +package io.smallrye.graphql.tests.client.typesafe.directives.model; + +import org.eclipse.microprofile.graphql.Name; + +import io.smallrye.graphql.tests.client.typesafe.directives.FieldDirective; + +@Name("SomeClassServerInput") +public class SomeClassClient { + @FieldDirective(fields = 4) + String id; + int number; + + public SomeClassClient(String id, int number) { + this.id = id; + this.number = number; + } + + public SomeClassClient() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } +} diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/model/SomeClass.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/model/SomeClassServer.java similarity index 76% rename from server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/model/SomeClass.java rename to server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/model/SomeClassServer.java index e44eaf46d..8aa3f958b 100644 --- a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/model/SomeClass.java +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/client/typesafe/directives/model/SomeClassServer.java @@ -2,17 +2,14 @@ import java.util.Objects; -import io.smallrye.graphql.tests.client.typesafe.directives.FieldDirective; - -public class SomeClass { - @FieldDirective(fields = 4) +public class SomeClassServer { private String id; private int number; - public SomeClass() { + public SomeClassServer() { } - public SomeClass(String id, int number) { + public SomeClassServer(String id, int number) { this.id = id; this.number = number; } @@ -39,7 +36,7 @@ public boolean equals(Object o) { return true; if (o == null || getClass() != o.getClass()) return false; - SomeClass someClass = (SomeClass) o; + SomeClassServer someClass = (SomeClassServer) o; return number == someClass.number && Objects.equals(id, someClass.id); } diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/context/AfterExecutionErrorTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/context/AfterExecutionErrorTest.java new file mode 100644 index 000000000..cff615c00 --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/context/AfterExecutionErrorTest.java @@ -0,0 +1,92 @@ +package io.smallrye.graphql.tests.context; + +import static org.junit.Assert.assertEquals; + +import java.net.URL; +import java.util.List; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +import graphql.ErrorClassification; +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import graphql.GraphQLError; +import graphql.language.SourceLocation; +import io.smallrye.graphql.api.Context; +import io.smallrye.graphql.execution.context.SmallRyeContext; +import io.smallrye.graphql.spi.EventingService; +import io.smallrye.graphql.tests.GraphQLAssured; + +@RunWith(Arquillian.class) +public class AfterExecutionErrorTest { + + @GraphQLApi + public static class SomeApi { + + @Query + public String foo() { + return "bar"; + } + } + + public static class Events implements EventingService { + + @Override + public String getConfigKey() { + return null; + } + + @Override + public void afterExecute(Context context) { + ExecutionResult executionResult = ((SmallRyeContext) context).unwrap(ExecutionResult.class); + ExecutionResult newExecutionResult = ExecutionResultImpl + .newExecutionResult() + .from(executionResult) + .errors(List.of(new GraphQLError() { + @Override + public String getMessage() { + return "Error occurred in afterExecute hook"; + } + + @Override + public List getLocations() { + return List.of(); + } + + @Override + public ErrorClassification getErrorType() { + return null; + } + })).build(); + ((SmallRyeContext) context).setExecutionResult(newExecutionResult); + } + } + + @Deployment + public static WebArchive deployment() { + return ShrinkWrap.create(WebArchive.class, "after-execute-test.war") + .addAsResource(new StringAsset(Events.class.getName()), + "META-INF/services/io.smallrye.graphql.spi.EventingService") + .addClasses(SomeApi.class); + } + + @ArquillianResource + URL testingURL; + + @Test + public void executionResultContainsErrorTest() { + GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); + String response = graphQLAssured.post("{ foo }"); + assertEquals("{\"errors\":[{\"message\":\"Error occurred in afterExecute hook\",\"locations\":[]}]" + + ",\"data\":{\"foo\":\"bar\"}}", response); + } +} diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/context/ContextTest.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/context/ContextTest.java new file mode 100644 index 000000000..0952ddd7c --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/context/ContextTest.java @@ -0,0 +1,158 @@ +package io.smallrye.graphql.tests.context; + +import static org.junit.Assert.assertEquals; + +import java.net.URL; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.eclipse.microprofile.graphql.Source; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.smallrye.graphql.api.Context; +import io.smallrye.graphql.execution.context.SmallRyeContext; +import io.smallrye.graphql.spi.EventingService; +import io.smallrye.graphql.tests.GraphQLAssured; + +/** + * Verifies that the correct SmallRyeContext is properly propagated to field executions + * during more complex execution scenarios, including an EventingService. + */ +@RunWith(Arquillian.class) +public class ContextTest { + + @GraphQLApi + public static class ComplexApi { + + @Inject + SmallRyeContext ctx; + + @Query + public Dummy query1() { + if (!ctx.getField().getName().equals("query1")) { + throw new RuntimeException("ERROR: Wrong selected fields: " + ctx.getSelectedFields()); + } + return new Dummy("ok"); + } + + @Query + public Dummy query2() { + if (!ctx.getField().getName().equals("query2")) { + throw new RuntimeException("ERROR: Wrong selected fields: " + ctx.getSelectedFields()); + } + return new Dummy("ok"); + } + + @Query + public Dummy query3() { + if (!ctx.getField().getName().equals("query3")) { + throw new RuntimeException("ERROR: Wrong selected fields: " + ctx.getSelectedFields()); + } + return new Dummy("ok"); + } + + public String source(@Source Dummy dummy) { + return ctx.getPath(); + } + + } + + public static class Events implements EventingService { + + private static final Map requestContexts = new ConcurrentHashMap<>(); + private static final Map fetchContexts = new ConcurrentHashMap<>(); + private static Logger LOG = Logger.getLogger(Events.class.getName()); + + private static String getFieldNameExecutionId(Context context) { + return context.getFieldName() + " " + context.getExecutionId(); + } + + @Override + public void afterDataFetch(Context context) { + String fieldNameExecutionId = getFieldNameExecutionId(context); + String remove = fetchContexts.remove(fieldNameExecutionId); + LOG.info("afterDataFetch " + fieldNameExecutionId + " " + remove); + if (remove == null) { + LOG.warning(fieldNameExecutionId + " twice"); + throw new NullPointerException(fieldNameExecutionId); + } + } + + @Override + public void beforeDataFetch(Context context) { + String fieldNameExecutionId = getFieldNameExecutionId(context); + LOG.info("beforeDataFetch " + fieldNameExecutionId); + fetchContexts.put(fieldNameExecutionId, fieldNameExecutionId); + } + + @Override + public void afterExecute(Context context) { + String remove = requestContexts.remove(context.getExecutionId()); + LOG.info("afterExecute " + context.getExecutionId() + " " + remove); + if (remove == null) { + LOG.warning(context.getExecutionId() + " twice"); + throw new NullPointerException(context.getExecutionId()); + } + } + + @Override + public void beforeExecute(Context context) { + LOG.info("beforeExecute " + context.getExecutionId()); + requestContexts.put(context.getExecutionId(), context.getExecutionId()); + } + + @Override + public String getConfigKey() { + return null; + } + } + + public static class Dummy { + + private String ok; + + public Dummy(String ok) { + this.ok = ok; + } + + public String getOk() { + return ok; + } + + public void setOk(String ok) { + this.ok = ok; + } + } + + @Deployment + public static WebArchive deployment() { + return ShrinkWrap.create(WebArchive.class, "context-test.war") + .addAsResource(new StringAsset(Events.class.getName()), + "META-INF/services/io.smallrye.graphql.spi.EventingService") + .addClasses(ComplexApi.class, Dummy.class); + } + + @ArquillianResource + URL testingURL; + + @Test + public void test() { + GraphQLAssured graphQLAssured = new GraphQLAssured(testingURL); + String response1 = graphQLAssured.post("{query1 {ok source} query2 {ok source} query3 {ok source}}"); + assertEquals("{\"data\":{\"query1\":{\"ok\":\"ok\",\"source\":\"/query1/source\"}," + + "\"query2\":{\"ok\":\"ok\",\"source\":\"/query2/source\"}," + + "\"query3\":{\"ok\":\"ok\",\"source\":\"/query3/source\"}}}", response1); + } +} diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/metrics/MPMetricsTestCase.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/metrics/MPMetricsTestCase.java new file mode 100644 index 000000000..de8bfa2a5 --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/metrics/MPMetricsTestCase.java @@ -0,0 +1,114 @@ +package io.smallrye.graphql.tests.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertTrue; + +import java.net.URL; +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.eclipse.microprofile.metrics.Gauge; +import org.eclipse.microprofile.metrics.MetricID; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.Tag; +import org.eclipse.microprofile.metrics.annotation.RegistryType; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.smallrye.graphql.api.Subscription; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.vertx.dynamic.VertxDynamicGraphQLClientBuilder; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.subscription.Cancellable; +import io.smallrye.mutiny.unchecked.Unchecked; + +/** + * Verify that metrics are produced as expected. + */ +@RunWith(Arquillian.class) +public class MPMetricsTestCase { + + @Deployment + public static WebArchive deployment() { + return ShrinkWrap.create(WebArchive.class, "metrics-test.war") + .addAsResource(new StringAsset("smallrye.graphql.metrics.enabled=true"), + "META-INF/microprofile-config.properties") + .addClasses(Foo.class); + } + + @ArquillianResource + URL testingURL; + + @GraphQLApi + public static class Foo { + + static CountDownLatch CANCELLED = new CountDownLatch(1); + + @Subscription + public Multi counting() { + return Multi.createFrom() + .ticks() + .every(Duration.ofSeconds(1L)) + .onCancellation().invoke(() -> CANCELLED.countDown()); + } + + @Query + public Long count() { + return CANCELLED.getCount(); + } + + } + + MeterRegistry meterRegistry = Metrics.globalRegistry; + + @Inject + @RegistryType(type = MetricRegistry.Type.VENDOR) + MetricRegistry metricRegistry; + + DynamicGraphQLClient client; + + @Before + public void before() { + client = new VertxDynamicGraphQLClientBuilder() + .url(testingURL.toString() + "graphql") + .build(); + } + + @Test + public void testSubscriptionMetrics() throws Exception { + Multi multi = client.subscription("subscription {counting}"); + CountDownLatch items = new CountDownLatch(2); + Cancellable cancellable = multi.subscribe().with( + response -> items.countDown(), + Unchecked.consumer(error -> { + throw new AssertionError("Subscription failed", error); + })); + + assertTrue(items.await(10, TimeUnit.SECONDS)); + + assertThat(metricRegistry.getGauges()).hasSize(1); + MetricID metricID = new MetricID("mp_graphql_subscription", new Tag("name", "counting")); + Gauge gauge = metricRegistry.getGauge(metricID); + assertThat(gauge).isNotNull(); + assertThat(gauge.getValue()).isEqualTo(1L); + + cancellable.cancel(); + assertTrue(Foo.CANCELLED.await(10, TimeUnit.SECONDS)); + assertThat(gauge.getValue()).isEqualTo(0L); + } +} diff --git a/server/integration-tests/src/test/java/io/smallrye/graphql/tests/metrics/MicrometerMetricsTestCase.java b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/metrics/MicrometerMetricsTestCase.java new file mode 100644 index 000000000..03a2f6dd2 --- /dev/null +++ b/server/integration-tests/src/test/java/io/smallrye/graphql/tests/metrics/MicrometerMetricsTestCase.java @@ -0,0 +1,105 @@ +package io.smallrye.graphql.tests.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertTrue; + +import java.net.URL; +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.smallrye.graphql.api.Subscription; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.vertx.dynamic.VertxDynamicGraphQLClientBuilder; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.subscription.Cancellable; +import io.smallrye.mutiny.unchecked.Unchecked; + +/** + * Verify that metrics are produced as expected. + */ +@RunWith(Arquillian.class) +public class MicrometerMetricsTestCase { + + @Deployment + public static WebArchive deployment() { + return ShrinkWrap.create(WebArchive.class, "metrics-test.war") + .addAsResource(new StringAsset("smallrye.graphql.metrics.enabled=true"), + "META-INF/microprofile-config.properties") + .addClasses(Foo.class); + } + + @ArquillianResource + URL testingURL; + + @GraphQLApi + public static class Foo { + + static CountDownLatch CANCELLED = new CountDownLatch(1); + + @Subscription + public Multi counting() { + return Multi.createFrom() + .ticks() + .every(Duration.ofSeconds(1L)) + .onCancellation().invoke(() -> CANCELLED.countDown()); + } + + @Query + public Long count() { + return CANCELLED.getCount(); + } + + } + + MeterRegistry meterRegistry = Metrics.globalRegistry; + + DynamicGraphQLClient client; + + @Before + public void before() { + client = new VertxDynamicGraphQLClientBuilder() + .url(testingURL.toString() + "graphql") + .build(); + //Needs to add this registry in order to maintain count state across timer counts + Metrics.addRegistry(new SimpleMeterRegistry()); + } + + @Test + public void testSubscriptionMetrics() throws Exception { + Multi multi = client.subscription("subscription {counting}"); + CountDownLatch items = new CountDownLatch(2); + Cancellable cancellable = multi.subscribe().with( + response -> items.countDown(), + Unchecked.consumer(error -> { + throw new AssertionError("Subscription failed", error); + })); + + assertTrue(items.await(10, TimeUnit.SECONDS)); + assertThat(meterRegistry.getMeters()).hasSizeGreaterThanOrEqualTo(1); + Gauge gauge = meterRegistry.get("mp_graphql_subscription").tags(Tags.of("name", "counting")).gauge(); + assertThat(gauge).isNotNull(); + assertThat(gauge.value()).isEqualTo(1L); + cancellable.cancel(); + assertTrue(Foo.CANCELLED.await(10, TimeUnit.SECONDS)); + assertThat(gauge.value()).isEqualTo(0L); + } +} diff --git a/server/pom.xml b/server/pom.xml index dcc8c5234..e037d78e2 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-server-parent diff --git a/server/runner/pom.xml b/server/runner/pom.xml index b293cd7a1..26206db81 100644 --- a/server/runner/pom.xml +++ b/server/runner/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-server-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-runner diff --git a/server/tck/pom.xml b/server/tck/pom.xml index b568915e6..de2fd2518 100644 --- a/server/tck/pom.xml +++ b/server/tck/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-server-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-tck diff --git a/tools/gradle-plugin/gradle.properties b/tools/gradle-plugin/gradle.properties index 2725eb44e..fe686ba3a 100644 --- a/tools/gradle-plugin/gradle.properties +++ b/tools/gradle-plugin/gradle.properties @@ -1 +1 @@ -version=2.8.1 +version=2.9.1 diff --git a/tools/gradle-plugin/pom.xml b/tools/gradle-plugin/pom.xml index 99131c32c..e4c0fd874 100644 --- a/tools/gradle-plugin/pom.xml +++ b/tools/gradle-plugin/pom.xml @@ -4,7 +4,7 @@ io.smallrye smallrye-graphql-tools-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT 4.0.0 diff --git a/tools/maven-plugin-tests/pom.xml b/tools/maven-plugin-tests/pom.xml index 6cae409fa..bcec73d43 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.8.2-SNAPSHOT + 2.9.2-SNAPSHOT 4.0.0 diff --git a/tools/maven-plugin/pom.xml b/tools/maven-plugin/pom.xml index ba6aa2050..b893500a1 100644 --- a/tools/maven-plugin/pom.xml +++ b/tools/maven-plugin/pom.xml @@ -3,7 +3,7 @@ io.smallrye smallrye-graphql-tools-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT 4.0.0 diff --git a/tools/pom.xml b/tools/pom.xml index bf4686790..38ad82cf7 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-tools-parent diff --git a/ui/graphiql/README.asciidoc b/ui/graphiql/README.asciidoc index 25aa04252..476f315c4 100644 --- a/ui/graphiql/README.asciidoc +++ b/ui/graphiql/README.asciidoc @@ -1,6 +1,6 @@ = SmallRye Graph__i__QL UI -This allows you you add GraphiQL UI with your project. +This allows you to add GraphiQL UI with your project. image:/ui/graphiql/graphiql.png[link="/main/ui/graphiql/graphiql.png"] diff --git a/ui/graphiql/pom.xml b/ui/graphiql/pom.xml index 313494fa7..5292c05a4 100644 --- a/ui/graphiql/pom.xml +++ b/ui/graphiql/pom.xml @@ -5,7 +5,7 @@ io.smallrye smallrye-graphql-ui-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-ui-graphiql @@ -16,7 +16,7 @@ graphql-ui 18.2.0 - 2.4.7 + 3.2.0 diff --git a/ui/pom.xml b/ui/pom.xml index a757f46f7..8e5065b20 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -5,14 +5,14 @@ io.smallrye smallrye-graphql-parent - 2.8.2-SNAPSHOT + 2.9.2-SNAPSHOT smallrye-graphql-ui-parent pom SmallRye: GraphQL UI - UI Tools effectivly repackaged + UI Tools effectively repackaged graphiql