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"() {