spring-graphql icon indicating copy to clipboard operation
spring-graphql copied to clipboard

Update documentation with advice on how to achieve schema namespacing

Open can-axelspringer opened this issue 2 years ago • 5 comments

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/

can-axelspringer avatar Dec 06 '23 13:12 can-axelspringer

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
    }

wadajo avatar Dec 14 '23 12:12 wadajo

Stacked into one file, it is very difficult to maintain. Is it possible to support sub-modules Query and Mutation?

Jamel-jun avatar Mar 01 '24 07:03 Jamel-jun

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) {
    }

}

  1. declaring @SchemaMapping(typeName="MusicQueries") on the controller avoids to repeat this typeName declaration on all endpoints.
  2. the @QueryMapping declaration provides an "empty" MusicQueries object, 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?

bclozel avatar Mar 20 '24 14:03 bclozel

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.

rstoyanchev avatar Mar 20 '24 16:03 rstoyanchev

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.

can-axelspringer avatar Mar 21 '24 08:03 can-axelspringer