diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d763d28b7..3f3433d46 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: jmartisk/quarkus + ref: smallrye-graphql-2.11 path: quarkus - uses: actions/setup-java@v4 diff --git a/client/implementation-vertx/pom.xml b/client/implementation-vertx/pom.xml index 990b526ce..73dbd7862 100644 --- a/client/implementation-vertx/pom.xml +++ b/client/implementation-vertx/pom.xml @@ -49,6 +49,11 @@ io.vertx vertx-web-client + + org.eclipse + yasson + runtime + io.smallrye @@ -65,11 +70,6 @@ smallrye-graphql-client-model-builder test - - org.eclipse - yasson - test - diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java index 948e04c57..4b74fc218 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientBuilder.java @@ -12,8 +12,10 @@ import java.util.List; import java.util.Map; +import org.eclipse.microprofile.graphql.Name; import org.jboss.logging.Logger; +import io.smallrye.graphql.api.Namespace; import io.smallrye.graphql.client.impl.ErrorMessageProvider; import io.smallrye.graphql.client.impl.GraphQLClientConfiguration; import io.smallrye.graphql.client.impl.GraphQLClientsConfiguration; @@ -145,6 +147,14 @@ public VertxTypesafeGraphQLClientBuilder websocketInitializationTimeout(Integer @Override public T build(Class apiClass) { + Name nameAnnotation = apiClass.getAnnotation(Name.class); + Namespace namespaceAnnotation = apiClass.getAnnotation(Namespace.class); + + if (nameAnnotation != null && namespaceAnnotation != null) { + throw new RuntimeException("You can only use one of the annotations - @Name or @Namespace " + + "over the GraphQLClientApi interface. Please, fix the following interface: " + apiClass.getName()); + } + if (this.options == null) { this.options = new WebClientOptions(); } diff --git a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java index 649f0684a..f9fe262d9 100644 --- a/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java +++ b/client/implementation-vertx/src/main/java/io/smallrye/graphql/client/vertx/typesafe/VertxTypesafeGraphQLClientProxy.java @@ -309,7 +309,7 @@ private JsonObject request(MethodInvocation method) { } request.add("query", query); request.add("variables", variables(method)); - request.add("operationName", method.getName()); + request.add("operationName", method.getOperationName()); JsonObject result = request.build(); log.tracef("full graphql request: %s", result.toString()); return result; diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/QueryBuilder.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/QueryBuilder.java index 8e1cf14f1..d15ddf51d 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/QueryBuilder.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/QueryBuilder.java @@ -24,15 +24,11 @@ public QueryBuilder(MethodInvocation method) { public String build() { StringBuilder request = new StringBuilder(method.getOperationTypeAsString()); request.append(" "); - request.append(method.getName()); + request.append(method.getOperationName()); if (method.hasValueParameters()) request.append(method.valueParameters().map(this::declare).collect(joining(", ", "(", ")"))); - String groupName = method.getGroupName(); - if (groupName != null) { - request.append(" { "); - request.append(groupName); - } + method.getNamespaces().forEach(namespace -> request.append(" { ").append(namespace)); if (method.isSingle()) { request.append(" { "); @@ -45,11 +41,11 @@ public String build() { request.append(fields(method.getReturnType())); - if (method.isSingle()) + if (method.isSingle()) { request.append(" }"); + } - if (groupName != null) - request.append(" } "); + request.append(" }".repeat(method.getNamespaces().size())); return request.toString(); } diff --git a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/ResultBuilder.java b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/ResultBuilder.java index 30e02bd4f..8506b2411 100644 --- a/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/ResultBuilder.java +++ b/client/implementation/src/main/java/io/smallrye/graphql/client/impl/typesafe/ResultBuilder.java @@ -92,10 +92,12 @@ public Object read() { private JsonObject readData() { if (!response.containsKey("data") || response.isNull("data")) return null; - String groupName = method.getGroupName(); - JsonObject data = groupName != null - ? response.getJsonObject("data").getJsonObject(groupName) - : response.getJsonObject("data"); + + JsonObject data = response.getJsonObject("data"); + for (String namespace : method.getNamespaces()) { + data = data.getJsonObject(namespace); + } + if (method.isSingle() && !data.containsKey(method.getName())) throw new InvalidResponseException("No data for '" + method.getName() + "'"); return data; 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 1a8a49504..5f44dc2a4 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 @@ -14,6 +14,7 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -23,6 +24,7 @@ import org.eclipse.microprofile.graphql.Name; import org.eclipse.microprofile.graphql.Query; +import io.smallrye.graphql.api.Namespace; import io.smallrye.graphql.api.Subscription; import io.smallrye.graphql.client.core.OperationType; import io.smallrye.graphql.client.model.MethodKey; @@ -36,14 +38,16 @@ public static MethodInvocation of(Method method, Object... args) { private final TypeInfo type; private final Method method; private final Object[] parameterValues; - private final String groupName; + private final List namespaces; + private final String operationName; private List parameters; private MethodInvocation(TypeInfo type, Method method, Object[] parameterValues) { this.type = type; this.method = method; this.parameterValues = parameterValues; - this.groupName = readGroupName(method); + this.namespaces = readNamespaces(method); + this.operationName = readOperationName(this.namespaces); } @Override @@ -262,18 +266,41 @@ public String getOperationTypeAsString() { } } - public String getGroupName() { - return groupName; + public List getNamespaces() { + return namespaces; } - private String readGroupName(Method method) { - Name annotation = method.getDeclaringClass().getAnnotation(Name.class); - if (annotation != null) { - String groupName = annotation.value().trim(); - if (!groupName.isEmpty()) { - return groupName; + public String getOperationName() { + return operationName; + } + + private List readNamespaces(Method method) { + if (method.getDeclaringClass().isAnnotationPresent(Namespace.class)) { + String[] names = method.getDeclaringClass().getAnnotation(Namespace.class).value(); + if (names.length > 0) { + return List.of(names); } + } else if (method.getDeclaringClass().isAnnotationPresent(Name.class)) { + String name = method.getDeclaringClass().getAnnotation(Name.class).value(); + if (!name.isBlank()) { + return List.of(name); + } + } + return List.of(); + } + + private String readOperationName(List names) { + if (names.isEmpty()) { + return getName(); + } else { + String namespace = names.stream() + .map(this::makeFirstLetterUppercase) + .collect(Collectors.joining()); + return namespace + makeFirstLetterUppercase(getName()); } - return null; + } + + private String makeFirstLetterUppercase(String value) { + return value.substring(0, 1).toUpperCase() + value.substring(1); } } diff --git a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Annotations.java b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Annotations.java index ef0af9910..a1e47fb53 100644 --- a/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Annotations.java +++ b/client/model-builder/src/main/java/io/smallrye/graphql/client/model/Annotations.java @@ -525,6 +525,7 @@ private static Map getAnnotationsWithFilter(Type ty public static final DotName NON_BLOCKING = DotName.createSimple("io.smallrye.common.annotation.NonBlocking"); // SmallRye GraphQL Annotations (Experimental) + public static final DotName NAMESPACE = DotName.createSimple("io.smallrye.graphql.api.Namespace"); public static final DotName TO_SCALAR = DotName.createSimple("io.smallrye.graphql.api.ToScalar"); // TODO: Remove public static final DotName ADAPT_TO_SCALAR = DotName.createSimple("io.smallrye.graphql.api.AdaptToScalar"); public static final DotName ADAPT_WITH = DotName.createSimple("io.smallrye.graphql.api.AdaptWith"); 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 17ce35ede..5e42f04b5 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 @@ -1,6 +1,8 @@ package io.smallrye.graphql.client.model; import static io.smallrye.graphql.client.model.Annotations.GRAPHQL_CLIENT_API; +import static io.smallrye.graphql.client.model.Annotations.NAME; +import static io.smallrye.graphql.client.model.Annotations.NAMESPACE; import static io.smallrye.graphql.client.model.ScanningContext.getIndex; import java.util.ArrayList; @@ -8,6 +10,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; @@ -56,6 +59,8 @@ private ClientModels generateClientModels() { Collection graphQLApiAnnotations = getIndex() .getAnnotations(GRAPHQL_CLIENT_API); + validateNamespaceAnnotations(graphQLApiAnnotations); + graphQLApiAnnotations.forEach(graphQLApiAnnotation -> { ClientModel operationMap = new ClientModel(); ClassInfo apiClass = graphQLApiAnnotation.target().asClass(); @@ -75,6 +80,19 @@ private ClientModels generateClientModels() { return clientModels; } + private void validateNamespaceAnnotations(Collection graphQLApiAnnotations) { + List errorInterfaces = graphQLApiAnnotations.stream() + .map(annotation -> annotation.target().asClass()) + .filter(classInfo -> classInfo.hasDeclaredAnnotation(NAMESPACE) && classInfo.hasDeclaredAnnotation(NAME)) + .map(classInfo -> classInfo.name().toString()) + .collect(Collectors.toList()); + if (!errorInterfaces.isEmpty()) { + throw new RuntimeException("You can only use one of the annotations - @Name or @Namespace " + + "over the GraphQLClientApi interface. Please, fix the following interfaces: " + + String.join(", ", errorInterfaces)); + } + } + /** * Retrieves all methods, including those from superclasses (interfaces). * 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 4470c877b..9ed0f5d8b 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 @@ -34,16 +34,12 @@ public QueryBuilder(MethodInfo method) { public String build() { StringBuilder request = new StringBuilder(method.getOperationTypeAsString()); request.append(" "); - request.append(method.getName()); // operationName + request.append(method.getOperationName()); if (method.hasValueParameters()) { request.append(method.valueParameters().stream().map(method::declare).collect(joining(", ", "(", ")"))); } - String groupName = method.getGroupName(); - if (groupName != null) { - request.append(" { "); - request.append(groupName); - } + method.getNamespaces().forEach(namespace -> request.append(" { ").append(namespace)); if (method.isSingle()) { request.append(" { "); @@ -61,11 +57,11 @@ public String build() { request.append(method.fields(method.getReturnType())); - if (method.isSingle()) + if (method.isSingle()) { request.append(" }"); + } - if (groupName != null) - request.append(" } "); + request.append(" }".repeat(method.getNamespaces().size())); return request.toString(); } 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 ba3844152..6fe59755f 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 @@ -3,6 +3,7 @@ import static io.smallrye.graphql.client.model.Annotations.MULTIPLE; import static io.smallrye.graphql.client.model.Annotations.MUTATION; import static io.smallrye.graphql.client.model.Annotations.NAME; +import static io.smallrye.graphql.client.model.Annotations.NAMESPACE; import static io.smallrye.graphql.client.model.Annotations.QUERY; import static io.smallrye.graphql.client.model.Annotations.SUBCRIPTION; import static io.smallrye.graphql.client.model.ScanningContext.getIndex; @@ -37,7 +38,8 @@ public class OperationModel implements NamedElement { private final Stack expressionStack = new Stack<>(); private Stack rawParametrizedTypes = new Stack<>(); private final List directives; - private final String groupName; + private final String operationName; + private final List namespaces; /** * Creates a new {@code OperationModel} instance based on the provided Jandex {@link MethodInfo}. @@ -51,7 +53,8 @@ public class OperationModel implements NamedElement { getDirectiveLocation(), AnnotationTarget.Kind.METHOD) .map(DirectiveInstance::of) .collect(toList()); - this.groupName = readGroupName(method); + this.namespaces = readNamespaces(method); + this.operationName = readOperationName(this.namespaces); } /** @@ -399,23 +402,42 @@ private boolean isRawParametrizedType(TypeModel type) { return type.isCustomParametrizedType() && !type.getFirstRawType().isTypeVariable(); } - public String getGroupName() { - return groupName; + public List getNamespaces() { + return namespaces; } - private String readGroupName(MethodInfo method) { - List annotationInstances = method.declaringClass().annotations(NAME); - for (AnnotationInstance annotationInstance : annotationInstances) { - if (annotationInstance.target().kind() == AnnotationTarget.Kind.CLASS) { - if (annotationInstance.target().asClass().name().equals(method.declaringClass().name())) { - String groupName = annotationInstance.value().asString().trim(); - if (!groupName.isEmpty()) { - return groupName; - } - } + public String getOperationName() { + return operationName; + } + + private List readNamespaces(MethodInfo method) { + if (method.declaringClass().hasDeclaredAnnotation(NAMESPACE)) { + String[] names = method.declaringClass().declaredAnnotation(NAMESPACE).value().asStringArray(); + if (names.length > 0) { + return List.of(names); } + } else if (method.declaringClass().hasDeclaredAnnotation(NAME)) { + String value = method.declaringClass().declaredAnnotation(NAME).value().asString(); + if (!value.isEmpty()) { + return List.of(value); + } + } + return List.of(); + } + + private String readOperationName(List names) { + if (names.isEmpty()) { + return getName(); + } else { + String namespace = names.stream() + .map(this::makeFirstLetterUppercase) + .collect(joining()); + return namespace + makeFirstLetterUppercase(getName()); } - return null; + } + + private String makeFirstLetterUppercase(String value) { + return value.substring(0, 1).toUpperCase() + value.substring(1); } private String fieldsFragment(TypeModel type) { 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 a03e7a415..649187fb7 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 @@ -16,6 +16,7 @@ import org.jboss.jandex.Index; import org.junit.jupiter.api.Test; +import io.smallrye.graphql.api.Namespace; import io.smallrye.graphql.api.Subscription; import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; @@ -235,13 +236,40 @@ void namedClientModelTest() throws IOException { assertEquals(3, clientModel.getOperationMap().size()); assertOperation(clientModel, new MethodKey("findAllStringsQuery", new Class[0]), - "query findAll { named { findAll } } "); + "query NamedFindAll { named { findAll } }"); assertOperation(clientModel, new MethodKey("findAllStringsName", new Class[0]), - "query findAll { named { findAll } } "); + "query NamedFindAll { named { findAll } }"); assertOperation(clientModel, new MethodKey("update", new Class[] { String.class }), - "mutation update($s: String) { named { update(s: $s) } } "); + "mutation NamedUpdate($s: String) { named { update(s: $s) } }"); + } + + @Namespace({ "first", "second" }) + @GraphQLClientApi(configKey = "namespaced-string-api") + interface NamespacedClientApi { + @Query("findAll") + List findAllStringsQuery(); + + @Mutation("update") + @Name("update") + String update(String s); + } + + @Test + void namespacedClientModelTest() throws IOException { + String configKey = "namespaced-string-api"; + ClientModels clientModels = ClientModelBuilder + .build(Index.of(NamespacedClientApi.class)); + assertNotNull(clientModels.getClientModelByConfigKey(configKey)); + ClientModel clientModel = clientModels.getClientModelByConfigKey(configKey); + assertEquals(2, clientModel.getOperationMap().size()); + assertOperation(clientModel, + new MethodKey("findAllStringsQuery", new Class[0]), + "query FirstSecondFindAll { first { second { findAll } } }"); + assertOperation(clientModel, + new MethodKey("update", new Class[] { String.class }), + "mutation FirstSecondUpdate($s: String) { first { second { update(s: $s) } } }"); } private void assertOperation(ClientModel clientModel, MethodKey methodKey, String expectedQuery) { 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 971dd5525..6d32044b5 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 @@ -596,6 +596,7 @@ private static Map getAnnotationsWithFilter(org.jbo public static final DotName NULLABLE = DotName.createSimple("io.smallrye.graphql.api.Nullable"); public static final DotName KOTLIN_METADATA = DotName.createSimple("kotlin.Metadata"); public static final DotName ONE_OF = DotName.createSimple("io.smallrye.graphql.api.OneOf"); + public static final DotName NAMESPACE = DotName.createSimple("io.smallrye.graphql.api.Namespace"); // MicroProfile GraphQL Annotations public static final DotName GRAPHQL_API = DotName.createSimple("org.eclipse.microprofile.graphql.GraphQLApi"); diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/SchemaBuilder.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/SchemaBuilder.java index 39566d1a4..e4e2ad13c 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/SchemaBuilder.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/SchemaBuilder.java @@ -2,12 +2,16 @@ import static io.smallrye.graphql.schema.Annotations.CUSTOM_SCALAR; import static io.smallrye.graphql.schema.Annotations.DIRECTIVE; +import static io.smallrye.graphql.schema.Annotations.GRAPHQL_API; +import static io.smallrye.graphql.schema.Annotations.NAME; +import static io.smallrye.graphql.schema.Annotations.NAMESPACE; import static io.smallrye.graphql.schema.Annotations.ONE_OF; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Queue; import java.util.Set; @@ -40,12 +44,13 @@ import io.smallrye.graphql.schema.helper.BeanValidationDirectivesHelper; import io.smallrye.graphql.schema.helper.DescriptionHelper; import io.smallrye.graphql.schema.helper.Directives; -import io.smallrye.graphql.schema.helper.GroupHelper; +import io.smallrye.graphql.schema.helper.NamespaceHelper; import io.smallrye.graphql.schema.helper.RolesAllowedDirectivesHelper; import io.smallrye.graphql.schema.helper.TypeAutoNameStrategy; import io.smallrye.graphql.schema.model.DirectiveType; import io.smallrye.graphql.schema.model.ErrorInfo; -import io.smallrye.graphql.schema.model.Group; +import io.smallrye.graphql.schema.model.Namespace; +import io.smallrye.graphql.schema.model.NamespaceContainer; import io.smallrye.graphql.schema.model.Operation; import io.smallrye.graphql.schema.model.OperationType; import io.smallrye.graphql.schema.model.Reference; @@ -119,10 +124,9 @@ private SchemaBuilder(TypeAutoNameStrategy autoNameStrategy) { } private Schema generateSchema() { - // Get all the @GraphQLAPI annotations Collection graphQLApiAnnotations = ScanningContext.getIndex() - .getAnnotations(Annotations.GRAPHQL_API); + .getAnnotations(GRAPHQL_API); final Schema schema = new Schema(); @@ -143,13 +147,19 @@ private Schema generateSchema() { addCustomScalarTypes(schema); + validateNamespaceAnnotations(graphQLApiAnnotations); + validateSubscriptions(graphQLApiAnnotations); + for (AnnotationInstance graphQLApiAnnotation : graphQLApiAnnotations) { ClassInfo apiClass = graphQLApiAnnotation.target().asClass(); List methods = getAllMethodsIncludingFromSuperClasses(apiClass); - Optional group = GroupHelper.getGroup(graphQLApiAnnotation); - addOperations(group, schema, methods); + NamespaceHelper.getNamespace(graphQLApiAnnotation).ifPresentOrElse( + namespace -> addNamespacedOperations(namespace, schema, methods), + () -> addOperations(schema, methods)); } + validateMethods(schema); + // The above queries and mutations reference some models (input / type / interfaces / enum), let's create those addTypesToSchema(schema); @@ -168,6 +178,61 @@ private Schema generateSchema() { return schema; } + private List findNamespacedMethodsErrors(Map namespaces, Set operations) { + return operations.stream() + .filter(operation -> namespaces.containsKey(operation.getName())) + .map(operation -> "operation name: " + operation.getName() + ", class: " + operation.getClassName() + + ", method name: " + operation.getMethodName()) + .collect(Collectors.toList()); + } + + private void validateMethods(Schema schema) { + List queryErrors = findNamespacedMethodsErrors(schema.getNamespacedQueries(), schema.getQueries()); + List mutationErrors = findNamespacedMethodsErrors(schema.getNamespacedMutations(), schema.getMutations()); + + if (!queryErrors.isEmpty() || !mutationErrors.isEmpty()) { + throw new RuntimeException("Inconsistent schema. Operation names overlap with namespaces." + + queryErrors.stream().collect(Collectors.joining(", ", " queries - ", ";")) + + mutationErrors.stream().collect(Collectors.joining(", ", " mutations - ", ";"))); + } + } + + private void validateSubscriptions(Collection graphQLApiAnnotations) { + List errors = new ArrayList<>(); + + for (AnnotationInstance annotation : graphQLApiAnnotations) { + ClassInfo apiClass = annotation.target().asClass(); + if (apiClass.hasDeclaredAnnotation(NAMESPACE) || apiClass.hasDeclaredAnnotation(NAME)) { + List methods = getAllMethodsIncludingFromSuperClasses(apiClass); + for (MethodInfo methodInfo : methods) { + Annotations annotationsForMethod = Annotations.getAnnotationsForMethod(methodInfo); + if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.SUBCRIPTION)) { + errors.add("class: " + apiClass.name().toString() + ", method: " + methodInfo.name()); + } + } + } + } + + if (!errors.isEmpty()) { + throw new RuntimeException("Subscriptions can't be nested. " + + "Move your subscriptions to another @GraphQLApi class, not marked @Namespace or @Name. " + + "Check these places: " + String.join("; ", errors)); + } + } + + private void validateNamespaceAnnotations(Collection graphQLApiAnnotations) { + List errorClasses = graphQLApiAnnotations.stream() + .map(annotation -> annotation.target().asClass()) + .filter(classInfo -> classInfo.hasDeclaredAnnotation(NAMESPACE) && classInfo.hasDeclaredAnnotation(NAME)) + .map(classInfo -> classInfo.name().toString()) + .collect(Collectors.toList()); + if (!errorClasses.isEmpty()) { + throw new RuntimeException("You can only use one of the annotations - @Name or @Namespace " + + "over the GraphQLClientApi interface. Please, fix the following classes: " + + String.join(", ", errorClasses)); + } + } + private List getAllMethodsIncludingFromSuperClasses(ClassInfo classInfo) { ClassInfo current = classInfo; IndexView index = ScanningContext.getIndex(); @@ -355,6 +420,19 @@ private boolean findOutstandingAndAddToSchema(ReferenceType referenceType, C return keepGoing; } + private void addNamespacedOperations(Namespace namespace, Schema schema, List methodInfoList) { + for (MethodInfo methodInfo : methodInfoList) { + Annotations annotationsForMethod = Annotations.getAnnotationsForMethod(methodInfo); + if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.QUERY)) { + Operation query = operationCreator.createOperation(methodInfo, OperationType.QUERY, null); + schema.addNamespacedQuery(namespace, query); + } else if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.MUTATION)) { + Operation mutation = operationCreator.createOperation(methodInfo, OperationType.MUTATION, null); + schema.addNamespacedMutation(namespace, mutation); + } + } + } + /** * This inspect all method, looking for Query and Mutation annotations, * to create those Operations. @@ -362,33 +440,18 @@ private boolean findOutstandingAndAddToSchema(ReferenceType referenceType, C * @param schema the schema to add the operation to. * @param methodInfoList the java methods. */ - private void addOperations(Optional group, Schema schema, List methodInfoList) { + private void addOperations(Schema schema, List methodInfoList) { for (MethodInfo methodInfo : methodInfoList) { Annotations annotationsForMethod = Annotations.getAnnotationsForMethod(methodInfo); if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.QUERY)) { Operation query = operationCreator.createOperation(methodInfo, OperationType.QUERY, null); - if (group.isPresent()) { - schema.addGroupedQuery(group.get(), query); - } else { - schema.addQuery(query); - } + schema.addQuery(query); } else if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.MUTATION)) { Operation mutation = operationCreator.createOperation(methodInfo, OperationType.MUTATION, null); - if (group.isPresent()) { - schema.addGroupedMutation(group.get(), mutation); - } else { - schema.addMutation(mutation); - } + schema.addMutation(mutation); } else if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.SUBCRIPTION)) { Operation subscription = operationCreator.createOperation(methodInfo, OperationType.SUBSCRIPTION, null); - if (group.isPresent()) { - schema.addGroupedSubscription(group.get(), subscription); - } else { - schema.addSubscription(subscription); - } - } else if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.RESOLVER)) { - Operation resolver = operationCreator.createOperation(methodInfo, OperationType.RESOLVER, null); - schema.addResolver(resolver); + schema.addSubscription(subscription); } } } diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/GroupHelper.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/GroupHelper.java deleted file mode 100644 index 6235cdab3..000000000 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/GroupHelper.java +++ /dev/null @@ -1,82 +0,0 @@ -package io.smallrye.graphql.schema.helper; - -import java.util.List; -import java.util.Optional; - -import org.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.AnnotationValue; -import org.jboss.jandex.ClassInfo; - -import io.smallrye.graphql.schema.Annotations; -import io.smallrye.graphql.schema.model.Group; - -/** - * Helping with Group creation - * - * @author Phillip Kruger (phillip.kruger@redhat.com) - */ -public class GroupHelper { - - private GroupHelper() { - } - - public static Optional getGroup(AnnotationInstance graphQLApiAnnotation) { - Optional name = getName(graphQLApiAnnotation); - if (name.isPresent()) { - Optional description = getDescription(graphQLApiAnnotation); - Group group = new Group(); - group.setName(name.get()); - group.setDescription(description.orElse(null)); - return Optional.of(group); - } - return Optional.empty(); - } - - /** - * This gets the root name (by default 'root). - * This will allow grouping root queries under a logical name. - * - * @param graphQLApiAnnotation - * @return - */ - private static Optional getName(AnnotationInstance graphQLApiAnnotation) { - // Get the name - AnnotationValue value = graphQLApiAnnotation.value(); - if (value != null && value.asString() != null && !value.asString().isEmpty()) { - return Optional.of(value.asString()); - } else { - // Try the Name annotation - ClassInfo apiClass = graphQLApiAnnotation.target().asClass(); - AnnotationInstance nameAnnotation = apiClass.classAnnotation(Annotations.NAME); - if (nameAnnotation != null && nameAnnotation.value() != null && nameAnnotation.value().asString() != null - && !nameAnnotation.value().asString().isEmpty()) { - return Optional.of(nameAnnotation.value().asString()); - } - } - return Optional.empty(); - } - - /** - * Get the description on a class type - * - * @param graphQLApiAnnotation annotation on the class - * @return the optional description - */ - private static Optional getDescription(AnnotationInstance graphQLApiAnnotation) { - ClassInfo apiClass = graphQLApiAnnotation.target().asClass(); - if (apiClass.annotationsMap().containsKey(Annotations.DESCRIPTION)) { - List descriptionAnnotations = apiClass.annotationsMap().get(Annotations.DESCRIPTION); - if (descriptionAnnotations != null && !descriptionAnnotations.isEmpty()) { - for (AnnotationInstance descriptionAnnotation : descriptionAnnotations) { - if (descriptionAnnotation.target().equals(graphQLApiAnnotation.target())) { - AnnotationValue value = descriptionAnnotation.value(); - if (value != null && value.asString() != null && !value.asString().isEmpty()) { - return Optional.of(value.asString()); - } - } - } - } - } - return Optional.empty(); - } -} diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/NamespaceHelper.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/NamespaceHelper.java new file mode 100644 index 000000000..7e375bd72 --- /dev/null +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/NamespaceHelper.java @@ -0,0 +1,69 @@ +package io.smallrye.graphql.schema.helper; + +import java.util.List; +import java.util.Optional; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; + +import io.smallrye.graphql.schema.Annotations; +import io.smallrye.graphql.schema.model.Namespace; + +public class NamespaceHelper { + private NamespaceHelper() { + } + + public static Optional getNamespace(AnnotationInstance graphQLApiAnnotation) { + Optional> names = getNames(graphQLApiAnnotation); + if (names.isPresent()) { + Optional description = getDescription(graphQLApiAnnotation); + Namespace group = new Namespace(); + group.setNames(names.get()); + group.setDescription(description.orElse(null)); + return Optional.of(group); + } + return Optional.empty(); + } + + /** + * This gets the namespaces. + * This will allow grouping root queries under a logical name. + * + * @param graphQLApiAnnotation annotation on the class + * @return list of namespaces + */ + private static Optional> getNames(AnnotationInstance graphQLApiAnnotation) { + ClassInfo apiClass = graphQLApiAnnotation.target().asClass(); + if (apiClass.hasDeclaredAnnotation(Annotations.NAMESPACE)) { + String[] namespaces = apiClass.declaredAnnotation(Annotations.NAMESPACE).value().asStringArray(); + if (namespaces.length > 0) { + return Optional.of(List.of(namespaces)); + } + } + + if (apiClass.hasDeclaredAnnotation(Annotations.NAME)) { + String value = apiClass.declaredAnnotation(Annotations.NAME).value().asString(); + if (!value.isEmpty()) { + return Optional.of(List.of(value)); + } + } + return Optional.empty(); + } + + /** + * Get the description on a class type + * + * @param graphQLApiAnnotation annotation on the class + * @return the optional description + */ + private static Optional getDescription(AnnotationInstance graphQLApiAnnotation) { + ClassInfo apiClass = graphQLApiAnnotation.target().asClass(); + if (apiClass.hasDeclaredAnnotation(Annotations.DESCRIPTION)) { + String value = apiClass.declaredAnnotation(Annotations.DESCRIPTION).value().asString(); + if (!value.isEmpty()) { + return Optional.of(value); + } + } + return Optional.empty(); + } +} diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/NamespaceTest.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/NamespaceTest.java new file mode 100644 index 000000000..d7b8a370d --- /dev/null +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/NamespaceTest.java @@ -0,0 +1,45 @@ +package io.smallrye.graphql.index; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.jboss.jandex.Index; +import org.junit.jupiter.api.Test; + +import io.smallrye.graphql.index.app.namespace.ApiWithName; +import io.smallrye.graphql.index.app.namespace.ApiWithNameAndNamespace; +import io.smallrye.graphql.index.app.namespace.ApiWithNamespace; +import io.smallrye.graphql.schema.IndexCreator; +import io.smallrye.graphql.schema.SchemaBuilder; +import io.smallrye.graphql.schema.model.Schema; + +/** + * Test namespaces + */ +public class NamespaceTest { + @Test + public void apiWithNameTest() { + Index index = IndexCreator.indexWithPackage(ApiWithName.class); + Schema schema = SchemaBuilder.build(index); + + assertNotNull(schema); + assertEquals(schema.getAllNamespacedQueryOperations().size(), 1); + } + + @Test + public void apiWithNameTestWithEnabledUseNamespaces() { + Index index = IndexCreator.indexWithPackage(ApiWithNameAndNamespace.class); + + assertThrows(RuntimeException.class, () -> SchemaBuilder.build(index)); + } + + @Test + public void apiWithNamespaceTest() { + Index index = IndexCreator.indexWithPackage(ApiWithNamespace.class); + Schema schema = SchemaBuilder.build(index); + + assertNotNull(schema); + assertEquals(schema.getAllNamespacedQueryOperations().size(), 1); + } +} diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/app/namespace/ApiWithName.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/app/namespace/ApiWithName.java new file mode 100644 index 000000000..c137ecd50 --- /dev/null +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/app/namespace/ApiWithName.java @@ -0,0 +1,14 @@ +package io.smallrye.graphql.index.app.namespace; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + +@GraphQLApi +@Name("name") +public class ApiWithName { + @Query + public String query() { + return "query"; + } +} diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/app/namespace/ApiWithNameAndNamespace.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/app/namespace/ApiWithNameAndNamespace.java new file mode 100644 index 000000000..7cfa4be90 --- /dev/null +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/app/namespace/ApiWithNameAndNamespace.java @@ -0,0 +1,17 @@ +package io.smallrye.graphql.index.app.namespace; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + +import io.smallrye.graphql.api.Namespace; + +@GraphQLApi +@Name("name") +@Namespace("namespace") +public class ApiWithNameAndNamespace { + @Query + public String query() { + return "query"; + } +} diff --git a/common/schema-builder/src/test/java/io/smallrye/graphql/index/app/namespace/ApiWithNamespace.java b/common/schema-builder/src/test/java/io/smallrye/graphql/index/app/namespace/ApiWithNamespace.java new file mode 100644 index 000000000..e5df861db --- /dev/null +++ b/common/schema-builder/src/test/java/io/smallrye/graphql/index/app/namespace/ApiWithNamespace.java @@ -0,0 +1,15 @@ +package io.smallrye.graphql.index.app.namespace; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; + +import io.smallrye.graphql.api.Namespace; + +@GraphQLApi +@Namespace("namespace") +public class ApiWithNamespace { + @Query + public String query() { + return "query"; + } +} diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Group.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Group.java deleted file mode 100644 index 992c8855a..000000000 --- a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Group.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.smallrye.graphql.schema.model; - -import java.io.Serializable; -import java.util.Objects; - -/** - * Allow grouping of queries and mutations. - * - * @author Phillip Kruger (phillip.kruger@redhat.com) - */ -public class Group implements Serializable { - private String name; - private String description; - - public Group() { - } - - public Group(String name, String description) { - this.name = name; - this.description = description; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - @Override - public int hashCode() { - int hash = 7; - hash = 59 * hash + Objects.hashCode(this.name); - return hash; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final Group other = (Group) obj; - if (!Objects.equals(this.name, other.name)) { - return false; - } - return true; - } - - @Override - public String toString() { - return "Group{" + "name=" + name + ", description=" + description + '}'; - } - -} diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/GroupContainer.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/GroupContainer.java new file mode 100644 index 000000000..95d4b8f7b --- /dev/null +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/GroupContainer.java @@ -0,0 +1,93 @@ +package io.smallrye.graphql.schema.model; + +import java.io.Serializable; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class GroupContainer implements Serializable { + private String name; + private String description; + private Set operations = new HashSet<>(); + private Map container = new HashMap<>(); + + public GroupContainer() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Set getOperations() { + return operations; + } + + public void setOperations(Set operations) { + this.operations = operations; + } + + public Map getContainer() { + return container; + } + + public void setContainer(Map container) { + this.container = container; + } + + public boolean hasOperations() { + return !operations.isEmpty() || container.values().stream().anyMatch(this::hasOperations); + } + + private boolean hasOperations(GroupContainer namespace) { + return !namespace.getOperations().isEmpty() + || namespace.container.values().stream().anyMatch(this::hasOperations); + } + + public void add(Collection names, String description, Operation operation) { + if (!names.isEmpty()) { + ArrayDeque queue = new ArrayDeque<>(names); + String name = queue.poll(); + add(name, description, queue, operation); + } else { + throw new RuntimeException("Namespaces can't be empty"); + } + } + + private void add(String name, String description, ArrayDeque queue, Operation operation) { + this.name = name; + + if (!queue.isEmpty()) { + String key = queue.poll(); + GroupContainer groupContainer = container.computeIfAbsent(key, s -> new GroupContainer()); + groupContainer.add(key, description, queue, operation); + } else { + this.description = description; + this.operations.add(operation); + } + } + + public List getAllOperations() { + List operations = new ArrayList<>(this.operations); + + container.values().forEach(groupContainer -> operations.addAll(groupContainer.getAllOperations())); + + return operations; + } +} diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Namespace.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Namespace.java new file mode 100644 index 000000000..2176c604f --- /dev/null +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Namespace.java @@ -0,0 +1,48 @@ +package io.smallrye.graphql.schema.model; + +import java.util.List; +import java.util.Objects; + +public class Namespace { + private List names; + private String description; + + public Namespace() { + } + + public List getNames() { + return names; + } + + public void setNames(List names) { + this.names = names; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Namespace namespace = (Namespace) o; + return Objects.equals(names, namespace.names) && Objects.equals(description, namespace.description); + } + + @Override + public int hashCode() { + return Objects.hash(names, description); + } + + @Override + public String toString() { + return "Namespace{" + "names=" + names + ", description=" + description + '}'; + } +} diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/NamespaceContainer.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/NamespaceContainer.java new file mode 100644 index 000000000..2552a0a6e --- /dev/null +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/NamespaceContainer.java @@ -0,0 +1,89 @@ +package io.smallrye.graphql.schema.model; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class NamespaceContainer { + private String name; + private String description; + private Set operations = new HashSet<>(); + private Map container = new HashMap<>(); + + public NamespaceContainer() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Set getOperations() { + return operations; + } + + public void setOperations(Set operations) { + this.operations = operations; + } + + public Map getContainer() { + return container; + } + + public void setContainer(Map container) { + this.container = container; + } + + public boolean hasOperations() { + return !operations.isEmpty() || container.values().stream().anyMatch(this::hasOperations); + } + + private boolean hasOperations(NamespaceContainer namespace) { + return !namespace.getOperations().isEmpty() + || namespace.container.values().stream().anyMatch(this::hasOperations); + } + + public void add(Collection names, String description, Operation operation) { + if (names.isEmpty()) { + throw new RuntimeException("Namespaces can't be empty"); + } + ArrayDeque queue = new ArrayDeque<>(names); + String name = queue.poll(); + add(name, description, queue, operation); + } + + private void add(String name, String description, ArrayDeque queue, Operation operation) { + this.name = name; + + if (queue.isEmpty()) { + this.description = description; + this.operations.add(operation); + } else { + String key = queue.poll(); + NamespaceContainer groupContainer = container.computeIfAbsent(key, s -> new NamespaceContainer()); + groupContainer.add(key, description, queue, operation); + } + } + + public List getAllOperations() { + List operations = new ArrayList<>(this.operations); + container.values().forEach(groupContainer -> operations.addAll(groupContainer.getAllOperations())); + return operations; + } +} diff --git a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Schema.java b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Schema.java index 06c85d859..5e284134d 100644 --- a/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Schema.java +++ b/common/schema-model/src/main/java/io/smallrye/graphql/schema/model/Schema.java @@ -2,11 +2,13 @@ import java.io.Serializable; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * Represents a GraphQL Schema. @@ -23,9 +25,8 @@ public final class Schema implements Serializable { private Set subscriptions = new HashSet<>(); private Set resolvers = new HashSet<>(); - private Map> groupedQueries = new HashMap<>(); - private Map> groupedMutations = new HashMap<>(); - private Map> groupedSubscriptions = new HashMap<>(); + private Map namespacedQueries = new HashMap<>(); + private Map namespacedMutations = new HashMap<>(); private List customScalarTypes = new ArrayList<>(); private List directiveTypes = new ArrayList<>(); @@ -44,6 +45,46 @@ public final class Schema implements Serializable { public Schema() { } + public Map getNamespacedQueries() { + return namespacedQueries; + } + + public void setNamespacedQueries(Map namespacedQueries) { + this.namespacedQueries = namespacedQueries; + } + + public Map getNamespacedMutations() { + return namespacedMutations; + } + + public Set getAllOperations() { + Set operations = new HashSet<>(); + operations.addAll(queries); + operations.addAll(mutations); + operations.addAll(subscriptions); + operations.addAll(getAllNamespacedQueryOperations()); + operations.addAll(getAllNamespacedMutationOperations()); + return operations; + } + + public Set getAllNamespacedQueryOperations() { + return namespacedQueries.values().stream() + .map(NamespaceContainer::getAllOperations) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + public Set getAllNamespacedMutationOperations() { + return namespacedMutations.values().stream() + .map(NamespaceContainer::getAllOperations) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + public void setNamespacedMutations(Map namespacedMutations) { + this.namespacedMutations = namespacedMutations; + } + public Set getQueries() { return queries; } @@ -57,7 +98,9 @@ public void addQuery(Operation query) { } public boolean hasOperations() { - return hasQueries() || hasGroupedQueries() || hasMutations() || hasGroupedMutations(); + return hasQueries() || hasNamespaceQueries() + || hasMutations() || hasNamespaceMutations() + || hasSubscriptions() || hasResolvers(); } public boolean hasQueries() { @@ -112,54 +155,6 @@ public boolean hasResolvers() { return !this.resolvers.isEmpty(); } - public Map> getGroupedQueries() { - return groupedQueries; - } - - public void setGroupedQueries(Map> groupedQueries) { - this.groupedQueries = groupedQueries; - } - - public void addGroupedQuery(Group group, Operation query) { - addToOperationMap(this.groupedQueries, group, query); - } - - public boolean hasGroupedQueries() { - return !this.groupedQueries.isEmpty(); - } - - public Map> getGroupedMutations() { - return groupedMutations; - } - - public void setGroupedMutations(Map> groupedMutations) { - this.groupedMutations = groupedMutations; - } - - public void addGroupedMutation(Group group, Operation mutation) { - addToOperationMap(this.groupedMutations, group, mutation); - } - - public boolean hasGroupedMutations() { - return !this.groupedMutations.isEmpty(); - } - - public Map> getGroupedSubscriptions() { - return groupedSubscriptions; - } - - public void setGroupedSubscriptions(Map> groupedSubscriptions) { - this.groupedSubscriptions = groupedSubscriptions; - } - - public void addGroupedSubscription(Group group, Operation subscription) { - addToOperationMap(this.groupedSubscriptions, group, subscription); - } - - public boolean hasGroupedSubscriptions() { - return !this.groupedSubscriptions.isEmpty(); - } - public Map getInputs() { return inputs; } @@ -326,18 +321,6 @@ public List getBatchOperations() { return batchOperations; } - private void addToOperationMap(Map> map, Group group, Operation query) { - Set set; - - if (map.containsKey(group)) { - set = map.get(group); - } else { - set = new HashSet<>(); - } - set.add(query); - map.put(group, set); - } - public void addCustomScalarType(CustomScalarType customScalarType) { customScalarTypes.add(customScalarType); Scalars.registerCustomScalarInSchema( @@ -376,9 +359,9 @@ public String toString() { ", queries=" + queries + ", mutations=" + mutations + ", subscriptions=" + subscriptions + - ", groupedQueries=" + groupedQueries + - ", groupedMutations=" + groupedMutations + - ", groupedSubscriptions=" + groupedSubscriptions + + ", resolvers=" + resolvers + + ", namespacedQueries=" + namespacedQueries + + ", namespacedMutations=" + namespacedMutations + ", directiveTypes=" + directiveTypes + ", customScalarTypes=" + customScalarTypes + ", inputs=" + inputs + @@ -406,4 +389,26 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } + + public void addNamespacedQuery(Namespace namespace, Operation operation) { + NamespaceContainer groupContainer = namespacedQueries.computeIfAbsent( + namespace.getNames().get(0), + key -> new NamespaceContainer()); + groupContainer.add(namespace.getNames(), namespace.getDescription(), operation); + } + + public void addNamespacedMutation(Namespace namespace, Operation operation) { + NamespaceContainer groupContainer = namespacedMutations.computeIfAbsent( + namespace.getNames().get(0), + key -> new NamespaceContainer()); + groupContainer.add(namespace.getNames(), namespace.getDescription(), operation); + } + + public boolean hasNamespaceQueries() { + return namespacedQueries.values().stream().anyMatch(NamespaceContainer::hasOperations); + } + + public boolean hasNamespaceMutations() { + return namespacedMutations.values().stream().anyMatch(NamespaceContainer::hasOperations); + } } diff --git a/docs/client-standalone.md b/docs/client-standalone.md index 3bc39a1f0..2bf39d54c 100644 --- a/docs/client-standalone.md +++ b/docs/client-standalone.md @@ -19,11 +19,10 @@ This is a full script runnable directly with [JBang](https://www.jbang.dev/) tha uses a dynamic client for connecting to [countries.trevorblades.com](https://countries.trevorblades.com) to obtain a list of countries from its database. -``` +```java ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.smallrye:smallrye-graphql-client-implementation-vertx:1.5.0 +//DEPS io.smallrye:smallrye-graphql-client-implementation-vertx:RELEASE -import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder; import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; import io.smallrye.graphql.client.Response; import io.smallrye.graphql.client.vertx.dynamic.VertxDynamicGraphQLClientBuilder; @@ -55,4 +54,4 @@ class Client { } ``` -Save this file as `client.java` and execute with `jbang client.java`. \ No newline at end of file +Save this file as `Client.java` and execute with `jbang Client.java`. diff --git a/docs/namespaces-on-server-side.md b/docs/namespaces-on-server-side.md index 0fbf49dd2..ba76a0bec 100644 --- a/docs/namespaces-on-server-side.md +++ b/docs/namespaces-on-server-side.md @@ -1,20 +1,22 @@ # Namespacing on the server side -## Before you continue reading > [NOTE] > Using approaches to form namespaces in the schema can be useful for large APIs. There are several ways to do this. > However, read the documentation carefully, especially the limitations and possible problems. -## How use namespaces -There are 3 options how to use the name space - use the @Name annotation, @Source, or combine them. +> [NOTE] You can only use one of the annotations - @Name or @Namespace over the GraphQLApi classes. + +## Using @Namespace annotation + +The annotation accepts an array of strings containing the nesting of the namespace. +This method allows you to create any nesting of namespaces. +You can use any nesting and also combine different levels. -### Using @Name annotation -The easiest way is that you can separate your API into namespace areas using the annotation @Name with @GraphQLApi. ```java @GraphQLApi -@Name("users") -@Description("Users operations") -public class UserApi { +@Namespace({"admin", "users"}) +@Description("Admin users operations") +public class AdminUsersApi { @Query public List findAll() { // @@ -22,134 +24,84 @@ public class UserApi { } @GraphQLApi -@Name("roles") -@Description("Roles operations") -public class RoleApi { +@Namespace({"admin"}) +@Description("Admin operations") +public class AdminApi { @Query - public List findAll() { + public List findAll() { // } } ``` -As a result, you can get methods with the same names. -``` -query { - users { - findAll { - .... - } - } - roles { - findAll { - .... - } - } -} -``` -When using annotation @Name, will be generated type - NameQuery, NameMutation and NameSubscription -(Subscriptions placed in this type will not work. More details below). -### Using @Source annotation for deep nesting -You can use the @Source annotation to create deep nesting of namespaces. -```java -// create classes that represent namespaces -public class AdminQueryNamespace { +Will be generated schema +``` +"Query root" +type Query { + admin: AdminQuery } -public class AdminMutationNamespace { +"Admin operations" +type AdminQuery { + users: AdminUsersQuery + findAll: [Admin] } -public class UserQueryNamespace { +"Admin users operations" +type AdminUsersQuery { + findAll: [User] } -public class UserMutationNamespace { +type Admin { + id: BigInteger + ... } -@GraphQLApi -public class UserApi { - @Query("admin") - public AdminQueryNamespace adminQueryNamespace() { - return new AdminQueryNamespace(); - } - - public UserQueryNamespace userQueryNamespace(@Source AdminQueryNamespace namespace) { - return new UserQueryNamespace(); - } - - public List findAll(@Source UserQueryNamespace namespace) { - // return users; - } - - @Mutation("admin") - public AdminMutationNamespace adminMutationNamespace() { - return new AdminMutationNamespace(); - } - - public UserMutationNamespace userMutationNamespace(@Source AdminMutationNamespace namespace) { - return new UserMutationNamespace(); - } - - public List save(@Source UserMutationNamespace namespace, User user) { - // save user - } +type User { + id: BigInteger + ... } ``` -As a result, you will be able to execute the following query. + +And you will can send such request ``` query { - admin { - users { - findAll { - .... - } - } - } -} - -mutation { - admin { - users { - save (user: ...) { - .... - } - } + admin { + users { + findAll { + id + } } + } } ``` -### Using @Source and @Name annotations together for deep nesting -You can also simplify this example by using @Name. -```java -// create classes that represent namespaces -public class UserQueryNamespace { -} - -public class UserMutationNamespace { -} +## Using @Name annotation (deprecated) +> [NOTE] This feature may be removed in the future. +Does the same thing as @Namespace, the only difference is that there can only be one nesting level. +```java @GraphQLApi -@Name("admin") +@Name("users") @Description("Users operations") public class UserApi { - @Query("users") - public UserQueryNamespace userQueryNamespace() { - return new UserQueryNamespace(); - } - - public List findAll(@Source UserQueryNamespace namespace) { - // return users; - } - - @Mutation("users") - public UserMutationNamespace userMutationNamespace() { - return new UserMutationNamespace(); + @Query + public List findAll() { + // } - - public List save(@Source UserMutationNamespace namespace, User user) { - // save user +} +``` +As a result, you can get methods with the same names. +``` +query { + users { + findAll { + .... } + } } ``` + ## Problems While dividing APIs into namespaces may seem convenient, it has several issues that are important to be aware of. @@ -170,7 +122,7 @@ As example, if you try to run such a subscription request, you will get an error ```java @GraphQLApi -@Name("resource") +@Namepace("resource") public class ResourceApi { @Subscription public Multi resourceChange() { diff --git a/docs/typesafe-client-usage.md b/docs/typesafe-client-usage.md index 1d9b38b71..8baacfb01 100644 --- a/docs/typesafe-client-usage.md +++ b/docs/typesafe-client-usage.md @@ -1,9 +1,10 @@ +# Java code-first type-safe GraphQL Client API + A Java code-first type-safe GraphQL Client API suggestion for [Microprofile GraphQL Issue \#185](https://github.com/eclipse/microprofile-graphql/issues/185). -Basic Usage -=========== +## Basic Usage Creating the client-side counterpart of the GraphQL API: @@ -46,8 +47,7 @@ Contracts](https://martinfowler.com/articles/consumerDrivenContracts.html). If the server uses names different from yours, you can simply use annotations to do a mapping: -Name Mapping / Aliases -====================== +### Name Mapping / Aliases If the server defines a different field or parameter name, annotate it with `@Name`. If the server defines a different query name, annotate the @@ -75,8 +75,7 @@ If you rename a field or method, the real field or method name will be used as an alias, so you can select the same data twice (see `` and `` below). -Configuration -============= +### Configuration If the endpoint is always the same, e.g. a public API of a cloud service, you can add the URL to your API annotation, e.g.: @@ -116,8 +115,7 @@ key for the endpoint `superheroes/mp-graphql/url`. When using the builder, you can override the config key as well: `TypesafeGraphQLClientBuilder.newBuilder().configKey("superheroes")`. -NestedParameter -=============== +### NestedParameter Some APIs require parameters beyond the root level, e.g. for filtering or paginating nested lists. Say you have a schema like this: @@ -200,130 +198,82 @@ public interface ApiClient { } ``` -Namespaces -========== -> [NOTE] -> We strongly unrecommended the use namespaces with a type-safe client. -> It is possible to use the @Name annotation, with minimal changes to the code. -> However, for more complex cases it is better to use the dynamic client, since understanding the resulting code can be difficult. +## Namespaces -There are several ways to work with namespaces with the type-safe client. +There are several ways to work with namespaces in a type-safe client +1. Using @Namespace +2. Using @Name (deprecated) -If only 1 level of nesting is used, then the interface can be marked with the @Name annotation. +> [NOTE] You can only use one of the annotations - @Name or @Namespace over the GraphQLApi classes. -``` -query { - users { - findAll { - .... - } - } -} -``` +### Using @Namespace annotation -```java -@Name("users") -@GraphQLClientApi -public interface ApiClient { - List findAll(); -} -``` +The annotation accepts an array of strings containing the nesting of the namespace. +This method allows you to create any nesting of namespaces. +You can use any nesting and also combine different levels. -You also can use Wrapper class (but the code doesn't look great.). +If remote graphql api has next schema +``` +"Query root" +type Query { + admin: AdminQuery +} -Modify server code from above. -```java -@Name("roles") // Add namespace -@GraphQLApi -public class RoleApi { - //// +type AdminQuery { + users: AdminUsersQuery } -``` -Client code -```java -// Name field roles as method named -public record RolesWrapper(List findAllRolesByUserId) { +type AdminUsersQuery { + findAll: User } -@GraphQLClientApi -public interface ApiClient { - @Query("roles") // required naming as namespace - RolesWrapper getRoles( // extend nested params - @NestedParameter("findAllRolesByUserId") UUID userId, // here roles id name of field in wrapper class - @NestedParameter("findAllRolesByUserId.permission") @Name("limit") int permissionLimit, - @NestedParameter("findAllRolesByUserId.permission.permissionType") @Name("limit") int permissionTypeLimit - ); +type User { + id: BigInteger + ... } ``` -If you have more than 1 level of nesting and want to use a type-safe client, the only way is with wrapper classes. +You can create next interface ```java -@Name("admin") -@GraphQLApi -public class RoleApi { - public static class RoleQueryNamespace{ } - - @Query("roles") - public RoleQueryNamespace roleQueryNamespace(){ - return new RoleQueryNamespace(); - } - - public List findAll(@Source RoleQueryNamespace namespace, UUID userId) {} - public List permission(@Source Role role, @DefaultValue("5") int limit) {} - public List permissionType(@Source Permission permissions, @DefaultValue("5") int limit) {} +@Namespace({"admin", "users"}) +@GraphQLClientApi +public interface UsersClient { + List findAll(); } ``` -Query will be like +Here will be generated next query ``` -query { +query AminUsersFindAll { admin { - roles { - findAll(userId: ...) { + users { + findAll { id - permission(limit: 1) { - id - permissionType(limit: 2) { - id - } - } } } } } ``` -```java -// Without Name annotation it must have name as is in schema -public record AdminWrapper(RolesWrapper roles) { // namespace in schema - public record RolesWrapper(List findAllRolesByUserId){} // name method in schema -} +### Using @Name (deprecated) +> [NOTE] This feature may be removed in the future. -@GraphQLClientApi(endpoint = "http://localhost:8081/graphql") -public interface ApiClient { - @Query("admin") - AdminWrapper findAllRolesByUserId( - // nested params must be such as field names in wrapper class - @NestedParameter("roles.findAllRolesByUserId") UUID userId, - @NestedParameter("roles.findAllRolesByUserId.permission") @Name("limit") int permissionLimit, - @NestedParameter("roles.findAllRolesByUserId.permission.permissionType") @Name("limit") int permissionTypeLimit - ); -} -/// or -public record AdminWrapper(@Name("roles") RolesWrapper rolesWrapper) { // @Name like namespace in schema - public record RolesWrapper(@Name("findAllRolesByUserId") List roles){} // @Name like method name in schema +Does the same thing as @Namespace, the only difference is that there can only be one nesting level. + +``` +query { + users { + findAll { + .... + } + } } +``` +```java +@Name("users") @GraphQLClientApi public interface ApiClient { - @Query("admin") - AdminWrapper findAllRolesByUserId( - // nested params value must be such as field names in wrapper class - @NestedParameter("rolesWrapper.roles") UUID userId, - @NestedParameter("rolesWrapper.roles.permission") @Name("limit") int permissionLimit, - @NestedParameter("rolesWrapper.roles.permission.permissionType") @Name("limit") int permissionTypeLimit - ); + List findAll(); } ``` -As you can see, the code with wrapper classes is quite messy, so we don't recommend using this approach in a type-safe client. diff --git a/server/api/src/main/java/io/smallrye/graphql/api/Namespace.java b/server/api/src/main/java/io/smallrye/graphql/api/Namespace.java new file mode 100644 index 000000000..4d8d3b784 --- /dev/null +++ b/server/api/src/main/java/io/smallrye/graphql/api/Namespace.java @@ -0,0 +1,20 @@ +package io.smallrye.graphql.api; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.smallrye.common.annotation.Experimental; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +@Documented +@Experimental("Grouping APIs by nested namespaces") +public @interface Namespace { + /** + * @return Array of nested namespaces + */ + String[] value(); +} diff --git a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/producer/GraphQLProducer.java b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/producer/GraphQLProducer.java index 584f8f3ee..93ec36780 100644 --- a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/producer/GraphQLProducer.java +++ b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/producer/GraphQLProducer.java @@ -1,5 +1,8 @@ package io.smallrye.graphql.cdi.producer; +import java.util.Optional; +import java.util.concurrent.SubmissionPublisher; + import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Produces; @@ -23,6 +26,10 @@ public void setSchema(Schema schema) { this.schema = schema; } + public void setTraficPublisher(SubmissionPublisher traficPublisher) { + this.traficPublisher = Optional.of(traficPublisher); + } + public GraphQLSchema initialize(Schema schema) { return initialize(schema, null, null); } @@ -50,7 +57,7 @@ public GraphQLSchema initialize(boolean allowMultipleDeployments, ExecutionStrat ExecutionStrategy mutationExecutionStrategy) { this.graphQLSchema = Bootstrap.bootstrap(schema, allowMultipleDeployments); - this.executionService = new ExecutionService(graphQLSchema, this.schema, queryExecutionStrategy, + this.executionService = new ExecutionService(graphQLSchema, this.schema, this.traficPublisher, queryExecutionStrategy, mutationExecutionStrategy); return this.graphQLSchema; } @@ -72,6 +79,8 @@ public GraphQLSchema initialize() { @Produces Schema schema; + private Optional> traficPublisher = Optional.empty(); + @Produces @Dependent public CDISmallRyeContext produceSmallRyeContext() { diff --git a/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java b/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java index 6a4a189ce..48e02047d 100644 --- a/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java +++ b/server/implementation-servlet/src/main/java/io/smallrye/graphql/entry/http/IndexInitializer.java @@ -29,6 +29,7 @@ import org.jboss.jandex.Indexer; import io.smallrye.graphql.api.Entry; +import io.smallrye.graphql.api.Namespace; import io.smallrye.graphql.api.OneOf; import io.smallrye.graphql.api.federation.Authenticated; import io.smallrye.graphql.api.federation.ComposeDirective; @@ -126,7 +127,7 @@ private IndexView createCustomIndex() { indexer.index(convertClassToInputStream(ScopeItem.class)); indexer.index(convertClassToInputStream(Shareable.class)); indexer.index(convertClassToInputStream(Tag.class)); - indexer.index(convertClassToInputStream(OneOf.class)); + indexer.index(convertClassToInputStream(Namespace.class)); indexer.index(convertClassToInputStream(Resolver.class)); } catch (IOException ex) { throw new RuntimeException(ex); 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 0b55347dd..58fa24f25 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 @@ -20,7 +20,6 @@ import java.util.Set; import java.util.Stack; import java.util.stream.Collectors; -import java.util.stream.Stream; import jakarta.json.JsonReader; import jakarta.json.JsonReaderFactory; @@ -82,8 +81,8 @@ import io.smallrye.graphql.schema.model.EnumType; import io.smallrye.graphql.schema.model.EnumValue; import io.smallrye.graphql.schema.model.Field; -import io.smallrye.graphql.schema.model.Group; import io.smallrye.graphql.schema.model.InputType; +import io.smallrye.graphql.schema.model.NamespaceContainer; import io.smallrye.graphql.schema.model.Operation; import io.smallrye.graphql.schema.model.Reference; import io.smallrye.graphql.schema.model.ReferenceType; @@ -156,12 +155,8 @@ private void verifyInjectionIsAvailable() { LookupService lookupService = LookupService.get(); // This crazy stream operation basically collects all class names where we need to verify that // it belongs to an injectable bean - Stream.of( - schema.getQueries().stream().map(Operation::getClassName), - schema.getMutations().stream().map(Operation::getClassName), - schema.getGroupedQueries().values().stream().flatMap(Collection::stream).map(Operation::getClassName), - schema.getGroupedMutations().values().stream().flatMap(Collection::stream).map(Operation::getClassName)) - .flatMap(stream -> stream) + schema.getAllOperations().stream() + .map(Operation::getClassName) .distinct().forEach(beanClassName -> { // verify that the bean is injectable if (!lookupService.isResolvable(classloadingService.loadClass(beanClassName))) { @@ -378,8 +373,8 @@ private GraphQLObjectType addQueries(GraphQLSchema.Builder schemaBuilder) { if (schema.hasQueries()) { addRootObject(queryBuilder, schema.getQueries(), QUERY); } - if (schema.hasGroupedQueries()) { - addGroupedRootObject(queryBuilder, schema.getGroupedQueries(), QUERY); + if (schema.hasNamespaceQueries()) { + addNamespacedRootObject(queryBuilder, schema.getNamespacedQueries(), QUERY); } GraphQLObjectType query = queryBuilder.build(); @@ -395,8 +390,8 @@ private void addMutations(GraphQLSchema.Builder schemaBuilder) { if (schema.hasMutations()) { addRootObject(mutationBuilder, schema.getMutations(), MUTATION); } - if (schema.hasGroupedMutations()) { - addGroupedRootObject(mutationBuilder, schema.getGroupedMutations(), MUTATION); + if (schema.hasNamespaceMutations()) { + addNamespacedRootObject(mutationBuilder, schema.getNamespacedMutations(), MUTATION); } GraphQLObjectType mutation = mutationBuilder.build(); @@ -413,9 +408,6 @@ private void addSubscriptions(GraphQLSchema.Builder schemaBuilder) { if (schema.hasSubscriptions()) { addRootObject(subscriptionBuilder, schema.getSubscriptions(), SUBSCRIPTION); } - if (schema.hasGroupedSubscriptions()) { - addGroupedRootObject(subscriptionBuilder, schema.getGroupedSubscriptions(), SUBSCRIPTION); - } GraphQLObjectType subscription = subscriptionBuilder.build(); if (subscription.getFieldDefinitions() != null && !subscription.getFieldDefinitions().isEmpty()) { @@ -434,48 +426,86 @@ private void addRootObject(GraphQLObjectType.Builder rootBuilder, Set } } - private void addGroupedRootObject(GraphQLObjectType.Builder rootBuilder, - Map> operationMap, String rootName) { - Set>> operationsSet = operationMap.entrySet(); + private String makeFirstLetterUppercase(String value) { + return value.substring(0, 1).toUpperCase() + value.substring(1); + } - for (Map.Entry> operationsEntry : operationsSet) { - Group group = operationsEntry.getKey(); - Set operations = operationsEntry.getValue(); + private void addNamespacedRootObject(GraphQLObjectType.Builder rootBuilder, + Map namespaceMutations, String mutation) { + namespaceMutations.values() + .forEach(groupContainer -> addNamespacedRootObject(rootBuilder, groupContainer, "", mutation)); + } - GraphQLObjectType namedType = createNamedType(rootName, group, operations); + private List addNamespacedRootObject(GraphQLObjectType.Builder rootBuilder, + NamespaceContainer groupContainer, String rootName, String suffix) { + List graphQLFieldDefinitions = groupContainer.getContainer().isEmpty() + ? List.of() + : getGraphQLFieldDefinition(groupContainer, rootName, suffix); - GraphQLFieldDefinition.Builder graphQLFieldDefinitionBuilder = GraphQLFieldDefinition.newFieldDefinition() - .name(group.getName()).description(group.getDescription()); + if (groupContainer.getOperations().isEmpty() && graphQLFieldDefinitions.isEmpty()) { + return List.of(); + } - graphQLFieldDefinitionBuilder.type(namedType); + GraphQLObjectType namedType = createNamespaceType(rootName, suffix, groupContainer, + groupContainer.getOperations(), graphQLFieldDefinitions); - DataFetcher dummyDataFetcher = dfe -> namedType.getName(); + GraphQLFieldDefinition.Builder graphQLFieldDefinitionBuilder = GraphQLFieldDefinition + .newFieldDefinition() + .name(groupContainer.getName()) + .description(groupContainer.getDescription()); - GraphQLFieldDefinition namedField = graphQLFieldDefinitionBuilder.build(); + graphQLFieldDefinitionBuilder.type(namedType); - this.codeRegistryBuilder.dataFetcherIfAbsent( - FieldCoordinates.coordinates(rootName, namedField.getName()), - dummyDataFetcher); + DataFetcher dummyDataFetcher = dfe -> namedType.getName(); - rootBuilder.field(namedField); - } + GraphQLFieldDefinition namedField = graphQLFieldDefinitionBuilder.build(); + + this.codeRegistryBuilder.dataFetcherIfAbsent( + FieldCoordinates.coordinates(rootName + suffix, namedField.getName()), + dummyDataFetcher); + rootBuilder.field(namedField); + + return List.of(namedField); + } + + private List getGraphQLFieldDefinition(NamespaceContainer groupContainer, String rootName, + String suffix) { + String name = makeFirstLetterUppercase(groupContainer.getName()); + String namedTypeName = rootName + name + suffix; + + GraphQLObjectType.Builder wrapperBuilder = GraphQLObjectType.newObject() + .name(namedTypeName) + .description(groupContainer.getDescription()); + + return groupContainer + .getContainer() + .values() + .stream() + .map(namespace -> addNamespacedRootObject( + wrapperBuilder, namespace, rootName + makeFirstLetterUppercase(groupContainer.getName()), suffix)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); } - private GraphQLObjectType createNamedType(String parent, Group group, Set operations) { - String namedTypeName = group.getName() + parent; + private GraphQLObjectType createNamespaceType(String root, String suffix, NamespaceContainer namespace, + Set operations, List graphQLFieldDefinitions) { + String name = makeFirstLetterUppercase(namespace.getName()); + + String namedTypeName = root + name + suffix; GraphQLObjectType.Builder objectTypeBuilder = GraphQLObjectType.newObject() .name(namedTypeName) - .description(group.getDescription()); + .description(namespace.getDescription()); // Operations for (Operation operation : operations) { operation = eventEmitter.fireCreateOperation(operation); - GraphQLFieldDefinition graphQLFieldDefinition = createGraphQLFieldDefinitionFromOperation(namedTypeName, - operation); + GraphQLFieldDefinition graphQLFieldDefinition = createGraphQLFieldDefinitionFromOperation( + namedTypeName, operation); objectTypeBuilder = objectTypeBuilder.field(graphQLFieldDefinition); } + objectTypeBuilder.fields(graphQLFieldDefinitions); return objectTypeBuilder.build(); } @@ -1037,11 +1067,6 @@ private GraphQLOutputType referenceGraphQLOutputType(Field field) { } } - private GraphQLInputType referenceGraphQLInputType(Field field) { - Reference reference = getCorrectFieldReference(field); - return getGraphQLInputType(reference); - } - private GraphQLInputType getGraphQLInputType(Reference reference) { ReferenceType type = reference.getType(); String className = reference.getClassName(); diff --git a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/FederationDataFetcher.java b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/FederationDataFetcher.java index 279da5937..61a8c6277 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/FederationDataFetcher.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/FederationDataFetcher.java @@ -11,6 +11,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; @@ -40,7 +41,7 @@ public class FederationDataFetcher implements DataFetcher cache = new HashMap<>(); + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); public FederationDataFetcher(GraphQLObjectType resolversType, GraphQLObjectType queryType, GraphQLCodeRegistry codeRegistry) { 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 db07c72df..4abf6dc47 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 @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.SubmissionPublisher; import java.util.concurrent.atomic.AtomicLong; import jakarta.json.JsonObject; @@ -76,14 +77,26 @@ public class ExecutionService { private final QueryCache queryCache; private final LogPayloadOption payloadOption; + private final Optional> traficPublisher; private final ExecutionStrategy queryExecutionStrategy; private final ExecutionStrategy mutationExecutionStrategy; public ExecutionService(GraphQLSchema graphQLSchema, Schema schema) { - this(graphQLSchema, schema, null, null); + this(graphQLSchema, schema, Optional.empty(), null, null); } - public ExecutionService(GraphQLSchema graphQLSchema, Schema schema, ExecutionStrategy queryExecutionStrategy, + public ExecutionService(GraphQLSchema graphQLSchema, Schema schema, Optional> traficPublisher) { + this(graphQLSchema, schema, traficPublisher, null, null); + } + + public ExecutionService(GraphQLSchema graphQLSchema, Schema schema, + ExecutionStrategy queryExecutionStrategy, + ExecutionStrategy mutationExecutionStrategy) { + this(graphQLSchema, schema, Optional.empty(), queryExecutionStrategy, mutationExecutionStrategy); + } + + public ExecutionService(GraphQLSchema graphQLSchema, Schema schema, Optional> traficPublisher, + ExecutionStrategy queryExecutionStrategy, ExecutionStrategy mutationExecutionStrategy) { this.graphQLSchema = graphQLSchema; @@ -98,6 +111,7 @@ public ExecutionService(GraphQLSchema graphQLSchema, Schema schema, ExecutionStr Config config = Config.get(); this.payloadOption = config.logPayload(); + this.traficPublisher = traficPublisher; } @Deprecated @@ -149,12 +163,8 @@ public void execute(JsonObject jsonInput, Map context, Execution sendError("Missing 'query' field in the request", writer); return; } - if (payloadOption.equals(LogPayloadOption.queryOnly)) { - log.payloadIn(query); - } else if (payloadOption.equals(LogPayloadOption.queryAndVariables)) { - log.payloadIn(query); - log.payloadIn(variables.toString()); - } + + logInput(query, variables); GraphQL g = getGraphQL(); if (g != null) { @@ -267,9 +277,8 @@ private void notifyAndWrite(SmallRyeContext smallRyeContext, eventEmitter.fireAfterExecute(smallRyeContext); ExecutionResponse executionResponse = new ExecutionResponse(smallRyeContext.unwrap(ExecutionResult.class), smallRyeContext.getAddedExtensions()); - if (!payloadOption.equals(LogPayloadOption.off)) { - log.payloadOut(executionResponse.toString()); - } + + logOutput(executionResponse); writer.write(executionResponse); } @@ -377,4 +386,34 @@ private void setParserOptions(Config config) { ParserOptions.setDefaultParserOptions(parserOptionsBuilder.build()); } } + + private void logInput(String query, Optional> variables) { + if (payloadOption.equals(LogPayloadOption.queryOnly)) { + log.payloadIn(query); + } else if (payloadOption.equals(LogPayloadOption.queryAndVariables)) { + log.payloadIn(query); + if (variables.isPresent()) { + log.payloadIn(variables.get().toString()); + } + } + + // Also submit to the provided publisher + if (traficPublisher.isPresent()) { + if (variables.isPresent()) { + traficPublisher.get().submit(variables.get().toString()); + } + traficPublisher.get().submit("> " + query); + } + } + + private void logOutput(ExecutionResponse executionResponse) { + if (!payloadOption.equals(LogPayloadOption.off)) { + log.payloadOut(executionResponse.getExecutionResultAsJsonObject().toString()); + } + + // Also submit to the provided publisher + if (traficPublisher.isPresent()) { + traficPublisher.get().submit("< " + executionResponse.getExecutionResultAsJsonObject().toString()); + } + } } diff --git a/server/implementation/src/test/java/io/smallrye/graphql/execution/ExperimentalNamespaceTest.java b/server/implementation/src/test/java/io/smallrye/graphql/execution/ExperimentalNamespaceTest.java new file mode 100644 index 000000000..4c75555db --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/execution/ExperimentalNamespaceTest.java @@ -0,0 +1,95 @@ +package io.smallrye.graphql.execution; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.util.stream.Stream; + +import jakarta.json.JsonObject; + +import org.jboss.jandex.IndexView; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import graphql.schema.GraphQLSchema; +import io.smallrye.graphql.bootstrap.Bootstrap; +import io.smallrye.graphql.schema.SchemaBuilder; +import io.smallrye.graphql.schema.model.Schema; +import io.smallrye.graphql.test.namespace.ExperimentalNamespaceApi; +import io.smallrye.graphql.test.namespace.ExperimentalNamespaceWithErrorApi; + +/** + * Test for Federated namespaces + */ +public class ExperimentalNamespaceTest { + private static ExecutionService executionService; + + private static IndexView buildIndex(Class... classes) { + org.jboss.jandex.Indexer indexer = new org.jboss.jandex.Indexer(); + Stream.of(classes).forEach(cls -> index(indexer, cls)); + return indexer.complete(); + } + + private static InputStream getResourceStream(Class type) { + String name = type.getName().replace(".", "/") + ".class"; + return Thread.currentThread().getContextClassLoader().getResourceAsStream(name); + } + + private static void index(org.jboss.jandex.Indexer indexer, Class cls) { + try { + indexer.index(getResourceStream(cls)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static GraphQLSchema createGraphQLSchema(IndexView index) { + Schema schema = SchemaBuilder.build(index); + assertNotNull(schema, "Schema should not be null"); + GraphQLSchema graphQLSchema = Bootstrap.bootstrap(schema, true); + assertNotNull(graphQLSchema, "GraphQLSchema should not be null"); + return graphQLSchema; + } + + private static JsonObject executeAndGetResult(String graphQL) { + JsonObjectResponseWriter jsonObjectResponseWriter = new JsonObjectResponseWriter(graphQL); + jsonObjectResponseWriter.logInput(); + executionService.executeSync(jsonObjectResponseWriter.getInput(), jsonObjectResponseWriter); + jsonObjectResponseWriter.logOutput(); + return jsonObjectResponseWriter.getOutput(); + } + + @Test + public void experimentalNamespaceTest() { + IndexView index = buildIndex(ExperimentalNamespaceApi.class); + + GraphQLSchema graphQLSchema = createGraphQLSchema(index); + Schema schema = SchemaBuilder.build(index); + executionService = new ExecutionService(graphQLSchema, schema); + + JsonObject jsonObject = executeAndGetResult(NAMESPACED_QUERY); + assertNotNull(jsonObject); + + String result = jsonObject.getJsonObject("data") + .getJsonObject("admin") + .getJsonObject("users") + .getString("find"); + assertEquals(result, "AdminUsersFind"); + } + + @Test + public void experimentalNamespaceFailureWithUsingNameTest() { + IndexView index = buildIndex(ExperimentalNamespaceWithErrorApi.class); + Assertions.assertThrows(RuntimeException.class, () -> SchemaBuilder.build(index)); + } + + private static final String NAMESPACED_QUERY = "query AminUsersFind {\n" + + " admin {\n" + + " users {\n" + + " find \n" + + " }\n" + + " }\n" + + "}"; +} diff --git a/server/implementation/src/test/java/io/smallrye/graphql/test/namespace/ExperimentalNamespaceApi.java b/server/implementation/src/test/java/io/smallrye/graphql/test/namespace/ExperimentalNamespaceApi.java new file mode 100644 index 000000000..c385159c6 --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/test/namespace/ExperimentalNamespaceApi.java @@ -0,0 +1,15 @@ +package io.smallrye.graphql.test.namespace; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; + +import io.smallrye.graphql.api.Namespace; + +@GraphQLApi +@Namespace({ "admin", "users" }) +public class ExperimentalNamespaceApi { + @Query + public String find() { + return "AdminUsersFind"; + } +} diff --git a/server/implementation/src/test/java/io/smallrye/graphql/test/namespace/ExperimentalNamespaceWithErrorApi.java b/server/implementation/src/test/java/io/smallrye/graphql/test/namespace/ExperimentalNamespaceWithErrorApi.java new file mode 100644 index 000000000..425a6cbf2 --- /dev/null +++ b/server/implementation/src/test/java/io/smallrye/graphql/test/namespace/ExperimentalNamespaceWithErrorApi.java @@ -0,0 +1,17 @@ +package io.smallrye.graphql.test.namespace; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; + +import io.smallrye.graphql.api.Namespace; + +@GraphQLApi +@Name("users") +@Namespace({ "admin", "users" }) +public class ExperimentalNamespaceWithErrorApi { + @Query + public String find() { + return "AdminUsersFind"; + } +}