Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 38 additions & 13 deletions src/main/java/graphql/schema/idl/SchemaPrinter.java
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,7 @@ public String directivesString(Class<? extends GraphQLSchemaElement> parentType,
String directivesString(Class<? extends GraphQLSchemaElement> parentType, boolean isDeprecated, GraphQLDirectiveContainer directiveContainer) {
List<GraphQLAppliedDirective> directives;
if (isDeprecated) {
directives = addDeprecatedDirectiveIfNeeded(directiveContainer);
directives = addOrUpdateDeprecatedDirectiveIfNeeded(directiveContainer);
} else {
directives = DirectivesUtil.toAppliedDirectives(directiveContainer);
}
Expand Down Expand Up @@ -1041,25 +1041,50 @@ private boolean hasDeprecatedDirective(List<GraphQLAppliedDirective> directives)
.count() == 1;
}

private List<GraphQLAppliedDirective> addDeprecatedDirectiveIfNeeded(GraphQLDirectiveContainer directiveContainer) {
private List<GraphQLAppliedDirective> addOrUpdateDeprecatedDirectiveIfNeeded(GraphQLDirectiveContainer directiveContainer) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reorganising because the method was getting too long

List<GraphQLAppliedDirective> directives = DirectivesUtil.toAppliedDirectives(directiveContainer);
String reason = getDeprecationReason(directiveContainer);

if (!hasDeprecatedDirective(directives) && isDeprecatedDirectiveAllowed()) {
directives = new ArrayList<>(directives);
String reason = getDeprecationReason(directiveContainer);
GraphQLAppliedDirectiveArgument arg = GraphQLAppliedDirectiveArgument.newArgument()
.name("reason")
.valueProgrammatic(reason)
.type(GraphQLString)
.build();
GraphQLAppliedDirective directive = GraphQLAppliedDirective.newDirective()
.name("deprecated")
.argument(arg)
.build();
directives.add(directive);
directives.add(createDeprecatedDirective(reason));
} else if (hasDeprecatedDirective(directives) && isDeprecatedDirectiveAllowed()) {
// Update deprecated reason in case modified by schema transform
directives = updateDeprecatedDirective(directives, reason);
}
return directives;
}

private GraphQLAppliedDirective createDeprecatedDirective(String reason) {
GraphQLAppliedDirectiveArgument arg = GraphQLAppliedDirectiveArgument.newArgument()
.name("reason")
.valueProgrammatic(reason)
.type(GraphQLString)
.build();
return GraphQLAppliedDirective.newDirective()
.name("deprecated")
.argument(arg)
.build();
}

private List<GraphQLAppliedDirective> updateDeprecatedDirective(List<GraphQLAppliedDirective> directives, String reason) {
GraphQLAppliedDirectiveArgument newArg = GraphQLAppliedDirectiveArgument.newArgument()
.name("reason")
.valueProgrammatic(reason)
.type(GraphQLString)
.build();

return directives.stream().map(d -> {
if (isDeprecatedDirective(d)) {
// Don't include reason is deliberately replaced with NOT_SET, for example in Anonymizer
if (d.getArgument("reason").getArgumentValue() != InputValueWithState.NOT_SET) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very special case. The Anonymizer wipes out deprecated reasons with a special NOT_SET value, which is distinct from null. If this is spotted, we won't add back the deprecated reason.

return d.transform(builder -> builder.argument(newArg));
Copy link
Member Author

@dondonz dondonz Jan 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a subtle change in behaviour: it means the field's reason always takes precedence over the deprecated directive's reason

Do we want this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we do want this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SchemaGenerator (SDL) will generate the field def deprecation reason by default from the directives

graphql.schema.idl.SchemaGeneratorHelper#buildField

        builder.deprecate(buildDeprecationReason(fieldDef.getDirectives()));
....
    String buildDeprecationReason(List<Directive> directives) {
        directives = Optional.ofNullable(directives).orElse(emptyList());
        Optional<Directive> directive = directives.stream().filter(d -> "deprecated".equals(d.getName())).findFirst();
        if (directive.isPresent()) {
            Map<String, String> args = directive.get().getArguments().stream().collect(toMap(
                    Argument::getName, arg -> ((StringValue) arg.getValue()).getValue()
            ));
            if (args.isEmpty()) {
                return NO_LONGER_SUPPORTED; // default value from spec
            } else {
                // pre flight checks have ensured it's valid
                return args.get("reason");
            }
        }
        return null;
    }

So the graphql.schema.GraphQLFieldDefinition#getDeprecationReason is the most important part of the deprecation bit.

}
}
return d;
}).collect(toList());
}

private String getDeprecationReason(GraphQLDirectiveContainer directiveContainer) {
if (directiveContainer instanceof GraphQLFieldDefinition) {
GraphQLFieldDefinition type = (GraphQLFieldDefinition) directiveContainer;
Expand Down
63 changes: 63 additions & 0 deletions src/test/groovy/graphql/schema/SchemaTransformerTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -959,4 +959,67 @@ type Query {
visitedSchema == schema
visitedCodeRegistry instanceof GraphQLCodeRegistry.Builder
}

def "deprecation transformation correctly overrides existing deprecated directive reasons"() {
def schema = TestUtil.schema("""
schema {
query: QueryType
}

type QueryType {
a: String
b: String @deprecated(reason: "Replace this doc")
}

interface InterfaceType {
a: String
b: String @deprecated(reason: "Replace this doc")
}

input InputType {
a: String
b: String @deprecated(reason: "Replace this doc")
}
""")

when:
def typeVisitor = new GraphQLTypeVisitorStub() {
@Override
TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition node, TraverserContext<GraphQLSchemaElement> context) {
def n = node.transform(b -> b.deprecate("NEW REASON"));
return changeNode(context, n);
}

@Override
TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField node, TraverserContext<GraphQLSchemaElement> context) {
def n = node.transform(b -> b.deprecate("NEW REASON"));
return changeNode(context, n);
}
}
def newSchema = SchemaTransformer.transformSchema(schema, typeVisitor)

then:
def newQueryType = newSchema.getObjectType("QueryType")
def newQueryTypePrinted = new SchemaPrinter().print(newQueryType)

newQueryTypePrinted == """type QueryType {
a: String @deprecated(reason : "NEW REASON")
b: String @deprecated(reason : "NEW REASON")
}
"""
def newInterfaceType = newSchema.getType("InterfaceType")
def newInterfaceTypePrinted = new SchemaPrinter().print(newInterfaceType)
newInterfaceTypePrinted == """interface InterfaceType {
a: String @deprecated(reason : "NEW REASON")
b: String @deprecated(reason : "NEW REASON")
}
"""
def newInputType = newSchema.getType("InputType")
def newInputTypePrinted = new SchemaPrinter().print(newInputType)
newInputTypePrinted == """input InputType {
a: String @deprecated(reason : "NEW REASON")
b: String @deprecated(reason : "NEW REASON")
}
"""
}
}
Loading