Update documentation with advice on how to achieve schema namespacing
Hello,
I hope this is the right place to ask this question. I could not find anything on the internet that could provide me with an answer.
Is it possible to define schemas like following:
type UserQueries {
get(id: String! @NotEmpty): String
}
type UserMutations {
delete(id: String! @NotEmpty): String
}
extend type Mutation {
users: UserMutations
}
extend type Query {
users: UserQueries
}
When I attempt to do it like that. I can't make annotated controller model work.
@Controller
@SchemaMapping(field = "users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@SchemaMapping(typeName = "UsersMutations", field = "delete")
String delete(@Argument String id) {
userService.delete(id);
return id;
}
}
- Reference: https://www.apollographql.com/docs/technotes/TN0012-namespacing-by-separation-of-concern/
As far as I understand, in Spring for GraphQL you should define your schema like this:
type Mutation {
deleteUser(id: String! @NotEmpty): String
}
type Query {
getUser(id: String! @NotEmpty): String
}
And then, in your Controller:
@QueryMapping
String getUser(@Argument id){
// business logic
}
@MutationMapping
String deleteUser(@Argument id){
// business logic
}
Stacked into one file, it is very difficult to maintain. Is it possible to support sub-modules Query and Mutation?
Sorry for the delayed response. I had a look into this and I'll share my findings here.
First @SchemaMapping at the type level is already supported for the typeName field as described in the annotation support section of the reference documentation. This is implemented by the AnnotatedControllerConfigurer.
I think that the suggested approach with @SchemaMapping(field = "users") doesn't really make sense here, since our controller method provides a data fetcher for the UserMutations type and its delete field. It is declared as it should on the controller method directly with @SchemaMapping(typeName = "UsersMutations", field = "delete").
Here's, what's missing is the actual UserMutations type and how it relates to our application. I managed to implement this in a sample application with the following:
type Query {
music: MusicQueries
users: UserQueries
}
type MusicQueries {
album(id: ID!): Album
searchForArtist(name: String!): [Artist]
}
type Album {
id: ID!
title: String!
}
type Artist {
id: ID!
name: String!
}
type UserQueries {
user(login: String): User
}
type User {
id: ID!
login: String!
}
package io.spring.sample.graphqlnamespace;
import java.util.List;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
@Controller
@SchemaMapping(typeName="MusicQueries")
public class MusicController {
@QueryMapping
public MusicQueries music() {
return new MusicQueries();
}
@SchemaMapping
public Album album(@Argument String id) {
return new Album(id, "Spring GraphQL");
}
@SchemaMapping
public List<Artist> searchForArtist(@Argument String name) {
return List.of(new Artist("100", "the Spring team"));
}
public record MusicQueries() {
}
public record Album(String id, String title) {
}
public record Artist(String id, String name) {
}
}
- declaring
@SchemaMapping(typeName="MusicQueries")on the controller avoids to repeat this typeName declaration on all endpoints. - the
@QueryMappingdeclaration provides an "empty"MusicQueriesobject, but could also return an empty map. Here, we could choose to move this to a separate controller if we wanted to (to regroup all root namespaces).
I don't think additional support is needed in Spring for GraphQL right now. We could turn this into a documentation issue and mention this pattern in the reference docs. @can-axelspringer would this work for you?
The UserQueries and UserMutations are just wrappers, and it doesn't mater what type is returned, even an empty map should do, since all of their child fields are mapped. You could do that from a controller as Brian showed above, or you could also register them all from a single place like so:
@Bean
public GraphQlSourceBuilderCustomizer customizer() {
List<String> queryWrappers = List.of("users", ... );
List<String> mutationWrappers = List.of("users", ... );
return sourceBuilder -> sourceBuilder.configureRuntimeWiring(wiringBuilder -> {
queryWrappers.forEach(field -> wiringBuilder.type("Query",
builder -> builder.dataFetcher(field, env -> Collections.emptyMap())));
mutationWrappers.forEach(field -> wiringBuilder.type("Mutation",
builder -> builder.dataFetcher(field, env -> Collections.emptyMap())));
});
}
There isn't much we can do to make this easier, and it seems pretty straight forward in any case.
Thank you very much for the help and support.
I was able to reproduce proper namespacing setup using given examples.
@bclozel and @rstoyanchev I checked the documentation before creating this issue but was not able to realize name-spacing setup in our project. It might have been due to my lack of understanding of the documentation or documentation does not delve deep into annotated controller model enough, but it could be great if we can have a subsection in the documentation for name-spacing and separation of concerns.(?)
Thank you all again.