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

Skip to content

Conversation

@dondonz
Copy link
Member

@dondonz dondonz commented Jan 5, 2025

Fixes #3786

Background

With the schema transformer, it's possible to update the deprecated reason stored on a field definition.

@t2gran reported that the schema printer wasn't printing out the correct deprecated reason when it had been changed by a schema transformer. See #3786 and test in this gist: https://gist.github.com/t2gran/69b149357477870ba25e00375a2626a2. The bug was happening on fields in object types, interface types, and input types. I've recycled the gist object type test and added interface and input type cases.

The root cause was that the schema printer didn't re-check deprecated reason arguments if the deprecated directive already existed. Previously it (incorrectly) assumed that if the deprecated directive already existed, then the reason was fine. This PR fixes that.

I got nerd sniped 😄

@dondonz dondonz added this to the 23.x breaking changes milestone Jan 5, 2025
}

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

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.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 5, 2025

Test Results

  306 files    306 suites   46s ⏱️
3 483 tests 3 478 ✅ 5 💤 0 ❌
3 572 runs  3 567 ✅ 5 💤 0 ❌

Results for commit a54f7b9.

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) {
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.

@bbakerman
Copy link
Member

ps in theory I think we still have a bug in introspection.

        register(__Field, "deprecationReason", GraphQLFieldDefinition.class, GraphQLFieldDefinition::getDeprecationReason);

imagine this

  • some one builds a schema via SDL
  • introspection works
  • they then change the "directive itself" to a new reason string or remove the directive
  • introspection will not work

This is because GraphQLFieldDefinition::getDeprecationReason only reads the string and not the presence of the directive itself and the string value is only set once during "SDL generation"

But this PR does not have to address this. Its pretty edge case

@dondonz
Copy link
Member Author

dondonz commented Jan 6, 2025

they then change the "directive itself" to a new reason string or remove the directive

Yes agreed it's tricky because there's two places which both "seem" like the source of truth. It's not possible to guess which one is authoritative, for example imagine a wacky case where both the directive's reason and the field definition's reason change. I'd rather make one authoritative and always choose the field definition's reason rather than the directive version.

Thanks for the speedy review!

@dondonz dondonz merged commit 9b4739e into master Jan 6, 2025
2 checks passed
@dondonz dondonz deleted the schema-transformer-deprecated-bug branch January 6, 2025 22:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The deprecated reson is NOT updated using the SchemaTransformer

3 participants