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