Skip to content
Open
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
650 changes: 650 additions & 0 deletions argument_coercion_test.go

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,13 +498,13 @@ func shouldIncludeNode(eCtx *executionContext, directives []*ast.Directive) bool
}
// precedence: skipAST > includeAST
if skipAST != nil {
argValues = getArgumentValues(SkipDirective.Args, skipAST.Arguments, eCtx.VariableValues)
argValues = getArgumentValues(SkipDirective.Args, skipAST.Arguments, eCtx.VariableValues, eCtx.Schema.specCompliantArgumentCoercion)
if skipIf, ok := argValues["if"].(bool); ok && skipIf {
return false // excluded selectionSet's fields
}
}
if includeAST != nil {
argValues = getArgumentValues(IncludeDirective.Args, includeAST.Arguments, eCtx.VariableValues)
argValues = getArgumentValues(IncludeDirective.Args, includeAST.Arguments, eCtx.VariableValues, eCtx.Schema.specCompliantArgumentCoercion)
if includeIf, ok := argValues["if"].(bool); ok && !includeIf {
return false // excluded selectionSet's fields
}
Expand Down Expand Up @@ -624,7 +624,7 @@ func resolveField(eCtx *executionContext, parentType *Object, source interface{}
// Build a map of arguments from the field.arguments AST, using the
// variables scope to fulfill any variable references.
// TODO: find a way to memoize, in case this field is within a List type.
args := getArgumentValues(fieldDef.Args, fieldAST.Arguments, eCtx.VariableValues)
args := getArgumentValues(fieldDef.Args, fieldAST.Arguments, eCtx.VariableValues, eCtx.Schema.specCompliantArgumentCoercion)

info := ResolveInfo{
FieldName: fieldName,
Expand Down
4 changes: 2 additions & 2 deletions rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -1271,7 +1271,7 @@ func ProvidedNonNullArgumentsRule(context *ValidationContext) *ValidationRuleIns
for _, argDef := range fieldDef.Args {
argAST, _ := argASTMap[argDef.Name()]
if argAST == nil {
if argDefType, ok := argDef.Type.(*NonNull); ok {
if argDefType, ok := argDef.Type.(*NonNull); ok && argDef.DefaultValue == nil {
fieldName := ""
if fieldAST.Name != nil {
fieldName = fieldAST.Name.Value
Expand Down Expand Up @@ -1312,7 +1312,7 @@ func ProvidedNonNullArgumentsRule(context *ValidationContext) *ValidationRuleIns
for _, argDef := range directiveDef.Args {
argAST, _ := argASTMap[argDef.Name()]
if argAST == nil {
if argDefType, ok := argDef.Type.(*NonNull); ok {
if argDefType, ok := argDef.Type.(*NonNull); ok && argDef.DefaultValue == nil {
directiveName := ""
if directiveAST.Name != nil {
directiveName = directiveAST.Name.Value
Expand Down
93 changes: 93 additions & 0 deletions rules_provided_non_null_arguments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,96 @@ func TestValidate_ProvidedNonNullArguments_DirectiveArguments_WithDirectiveWithM
testutil.RuleError(`Directive "@skip" argument "if" of type "Boolean!" is required but not provided.`, 4, 18),
})
}

// Spec §5.4.2.1: "An argument is required if the argument type is non-null and
// does not have a default value. Otherwise, the argument is optional."
// See graphql-go/graphql#739.
func TestValidate_ProvidedNonNullArguments_FieldArguments_NoErrorOnNonNullArgumentWithDefaultValue(t *testing.T) {
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"fieldWithDefault": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"arg": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.Boolean),
DefaultValue: true,
},
},
},
},
}),
})
if err != nil {
t.Fatalf("Unexpected error, got: %v", err)
}
testutil.ExpectPassesRuleWithSchema(t, &schema, graphql.ProvidedNonNullArgumentsRule, `
{
fieldWithDefault
}
`)
}

func TestValidate_ProvidedNonNullArguments_FieldArguments_StillErrorsOnNonNullArgumentWithoutDefaultValue(t *testing.T) {
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"fieldWithoutDefault": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"arg": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.Boolean),
},
},
},
},
}),
})
if err != nil {
t.Fatalf("Unexpected error, got: %v", err)
}
testutil.ExpectFailsRuleWithSchema(t, &schema, graphql.ProvidedNonNullArgumentsRule, `
{
fieldWithoutDefault
}
`, []gqlerrors.FormattedError{
testutil.RuleError(`Field "fieldWithoutDefault" argument "arg" of type "Boolean!" is required but not provided.`, 3, 11),
})
}

func TestValidate_ProvidedNonNullArguments_DirectiveArguments_NoErrorOnNonNullArgumentWithDefaultValue(t *testing.T) {
deferDirective := graphql.NewDirective(graphql.DirectiveConfig{
Name: "defer",
Locations: []string{
graphql.DirectiveLocationFragmentSpread,
graphql.DirectiveLocationInlineFragment,
},
Args: graphql.FieldConfigArgument{
"if": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.Boolean),
DefaultValue: true,
},
},
})
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"a": &graphql.Field{Type: graphql.String},
},
}),
Directives: []*graphql.Directive{deferDirective},
})
if err != nil {
t.Fatalf("Unexpected error, got: %v", err)
}
testutil.ExpectPassesRuleWithSchema(t, &schema, graphql.ProvidedNonNullArgumentsRule, `
{
... on Query @defer {
a
}
}
`)
}
26 changes: 26 additions & 0 deletions schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ type SchemaConfig struct {
Types []Type
Directives []*Directive
Extensions []Extension

// SpecCompliantArgumentCoercion opts this schema into the argument and
// variable coercion rules described by the GraphQL specification
// (CoerceArgumentValues §6.4.1, CoerceVariableValues §6.1.2 and input
// object coercion §3.10):
//
// - A variable the caller did not supply leaves its argument absent from
// ResolveParams.Args instead of materialising it as nil, so a resolver
// can tell "not provided" from "explicitly null".
// - A default value applies only when no value was supplied. An explicit
// null stays null instead of falling back to the default.
//
// It is opt-in because both rules change what resolvers observe: code
// written against the previous behaviour may rely on every declared
// argument being present, or on an explicit null being replaced by the
// default. Leaving this false keeps that behaviour byte-for-byte.
SpecCompliantArgumentCoercion bool
}

type TypeMap map[string]Type
Expand Down Expand Up @@ -43,6 +60,8 @@ type Schema struct {
implementations map[string][]*Object
possibleTypeMap map[string]map[string]bool
extensions []Extension

specCompliantArgumentCoercion bool
}

func NewSchema(config SchemaConfig) (Schema, error) {
Expand All @@ -65,6 +84,7 @@ func NewSchema(config SchemaConfig) (Schema, error) {
schema.queryType = config.Query
schema.mutationType = config.Mutation
schema.subscriptionType = config.Subscription
schema.specCompliantArgumentCoercion = config.SpecCompliantArgumentCoercion

// Provide specified directives (e.g. @include and @skip) by default.
schema.directives = config.Directives
Expand Down Expand Up @@ -210,6 +230,12 @@ func (gq *Schema) SubscriptionType() *Object {
return gq.subscriptionType
}

// SpecCompliantArgumentCoercion reports whether this schema coerces arguments
// and variables by the specification's rules. See SchemaConfig for details.
func (gq *Schema) SpecCompliantArgumentCoercion() bool {
return gq.specCompliantArgumentCoercion
}

func (gq *Schema) Directives() []*Directive {
return gq.directives
}
Expand Down
2 changes: 1 addition & 1 deletion subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func ExecuteSubscription(p ExecuteParams) chan *Result {
Key: responseName,
}

args := getArgumentValues(fieldDef.Args, fieldNode.Arguments, exeContext.VariableValues)
args := getArgumentValues(fieldDef.Args, fieldNode.Arguments, exeContext.VariableValues, exeContext.Schema.specCompliantArgumentCoercion)
info := ResolveInfo{
FieldName: fieldName,
FieldASTs: fieldNodes,
Expand Down
Loading
Loading