diff --git a/src/main/java/graphql/schema/GraphQLSchema.java b/src/main/java/graphql/schema/GraphQLSchema.java index 18f8701a3f..1cc2f341d0 100644 --- a/src/main/java/graphql/schema/GraphQLSchema.java +++ b/src/main/java/graphql/schema/GraphQLSchema.java @@ -931,6 +931,30 @@ public Builder additionalDirective(GraphQLDirective additionalDirective) { return this; } + /** + * Clears all directives from this builder, including any that were previously added + * via {@link #additionalDirective(GraphQLDirective)} or {@link #additionalDirectives(Set)}. + * Built-in directives ({@code @include}, {@code @skip}, {@code @deprecated}, etc.) will + * always be added back automatically at build time by {@code ensureBuiltInDirectives()}. + *
+ * This is useful when transforming a schema to replace all non-built-in directives: + *
{@code
+ * schema.transform(builder -> {
+ * List nonBuiltIns = schema.getDirectives().stream()
+ * .filter(d -> !Directives.isBuiltInDirective(d))
+ * .collect(toList());
+ * builder.clearDirectives()
+ * .additionalDirectives(transform(nonBuiltIns));
+ * })
+ * }
+ *
+ * @return this builder
+ */
+ public Builder clearDirectives() {
+ this.additionalDirectives.clear();
+ return this;
+ }
+
public Builder withSchemaDirectives(GraphQLDirective... directives) {
for (GraphQLDirective directive : directives) {
withSchemaDirective(directive);
diff --git a/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy b/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy
index f331d1d201..f3cafb9abb 100644
--- a/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy
+++ b/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy
@@ -3,6 +3,7 @@ package graphql.schema
import graphql.AssertException
import graphql.Directives
import graphql.ExecutionInput
+import graphql.introspection.Introspection.DirectiveLocation
import graphql.GraphQL
import graphql.TestUtil
import graphql.language.Directive
@@ -168,6 +169,83 @@ class GraphQLSchemaTest extends Specification {
schema = schema.transform({ builder -> builder })
then: "all 7 built-in directives are still present"
schema.directives.size() == 7
+
+ when: "clearDirectives is called"
+ schema = basicSchemaBuilder().clearDirectives().build()
+ then: "all 7 built-in directives are still present because ensureBuiltInDirectives re-adds them"
+ schema.directives.size() == 7
+
+ when: "clearDirectives is called and additional directives are added"
+ schema = basicSchemaBuilder().clearDirectives()
+ .additionalDirective(GraphQLDirective.newDirective()
+ .name("custom")
+ .validLocations(DirectiveLocation.FIELD)
+ .build())
+ .build()
+ then: "all 7 built-in directives are present plus the additional one"
+ schema.directives.size() == 8
+ schema.getDirective("custom") != null
+ }
+
+ def "clearDirectives supports replacing non-built-in directives in a schema transform"() {
+ given: "a schema with a custom directive"
+ def originalDirective = GraphQLDirective.newDirective()
+ .name("custom")
+ .description("v1")
+ .validLocations(DirectiveLocation.FIELD)
+ .build()
+ def schema = basicSchemaBuilder()
+ .additionalDirective(originalDirective)
+ .build()
+ assert schema.directives.size() == 8
+
+ when: "the schema is transformed to replace the custom directive"
+ def replacementDirective = GraphQLDirective.newDirective()
+ .name("custom")
+ .description("v2")
+ .validLocations(DirectiveLocation.FIELD)
+ .build()
+ def newSchema = schema.transform({ builder ->
+ def nonBuiltIns = schema.getDirectives().findAll { !Directives.isBuiltInDirective(it) }
+ .collect { it.getName() == "custom" ? replacementDirective : it }
+ builder.clearDirectives()
+ .additionalDirectives(new LinkedHashSet<>(nonBuiltIns))
+ })
+
+ then: "all 7 built-in directives are still present"
+ newSchema.directives.size() == 8
+ newSchema.getDirective("include") != null
+ newSchema.getDirective("skip") != null
+ newSchema.getDirective("deprecated") != null
+
+ and: "the custom directive has the updated description"
+ newSchema.getDirective("custom").description == "v2"
+ }
+
+ def "clearDirectives then adding directives gives expected ordering"() {
+ given: "a non-standard directive and a customized built-in directive"
+ def nonStandard = GraphQLDirective.newDirective()
+ .name("custom")
+ .validLocations(DirectiveLocation.FIELD)
+ .build()
+ def skipWithCustomDesc = Directives.SkipDirective.transform({ b ->
+ b.description("custom skip description")
+ })
+
+ when: "clearDirectives is called, then the non-standard directive is added, then the customized built-in is added after it"
+ def schema = basicSchemaBuilder()
+ .clearDirectives()
+ .additionalDirective(nonStandard)
+ .additionalDirective(skipWithCustomDesc)
+ .build()
+
+ then: "unoverridden built-ins come first (in BUILT_IN_DIRECTIVES order, skip excluded), then user-supplied in insertion order"
+ def names = schema.directives.collect { it.name }
+ names == ["include", "deprecated", "specifiedBy", "oneOf", "defer",
+ "experimental_disableErrorPropagation", "custom", "skip"]
+
+ and: "the customized skip directive retains its custom description"
+ schema.getDirective("skip").description == "custom skip description"
}
def "clear additional types works as expected"() {