Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ready: Add @deprecated validation for input object fields, field arguments, directive arguments #3591

Merged
merged 4 commits into from
May 14, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package graphql.schema.validation;

import graphql.Directives;
import graphql.Internal;
import graphql.schema.GraphQLAppliedDirective;
import graphql.schema.GraphQLArgument;
import graphql.schema.GraphQLDirective;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLInputObjectField;
import graphql.schema.GraphQLInputObjectType;
import graphql.schema.GraphQLSchemaElement;
import graphql.schema.GraphQLTypeUtil;
import graphql.schema.GraphQLTypeVisitorStub;
import graphql.util.TraversalControl;
import graphql.util.TraverserContext;

import static java.lang.String.format;

/*
From the spec:
The @deprecated directive must not appear on required (non-null without a default) arguments
or input object field definitions.
*/
@Internal
public class DeprecatedInputObjectAndArgumentsAreValid extends GraphQLTypeVisitorStub {

@Override
public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField inputObjectField, TraverserContext<GraphQLSchemaElement> context) {
// There can only be at most one @deprecated, because it is not a repeatable directive
GraphQLAppliedDirective deprecatedDirective = inputObjectField.getAppliedDirective(Directives.DEPRECATED_DIRECTIVE_DEFINITION.getName());

if (deprecatedDirective != null && GraphQLTypeUtil.isNonNull(inputObjectField.getType()) && !inputObjectField.hasSetDefaultValue()) {
GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) context.getParentNode();
SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class);
String message = format("Required input field '%s.%s' cannot be deprecated.", inputObjectType.getName(), inputObjectField.getName());
errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.RequiredInputFieldCannotBeDeprecated, message));
}
return TraversalControl.CONTINUE;
}

// An argument can appear as either a field argument or a directive argument. This visitor will visit both field arguments and directive arguments.
// An applied directive's argument cannot be deprecated.
@Override
public TraversalControl visitGraphQLArgument(GraphQLArgument argument, TraverserContext<GraphQLSchemaElement> context) {
// There can only be at most one @deprecated, because it is not a repeatable directive
GraphQLAppliedDirective deprecatedDirective = argument.getAppliedDirective(Directives.DEPRECATED_DIRECTIVE_DEFINITION.getName());

if (deprecatedDirective != null && GraphQLTypeUtil.isNonNull(argument.getType()) && !argument.hasSetDefaultValue()) {
if (context.getParentNode() instanceof GraphQLFieldDefinition) {
GraphQLFieldDefinition fieldDefinition = (GraphQLFieldDefinition) context.getParentNode();
SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class);
String message = format("Required argument '%s' on field '%s' cannot be deprecated.", argument.getName(), fieldDefinition.getName());
errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.RequiredFieldArgumentCannotBeDeprecated, message));
} else if (context.getParentNode() instanceof GraphQLDirective) {
GraphQLDirective directive = (GraphQLDirective) context.getParentNode();
SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class);
String message = format("Required argument '%s' on directive '%s' cannot be deprecated.", argument.getName(), directive.getName());
errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.RequiredDirectiveArgumentCannotBeDeprecated, message));
}
}
return TraversalControl.CONTINUE;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ public enum SchemaValidationErrorType implements SchemaValidationErrorClassifica
OutputTypeUsedInInputTypeContext,
InputTypeUsedInOutputTypeContext,
OneOfDefaultValueOnField,
OneOfNonNullableField
OneOfNonNullableField,
RequiredInputFieldCannotBeDeprecated,
RequiredFieldArgumentCannotBeDeprecated,
RequiredDirectiveArgumentCannotBeDeprecated
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public SchemaValidator() {
rules.add(new AppliedDirectiveArgumentsAreValid());
rules.add(new InputAndOutputTypesUsedAppropriately());
rules.add(new OneOfInputObjectRules());
rules.add(new DeprecatedInputObjectAndArgumentsAreValid());
}

public List<GraphQLTypeVisitor> getRules() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
package graphql.schema.validation

import graphql.TestUtil
import spock.lang.Specification

class DeprecatedInputObjectAndArgumentsAreValidTest extends Specification {

def "required input field cannot be deprecated"() {
def sdl = '''
type Query {
pizza(name: String!): String
}

type Mutation {
updatePizza(pizzaInfo: PizzaInfo!): String
}

input PizzaInfo {
name: String!
pineapples: Boolean! @deprecated(reason: "Don't need this input field")
spicy: Boolean @deprecated(reason: "Don't need this nullable input field")
}
'''

when:
TestUtil.schema(sdl)

then:
def schemaProblem = thrown(InvalidSchemaException)
schemaProblem.getErrors().size() == 1
schemaProblem.getErrors().first().description == "Required input field 'PizzaInfo.pineapples' cannot be deprecated."
}

def "multiple required input fields cannot be deprecated"() {
def sdl = '''
type Query {
pizza(name: String!): String
}

type Mutation {
updatePizza(pizzaInfo: PizzaInfo!): String
}

input PizzaInfo {
name: String!
pineapples: Boolean! @deprecated(reason: "Don't need this input field")
spicy: Boolean! @deprecated(reason: "Don't need this input field")
}
'''

when:
TestUtil.schema(sdl)

then:
def schemaProblem = thrown(InvalidSchemaException)
schemaProblem.getErrors().size() == 2
schemaProblem.getErrors()[0].description == "Required input field 'PizzaInfo.pineapples' cannot be deprecated."
schemaProblem.getErrors()[1].description == "Required input field 'PizzaInfo.spicy' cannot be deprecated."
}

def "required input field list cannot be deprecated"() {
def sdl = '''
type Query {
pizza(name: String!): String
}

type Mutation {
updatePizza(pizzaInfos: [PizzaInfo]!): String
}

input PizzaInfo {
name: String!
pineapples: Boolean! @deprecated(reason: "Don't need this input field")
}
'''

when:
TestUtil.schema(sdl)

then:
def schemaProblem = thrown(InvalidSchemaException)
schemaProblem.getErrors().size() == 1
schemaProblem.getErrors().first().description == "Required input field 'PizzaInfo.pineapples' cannot be deprecated."
}

def "nullable input field can be deprecated"() {
def sdl = '''
type Query {
pizza(name: String!): String
}

type Mutation {
updatePizza(pizzaInfo: PizzaInfo!): String
}

input PizzaInfo {
name: String!
pineapples: Boolean @deprecated(reason: "Don't need this input field")
}
'''

when:
TestUtil.schema(sdl)

then:
noExceptionThrown()
}

def "non-nullable input field with default value can be deprecated"() {
def sdl = '''
type Query {
pizza(name: String!): String
}

type Mutation {
updatePizza(pizzaInfo: PizzaInfo!): String
}

input PizzaInfo {
name: String!
pineapples: Boolean! = false @deprecated(reason: "Don't need this input field")
}
'''

when:
TestUtil.schema(sdl)

then:
noExceptionThrown()
}

def "required field argument cannot be deprecated"() {
def sdl = '''
type Query {
pizza(name: String!): String
}

type Mutation {
updatePizza(name: String!, pineapples: Boolean! @deprecated(reason: "Don't need this field argument")): String
}
'''

when:
TestUtil.schema(sdl)

then:
def schemaProblem = thrown(InvalidSchemaException)
schemaProblem.getErrors().size() == 1
schemaProblem.getErrors().first().description == "Required argument 'pineapples' on field 'updatePizza' cannot be deprecated."
}

def "multiple required field arguments cannot be deprecated"() {
def sdl = '''
type Query {
pizza(name: String!): String
}

type Mutation {
updatePizza(name: String! @deprecated(reason: "yeah nah"), pineapples: Boolean! @deprecated(reason: "Don't need this field argument")): String
}
'''

when:
TestUtil.schema(sdl)

then:
def schemaProblem = thrown(InvalidSchemaException)
schemaProblem.getErrors().size() == 2
schemaProblem.getErrors()[0].description == "Required argument 'name' on field 'updatePizza' cannot be deprecated."
schemaProblem.getErrors()[1].description == "Required argument 'pineapples' on field 'updatePizza' cannot be deprecated."
}

def "nullable field argument can be deprecated"() {
def sdl = '''
type Query {
pizza(name: String!): String
}

type Mutation {
updatePizza(name: String!, pineapples: Boolean @deprecated(reason: "Don't need this field argument")): String
}
'''

when:
TestUtil.schema(sdl)

then:
noExceptionThrown()
}

def "non-nullable field argument with default value can be deprecated"() {
def sdl = '''
type Query {
pizza(name: String!): String
}

type Mutation {
updatePizza(name: String!, pineapples: Boolean! = false @deprecated(reason: "Don't need this field argument")): String
}
'''

when:
TestUtil.schema(sdl)

then:
noExceptionThrown()
}

def "required directive argument cannot be deprecated"() {
def sdl = '''
directive @pizzaDirective(name: String!, likesPineapples: Boolean! @deprecated(reason: "Don't need this directive argument")) on FIELD_DEFINITION

type Query {
pizza(name: String!): String @pizzaDirective(name: "Stefano", likesPineapples: false)
}

type Mutation {
updatePizza(name: String!, pineapples: Boolean!): String
}
'''

when:
TestUtil.schema(sdl)

then:
def schemaProblem = thrown(InvalidSchemaException)
schemaProblem.getErrors().size() == 1
schemaProblem.getErrors().first().description == "Required argument 'likesPineapples' on directive 'pizzaDirective' cannot be deprecated."
}

def "multiple required directive arguments cannot be deprecated"() {
def sdl = '''
directive @pizzaDirective(name: String! @deprecated, likesPineapples: Boolean! @deprecated(reason: "Don't need this directive argument")) on FIELD_DEFINITION

type Query {
pizza(name: String!): String @pizzaDirective(name: "Stefano", likesPineapples: false)
}

type Mutation {
updatePizza(name: String!, pineapples: Boolean!): String
}
'''

when:
TestUtil.schema(sdl)

then:
def schemaProblem = thrown(InvalidSchemaException)
schemaProblem.getErrors().size() == 2
schemaProblem.getErrors()[0].description == "Required argument 'name' on directive 'pizzaDirective' cannot be deprecated."
schemaProblem.getErrors()[1].description == "Required argument 'likesPineapples' on directive 'pizzaDirective' cannot be deprecated."
}

def "nullable directive argument can be deprecated"() {
def sdl = '''
directive @pizzaDirective(name: String!, likesPineapples: Boolean @deprecated(reason: "Don't need this directive argument")) on FIELD_DEFINITION

type Query {
pizza(name: String!): String @pizzaDirective(name: "Stefano", likesPineapples: false)
}

type Mutation {
updatePizza(name: String!, pineapples: Boolean!): String
}

'''

when:
TestUtil.schema(sdl)

then:
noExceptionThrown()
}

def "non-nullable directive argument with default value can be deprecated"() {
def sdl = '''
directive @pizzaDirective(name: String!, likesPineapples: Boolean! = false @deprecated(reason: "Don't need this directive argument")) on FIELD_DEFINITION

type Query {
pizza(name: String!): String @pizzaDirective(name: "Stefano", likesPineapples: false)
}

type Mutation {
updatePizza(name: String!, pineapples: Boolean!): String
}

'''

when:
TestUtil.schema(sdl)

then:
noExceptionThrown()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class SchemaValidatorTest extends Specification {
def validator = new SchemaValidator()
def rules = validator.rules
then:
rules.size() == 8
rules.size() == 9
rules[0] instanceof NoUnbrokenInputCycles
rules[1] instanceof TypesImplementInterfaces
rules[2] instanceof TypeAndFieldRule
Expand All @@ -20,5 +20,6 @@ class SchemaValidatorTest extends Specification {
rules[5] instanceof AppliedDirectiveArgumentsAreValid
rules[6] instanceof InputAndOutputTypesUsedAppropriately
rules[7] instanceof OneOfInputObjectRules
rules[8] instanceof DeprecatedInputObjectAndArgumentsAreValid
}
}
Loading