Skip to content

Commit 78bdf30

Browse files
committed
Support schema transforming GraphQLTypeVisitor
Closes gh-536
1 parent 1a3f098 commit 78bdf30

File tree

5 files changed

+145
-26
lines changed

5 files changed

+145
-26
lines changed

spring-graphql-docs/src/docs/asciidoc/index.adoc

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -284,29 +284,23 @@ The main implementation, `DefaultExecutionGraphQlService`, is configured with a
284284
[[execution-graphqlsource]]
285285
=== `GraphQLSource`
286286

287-
`GraphQlSource` is a core Spring abstraction for access to the
288-
`graphql.GraphQL` instance to use for request execution. It provides a builder API to
289-
initialize GraphQL Java and build a `GraphQlSource`.
290-
291-
The default `GraphQlSource` builder, accessible via
292-
`GraphQlSource.schemaResourceBuilder()`, enables support for
293-
<<execution-reactive-datafetcher>>, <<execution-context>>, and <<execution-exceptions>>.
294-
295-
The Spring Boot {spring-boot-ref-docs}/web.html#web.graphql[starter] initializes a
296-
`GraphQlSource` instance through the default `GraphQlSource.Builder` and also enables
297-
the following:
298-
299-
- Load <<execution-graphqlsource-schema-resources, schema files>> from a configurable location.
300-
- Expose {spring-boot-ref-docs}/application-properties.html#appendix.application-properties.web[properties]
287+
`GraphQlSource` is a contract to expose the `graphql.GraphQL` instance to use that also
288+
includes a builder API to build that instance. The default builder is available via
289+
`GraphQlSource.schemaResourceBuilder()`. The
290+
{spring-boot-ref-docs}/web.html#web.graphql[Spring Boot starter] creates an instance of
291+
this builder and further initializes it as follows:
292+
293+
- Loads <<execution-graphqlsource-schema-resources, schema files>> from a configurable location.
294+
- Exposes {spring-boot-ref-docs}/application-properties.html#appendix.application-properties.web[properties]
301295
that apply to `GraphQlSource.Builder`.
302-
- Detect <<execution-graphqlsource-runtimewiring-configurer>> beans.
303-
- Detect https://www.graphql-java.com/documentation/instrumentation[Instrumentation] beans for
296+
- Detects <<execution-graphqlsource-runtimewiring-configurer>> beans.
297+
- Detects https://www.graphql-java.com/documentation/instrumentation[Instrumentation] beans for
304298
{spring-boot-ref-docs}/actuator.html#actuator.metrics.supported.spring-graphql[GraphQL metrics].
305-
- Detect `DataFetcherExceptionResolver` beans for <<execution-exceptions, exception resolution>>.
306-
- Detect `SubscriptionExceptionResolver` beans for <<execution-exceptions-subsctiption, subscription exception resolution>>.
299+
- Detects `DataFetcherExceptionResolver` beans for <<execution-exceptions, exception resolution>>.
300+
- Detects `SubscriptionExceptionResolver` beans for <<execution-exceptions-subsctiption, subscription exception resolution>>.
307301

308-
For further customizations, you can declare your own `GraphQlSourceBuilderCustomizer` beans;
309-
for example, for configuring your own `ExecutionIdProvider`:
302+
For further customizations, you can declare a `GraphQlSourceBuilderCustomizer` bean. For example, to
303+
configure your own `ExecutionIdProvider`:
310304

311305
[source,java,indent=0,subs="verbatim,quotes"]
312306
----
@@ -345,9 +339,9 @@ locations, e.g. across multiple modules.
345339
[[execution-graphqlsource-schema-creation]]
346340
==== Schema Creation
347341

348-
By default, `GraphQlSource.Builder` uses the GraphQL Java `GraphQLSchemaGenerator` to
349-
create the `graphql.schema.GraphQLSchema`. This works for most applications, but if
350-
necessary, you can hook into the schema creation through the builder:
342+
By default, `GraphQlSource.Builder` uses the GraphQL Java `SchemaGenerator` to create the
343+
`graphql.schema.GraphQLSchema`. This works for typical use, but if you need to use a
344+
different generator, e.g. for federation, you can register a `schemaFactory` callback:
351345

352346
[source,java,indent=0,subs="verbatim,quotes"]
353347
----
@@ -360,10 +354,29 @@ builder.schemaResources(..)
360354
})
361355
----
362356

363-
The primary reason for this is to create the schema through a federation library.
364-
365357
The <<execution-graphqlsource, GraphQlSource section>> explains how to configure that with Spring Boot.
366358

359+
360+
[[execution-graphqlsource-schema-traversal]]
361+
==== Schema Traversal
362+
363+
You can register a `graphql.schema.GraphQLTypeVisitor` via
364+
`builder.schemaResources(..).typeVisitors(..)` if you want to traverse the schema after
365+
it is created, and possibly apply changes to the `GraphQLCodeRegistry`. Keep in mind,
366+
however, that such a visitor cannot change the schema. See
367+
<<execution-graphqlsource-schema-transformation>>, if you need to make changes to the schema.
368+
369+
370+
[[execution-graphqlsource-schema-transformation]]
371+
==== Schema Transformation
372+
373+
You can register a `graphql.schema.GraphQLTypeVisitor` via
374+
`builder.schemaResources(..).typeVisitorsToTransformSchema(..)` if you want to traverse
375+
and transform the schema after it is created, and make changes to the schema. Keep in mind
376+
that this is more expensive than <<execution-graphqlsource-schema-traversal>> so generally
377+
prefer traversal to transformation unless you need to make schema changes.
378+
379+
367380
[[execution-graphqlsource-runtimewiring-configurer]]
368381
==== `RuntimeWiringConfigurer`
369382

spring-graphql/src/main/java/org/springframework/graphql/execution/AbstractGraphQlSourceBuilder.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import graphql.schema.GraphQLCodeRegistry;
2929
import graphql.schema.GraphQLSchema;
3030
import graphql.schema.GraphQLTypeVisitor;
31+
import graphql.schema.SchemaTransformer;
3132
import graphql.schema.SchemaTraverser;
3233

3334

@@ -47,6 +48,8 @@ abstract class AbstractGraphQlSourceBuilder<B extends GraphQlSource.Builder<B>>
4748

4849
private final List<GraphQLTypeVisitor> typeVisitors = new ArrayList<>();
4950

51+
private final List<GraphQLTypeVisitor> typeVisitorsToTransformSchema = new ArrayList<>();
52+
5053
private final List<Instrumentation> instrumentations = new ArrayList<>();
5154

5255
private Consumer<GraphQL.Builder> graphQlConfigurers = (builder) -> {
@@ -71,6 +74,12 @@ public B typeVisitors(List<GraphQLTypeVisitor> typeVisitors) {
7174
return self();
7275
}
7376

77+
@Override
78+
public B typeVisitorsToTransformSchema(List<GraphQLTypeVisitor> typeVisitorsToTransformSchema) {
79+
this.typeVisitorsToTransformSchema.addAll(typeVisitorsToTransformSchema);
80+
return self();
81+
}
82+
7483
@Override
7584
public B instrumentation(List<Instrumentation> instrumentations) {
7685
this.instrumentations.addAll(instrumentations);
@@ -92,6 +101,7 @@ private <T extends B> T self() {
92101
public GraphQlSource build() {
93102
GraphQLSchema schema = initGraphQlSchema();
94103

104+
schema = applyTypeVisitorsToTransformSchema(schema);
95105
schema = applyTypeVisitors(schema);
96106

97107
GraphQL.Builder builder = GraphQL.newGraphQL(schema);
@@ -112,6 +122,14 @@ public GraphQlSource build() {
112122
*/
113123
protected abstract GraphQLSchema initGraphQlSchema();
114124

125+
private GraphQLSchema applyTypeVisitorsToTransformSchema(GraphQLSchema schema) {
126+
SchemaTransformer transformer = new SchemaTransformer();
127+
for (GraphQLTypeVisitor visitor : this.typeVisitorsToTransformSchema) {
128+
schema = transformer.transform(schema, visitor);
129+
}
130+
return schema;
131+
}
132+
115133
private GraphQLSchema applyTypeVisitors(GraphQLSchema schema) {
116134
GraphQLTypeVisitor visitor = ContextDataFetcherDecorator.createVisitor(this.subscriptionExceptionResolvers);
117135
List<GraphQLTypeVisitor> visitors = new ArrayList<>(this.typeVisitors);

spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ interface Builder<B extends Builder<B>> {
105105

106106
/**
107107
* Add {@link GraphQLTypeVisitor}s to visit all element of the created
108-
* {@link graphql.schema.GraphQLSchema}.
108+
* {@link graphql.schema.GraphQLSchema} and make changes to the
109+
* {@link graphql.schema.GraphQLCodeRegistry}.
109110
* <p><strong>Note:</strong> Visitors are applied via
110111
* {@link graphql.schema.SchemaTraverser} and cannot change the schema.
111112
* @param typeVisitors the type visitors
@@ -114,6 +115,21 @@ interface Builder<B extends Builder<B>> {
114115
*/
115116
B typeVisitors(List<GraphQLTypeVisitor> typeVisitors);
116117

118+
/**
119+
* Alternative to {@link #typeVisitors(List)} for visitors that also
120+
* need to make schema changes.
121+
* <p><strong>Note:</strong> Visitors are applied via
122+
* {@link graphql.schema.SchemaTransformer}, and therefore can change
123+
* the schema. However, this is more expensive than using
124+
* {@link graphql.schema.SchemaTraverser}, so generally prefer
125+
* {@link #typeVisitors(List)} if it's not necessary to change the schema.
126+
* @param typeVisitors the type visitors to register
127+
* @return the current builder
128+
* @see graphql.schema.SchemaTransformer#transformSchema(GraphQLSchema, GraphQLTypeVisitor)
129+
* @since 1.1
130+
*/
131+
B typeVisitorsToTransformSchema(List<GraphQLTypeVisitor> typeVisitors);
132+
117133
/**
118134
* Provide {@link Instrumentation} components to instrument the
119135
* execution of GraphQL queries.

spring-graphql/src/test/java/org/springframework/graphql/execution/DefaultSchemaResourceGraphQlSourceBuilderTests.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,22 @@
1616
package org.springframework.graphql.execution;
1717

1818
import java.util.List;
19+
import java.util.concurrent.atomic.AtomicInteger;
1920

21+
import graphql.Scalars;
2022
import graphql.schema.DataFetcher;
2123
import graphql.schema.FieldCoordinates;
2224
import graphql.schema.GraphQLFieldDefinition;
25+
import graphql.schema.GraphQLObjectType;
2326
import graphql.schema.GraphQLSchema;
27+
import graphql.schema.GraphQLSchemaElement;
28+
import graphql.schema.GraphQLTypeVisitor;
29+
import graphql.schema.GraphQLTypeVisitorStub;
2430
import graphql.schema.idl.FieldWiringEnvironment;
2531
import graphql.schema.idl.RuntimeWiring;
2632
import graphql.schema.idl.WiringFactory;
33+
import graphql.util.TraversalControl;
34+
import graphql.util.TraverserContext;
2735
import org.junit.jupiter.api.Test;
2836

2937
import org.springframework.graphql.BookSource;
@@ -45,6 +53,65 @@ void duplicateResourcesAreIgnored() {
4553
GraphQlSetup.schemaResource(BookSource.schema, BookSource.schema).toGraphQlSource();
4654
}
4755

56+
@Test
57+
void typeVisitors() {
58+
59+
AtomicInteger counter = new AtomicInteger();
60+
61+
GraphQLTypeVisitor visitor = new GraphQLTypeVisitorStub() {
62+
63+
@Override
64+
public TraversalControl visitGraphQLObjectType(
65+
GraphQLObjectType node, TraverserContext<GraphQLSchemaElement> context) {
66+
67+
counter.incrementAndGet();
68+
return TraversalControl.CONTINUE;
69+
}
70+
};
71+
72+
GraphQlSetup.schemaContent("type Query { myQuery: String}").typeVisitor(visitor).toGraphQlSource();
73+
74+
assertThat(counter.get()).isPositive();
75+
}
76+
77+
@Test
78+
void typeVisitorToTransformSchema() {
79+
80+
String schemaContent = "" +
81+
"type Query {" +
82+
" person: Person" +
83+
"} " +
84+
"type Person {" +
85+
" firstName: String" +
86+
"}";
87+
88+
GraphQLTypeVisitor visitor = new GraphQLTypeVisitorStub() {
89+
90+
@Override
91+
public TraversalControl visitGraphQLObjectType(
92+
GraphQLObjectType node, TraverserContext<GraphQLSchemaElement> context) {
93+
94+
if (node.getName().equals("Person")) {
95+
node = node.transform(builder -> builder.field(
96+
GraphQLFieldDefinition.newFieldDefinition()
97+
.name("lastName")
98+
.type(Scalars.GraphQLString)
99+
.build()));
100+
changeNode(context, node);
101+
}
102+
103+
return TraversalControl.CONTINUE;
104+
}
105+
};
106+
107+
GraphQLSchema schema = GraphQlSetup.schemaContent(schemaContent)
108+
.typeVisitorToTransformSchema(visitor)
109+
.toGraphQlSource()
110+
.schema();
111+
112+
assertThat(schema.getObjectType("Person").getFieldDefinition("lastName")).isNotNull();
113+
}
114+
48115
@Test
49116
void wiringFactoryList() {
50117

spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ public GraphQlSetup typeVisitor(GraphQLTypeVisitor... visitors) {
117117
return this;
118118
}
119119

120+
public GraphQlSetup typeVisitorToTransformSchema(GraphQLTypeVisitor... visitors) {
121+
this.graphQlSourceBuilder.typeVisitorsToTransformSchema(Arrays.asList(visitors));
122+
return this;
123+
}
124+
120125
public GraphQL toGraphQl() {
121126
return this.graphQlSourceBuilder.build().graphQl();
122127
}

0 commit comments

Comments
 (0)