Document schema Namespacing with annotated controllers

Closes gh-863
This commit is contained in:
Brian Clozel
2024-04-16 17:16:44 +02:00
parent 2a54ef486d
commit 1d98f9b45f
8 changed files with 224 additions and 0 deletions

View File

@@ -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}

View File

@@ -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 {

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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>
});
}
}

View File

@@ -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!
}