Skip to content

Commit

Permalink
Document schema Namespacing with annotated controllers
Browse files Browse the repository at this point in the history
Closes gh-863
  • Loading branch information
bclozel committed Apr 16, 2024
1 parent 2a54ef4 commit 1d98f9b
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 0 deletions.
1 change: 1 addition & 0 deletions spring-graphql-docs/antora.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ asciidoc:
chomp: 'all'
docs-site: https://docs.spring.io
include-java: 'example$docs-src/main/java/org/springframework/graphql/docs'
include-resources: 'example$docs-src/main/resources'
github-tag: main
github-repo: spring-projects/spring-graphql
github-raw: https://raw.githubusercontent.com/{github-repo}/{github-tag}
Expand Down
1 change: 1 addition & 0 deletions spring-graphql-docs/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies {
api 'org.springframework:spring-messaging'
api 'org.springframework.data:spring-data-commons'
api 'com.querydsl:querydsl-core'
api "org.springframework.boot:spring-boot-starter-graphql:${springBootVersion}"
}

jar {
Expand Down
64 changes: 64 additions & 0 deletions spring-graphql-docs/modules/ROOT/pages/controllers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -754,3 +754,67 @@ Supported return types are listed below:
| For asynchronous resolution where `<T>` is one of the supported, synchronous, return types.

|===



[[controllers.namespacing]]
== Namespacing

At the schema level, query and mutation operations are defined directly under the `Query` and `Mutation` types.
Rich GraphQL APIs can define dozens of operation sunder those types, making it harder to explore the API and separate concerns.
You can choose to https://www.apollographql.com/docs/technotes/TN0012-namespacing-by-separation-of-concern/[define Namespaces in your GraphQL schema].
While there are some caveats with this approach, you can implement this pattern with Spring for GraphQL annotated controllers.

With namespacing, your GraphQL schema can, for example, nest query operations under top-level types, instead of listing them directly under `Query`.
Here, we will define `MusicQueries` and `UserQueries` types and make them available under `Query`:

[source,json,subs="verbatim,quotes"]
----
include::ROOT:{include-resources}/controllers/namespaces.graphqls[]
----

A GraphQL client would use the `album` query like this:

[source,graphql,subs="verbatim,quotes"]
----
{
music {
album(id: 42) {
id
title
}
}
}
----

And get the following response:

[source,json,subs="verbatim,quotes"]
----
{
"data": {
"music": {
"album": {
"id": "42",
"title": "Spring for GraphQL"
}
}
}
}
----


This can be implemented in a `@Controller` with the following pattern:

include-code::MusicController[]
<1> Annotate the controller with `@SchemaMapping` and a `typeName` attribute, to avoid repeating it on methods
<2> Define a `@QueryMapping` for the "music" namespace
<3> The "music" query returns an "empty" record, but could also return an empty map
<4> Queries are now declared as fields under the "MusicQueries" type

Instead of declaring wrapping types ("MusicQueries", "UserQueries") explicitly in controllers,
you can choose to configure them with the runtime wiring using a `GraphQlSourceBuilderCustomizer` with Spring Boot:

include-code::NamespaceConfiguration[]
<1> List all the wrapper types for the "Query" type
<2> Manually declare data fetchers for each of them, returning an empty Map
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2020-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.graphql.docs.controllers.namespacing;

public record Album(String id, String title) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2020-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.graphql.docs.controllers.namespacing;

public record Artist(String id, String name) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2020-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.graphql.docs.controllers.namespacing;

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") // <1>
public class MusicController {

@QueryMapping // <2>
public MusicQueries music() {
return new MusicQueries();
}

// <3>
public record MusicQueries() {

}

@SchemaMapping // <4>
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"));
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2020-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.graphql.docs.controllers.namespacing;

import java.util.Collections;
import java.util.List;

import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class NamespaceConfiguration {

@Bean
public GraphQlSourceBuilderCustomizer customizer() {
List<String> queryWrappers = List.of("music", "users"); // <1>
return sourceBuilder -> sourceBuilder.configureRuntimeWiring(wiringBuilder -> {
queryWrappers.forEach(field -> wiringBuilder.type("Query",
builder -> builder.dataFetcher(field, env -> Collections.emptyMap()))); // <2>
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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!
}

0 comments on commit 1d98f9b

Please sign in to comment.