Skip to content

Commit

Permalink
Add validation for directive and field arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
dondonz committed May 12, 2024
1 parent b177520 commit 07dd807
Show file tree
Hide file tree
Showing 2 changed files with 245 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

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;
Expand All @@ -28,10 +31,33 @@ public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField inp
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());
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("deprecated");

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 @@ -18,6 +18,7 @@ class DeprecatedInputObjectAndArgumentsAreValidTest extends Specification {
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")
}
'''

Expand All @@ -27,7 +28,59 @@ class DeprecatedInputObjectAndArgumentsAreValidTest extends Specification {
then:
def schemaProblem = thrown(InvalidSchemaException)
schemaProblem.getErrors().size() == 1
schemaProblem.getErrors().first().description == "Required input field PizzaInfo.pineapples cannot be deprecated."
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"() {
Expand Down Expand Up @@ -76,4 +129,168 @@ class DeprecatedInputObjectAndArgumentsAreValidTest extends Specification {
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: true)
}
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: true)
}
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: true)
}
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: true)
}
type Mutation {
updatePizza(name: String!, pineapples: Boolean!): String
}
'''

when:
TestUtil.schema(sdl)

then:
noExceptionThrown()
}

}

0 comments on commit 07dd807

Please sign in to comment.