Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add api group names for typesafe clients #2169

Merged
merged 5 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ public String build() {
if (method.hasValueParameters())
request.append(method.valueParameters().map(this::declare).collect(joining(", ", "(", ")")));

String groupName = method.getGroupName();
if (groupName != null) {
request.append(" { ");
request.append(groupName);
}

if (method.isSingle()) {
request.append(" { ");
request.append(method.getName());
Expand All @@ -41,6 +47,9 @@ public String build() {
if (method.isSingle())
request.append(" }");

if (groupName != null)
request.append(" } ");

return request.toString();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ public Object read() {
private JsonObject readData() {
if (!response.containsKey("data") || response.isNull("data"))
return null;
JsonObject data = response.getJsonObject("data");
String groupName = method.getGroupName();
JsonObject data = groupName != null
? response.getJsonObject("data").getJsonObject(groupName)
: response.getJsonObject("data");
if (method.isSingle() && !data.containsKey(method.getName()))
throw new InvalidResponseException("No data for '" + method.getName() + "'");
return data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ 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 List<ParameterInfo> parameters;

private MethodInvocation(TypeInfo type, Method method, Object[] parameterValues) {
this.type = type;
this.method = method;
this.parameterValues = parameterValues;
this.groupName = readGroupName(method);
}

@Override
Expand Down Expand Up @@ -259,4 +261,19 @@ public String getOperationTypeAsString() {
return "query";
}
}

public String getGroupName() {
return groupName;
}

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;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ public String build() {
request.append(method.valueParameters().stream().map(method::declare).collect(joining(", ", "(", ")")));
}

String groupName = method.getGroupName();
if (groupName != null) {
request.append(" { ");
request.append(groupName);
}

if (method.isSingle()) {
request.append(" { ");
request.append(method.getName());
Expand All @@ -58,6 +64,9 @@ public String build() {
if (method.isSingle())
request.append(" }");

if (groupName != null)
request.append(" } ");

return request.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class OperationModel implements NamedElement {
private final Stack<String> expressionStack = new Stack<>();
private Stack<TypeModel> rawParametrizedTypes = new Stack<>();
private final List<DirectiveInstance> directives;
private final String groupName;

/**
* Creates a new {@code OperationModel} instance based on the provided Jandex {@link MethodInfo}.
Expand All @@ -49,6 +50,7 @@ public class OperationModel implements NamedElement {
getDirectiveLocation(), AnnotationTarget.Kind.METHOD)
.map(DirectiveInstance::of)
.collect(toList());
this.groupName = readGroupName(method);
}

/**
Expand Down Expand Up @@ -388,4 +390,23 @@ private Optional<AnnotationInstance> getMethodAnnotation(DotName annotation) {
private boolean isRawParametrizedType(TypeModel type) {
return type.isCustomParametrizedType() && !type.getFirstRawType().isTypeVariable();
}

public String getGroupName() {
return groupName;
}

private String readGroupName(MethodInfo method) {
List<AnnotationInstance> 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;
}
}
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Set;

import org.eclipse.microprofile.graphql.Mutation;
import org.eclipse.microprofile.graphql.Name;
import org.eclipse.microprofile.graphql.Query;
import org.jboss.jandex.Index;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -210,6 +211,39 @@ void inheritedOperationsClientModelTest() throws IOException {
"query allStrings0 { allStrings0 }");
}

@Name("named")
@GraphQLClientApi(configKey = "string-api")
interface NamedClientApi {
@Query("findAll")
List<String> findAllStringsQuery();

@Name("findAll")
List<String> findAllStringsName();

@Mutation("update")
@Name("update")
String update(String s);
}

@Test
void namedClientModelTest() throws IOException {
String configKey = "string-api";
ClientModels clientModels = ClientModelBuilder
.build(Index.of(NamedClientApi.class));
assertNotNull(clientModels.getClientModelByConfigKey(configKey));
ClientModel clientModel = clientModels.getClientModelByConfigKey(configKey);
assertEquals(3, clientModel.getOperationMap().size());
assertOperation(clientModel,
new MethodKey("findAllStringsQuery", new Class[0]),
"query findAll { named { findAll } } ");
assertOperation(clientModel,
new MethodKey("findAllStringsName", new Class[0]),
"query findAll { named { findAll } } ");
assertOperation(clientModel,
new MethodKey("update", new Class[] { String.class }),
"mutation update($s: String) { named { update(s: $s) } } ");
}

private void assertOperation(ClientModel clientModel, MethodKey methodKey, String expectedQuery) {
String actualQuery = clientModel.getOperationMap().get(methodKey);
assertEquals(expectedQuery, actualQuery);
Expand Down
197 changes: 197 additions & 0 deletions docs/namespaces-on-server-side.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# 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.

### 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 {
@Query
public List<User> findAll() {
//
}
}

@GraphQLApi
@Name("roles")
@Description("Roles operations")
public class RoleApi {
@Query
public List<Role> 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 {
}

public class AdminMutationNamespace {
}

public class UserQueryNamespace {
}

public class UserMutationNamespace {
}

@GraphQLApi
public class UserApi {
@Query("admin")
public AdminQueryNamespace adminQueryNamespace() {
return new AdminQueryNamespace();
}

public UserQueryNamespace userQueryNamespace(@Source AdminQueryNamespace namespace) {
return new UserQueryNamespace();
}

public List<User> 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<User> save(@Source UserMutationNamespace namespace, User user) {
// save user
}
}
```
As a result, you will be able to execute the following query.
```
query {
admin {
users {
findAll {
....
}
}
}
}

mutation {
admin {
users {
save (user: ...) {
....
}
}
}
}
```
### 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 {
}

@GraphQLApi
@Name("admin")
@Description("Users operations")
public class UserApi {
@Query("users")
public UserQueryNamespace userQueryNamespace() {
return new UserQueryNamespace();
}

public List<User> findAll(@Source UserQueryNamespace namespace) {
// return users;
}

@Mutation("users")
public UserMutationNamespace userMutationNamespace() {
return new UserMutationNamespace();
}

public List<User> save(@Source UserMutationNamespace namespace, User user) {
// save user
}
}
```
## Problems
While dividing APIs into namespaces may seem convenient, it has several issues that are important to be aware of.

#### Mutations
Be careful when working with mutations on client.
This violates the Graphql specification, since mutations in this form can be executed in parallel.
Read more here about namespaces [Namespacing by Separation of Concerns](https://www.apollographql.com/docs/technotes/TN0012-namespacing-by-separation-of-concern/).
This article describes how you can work with namespaces, what problems you may encounter, and how to solve them.

What does Graphql say about this - ["GraphQL" Nested Mutations](https://benjie.dev/graphql/nested-mutations)

In summary, you can use nested mutations, but with some overhead on client. Be careful with mutations.

#### Subscriptions
Graphql does not allow creating subscriptions inside namespaces.
Or rather, you can create them, generated schema will be valid, but the subscription will not be resolved.
As example, if you try to run such a subscription request, you will get an error. This is the behavior of `graphql-java`.

```java
@GraphQLApi
@Name("resource")
public class ResourceApi {
@Subscription
public Multi<ResourceSubscription> resourceChange() {
//
}
}
```

```
subscription {
resource {
resourceChange {
....
}
}
}
```

There is currently no way around this problem.
You must move subscriptions into a separate class that is not placed in a namespace.

> [NOTE]
> Be very careful when designing API with namespace.
> And take into account all the features of working with mutations and subscriptions.
Loading
Loading