Skip to content

Variable and argument null vs undefined #90

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

6 changes: 3 additions & 3 deletions Sources/GraphQL/Execution/Execute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ func shouldIncludeNode(exeContext: ExecutionContext, directives: [Directive] = [
let skip = try getArgumentValues(
argDefs: GraphQLSkipDirective.args,
argASTs: skipAST.arguments,
variableValues: exeContext.variableValues
variables: exeContext.variableValues
)

if skip["if"] == .bool(true) {
Expand All @@ -606,7 +606,7 @@ func shouldIncludeNode(exeContext: ExecutionContext, directives: [Directive] = [
let include = try getArgumentValues(
argDefs: GraphQLIncludeDirective.args,
argASTs: includeAST.arguments,
variableValues: exeContext.variableValues
variables: exeContext.variableValues
)

if include["if"] == .bool(false) {
Expand Down Expand Up @@ -685,7 +685,7 @@ public func resolveField(
let args = try getArgumentValues(
argDefs: fieldDef.args,
argASTs: fieldAST.arguments,
variableValues: exeContext.variableValues
variables: exeContext.variableValues
)

// The resolve func's optional third argument is a context value that
Expand Down
185 changes: 93 additions & 92 deletions Sources/GraphQL/Execution/Values.swift
Original file line number Diff line number Diff line change
@@ -1,58 +1,76 @@
import Foundation
import OrderedCollections

/**
* Prepares an object map of variableValues of the correct type based on the
* provided variable definitions and arbitrary input. If the input cannot be
* parsed to match the variable definitions, a GraphQLError will be thrown.
*/
func getVariableValues(schema: GraphQLSchema, definitionASTs: [VariableDefinition], inputs: [String: Map]) throws -> [String: Map] {
return try definitionASTs.reduce([:]) { values, defAST in
var valuesCopy = values

var vars = [String: Map]()
for defAST in definitionASTs {
let varName = defAST.variable.name.value

valuesCopy[varName] = try getVariableValue(

let input: Map
if let nonNilInput = inputs[varName] {
input = nonNilInput
} else {
// If variable is not in inputs it is undefined
input = .undefined
}
vars[varName] = try getVariableValue(
schema: schema,
definitionAST: defAST,
input: inputs[varName] ?? .null
input: input
)

return valuesCopy
}
return vars
}


/**
* Prepares an object map of argument values given a list of argument
* definitions and list of argument AST nodes.
*/
func getArgumentValues(argDefs: [GraphQLArgumentDefinition], argASTs: [Argument]?, variableValues: [String: Map] = [:]) throws -> Map {
func getArgumentValues(argDefs: [GraphQLArgumentDefinition], argASTs: [Argument]?, variables: [String: Map] = [:]) throws -> Map {
guard let argASTs = argASTs else {
return [:]
}

let argASTMap = argASTs.keyMap({ $0.name.value })

return try argDefs.reduce([:]) { result, argDef in
var result = result
let name = argDef.name
let argAST = argASTMap[name]
var args = OrderedDictionary<String, Map>()
for argDef in argDefs {
let argName = argDef.name
let argValue: Map

if let argAST = argAST {
let valueAST = argAST.value

let value = try valueFromAST(
valueAST: valueAST,
if let argAST = argASTMap[argName] {
argValue = try valueFromAST(
valueAST: argAST.value,
type: argDef.type,
variables: variableValues
variables: variables
)

result[name] = value
} else {
result[name] = .null
// If AST doesn't contain field, it is undefined
if let defaultValue = argDef.defaultValue {
argValue = defaultValue
} else {
argValue = .undefined
}
}

return result

let errors = try validate(value: argValue, forType: argDef.type)
guard errors.isEmpty else {
let message = "\n" + errors.joined(separator: "\n")
throw GraphQLError(
message:
"Argument \"\(argName)\" got invalid value \(argValue).\(message)" // TODO: "\(JSON.stringify(input)).\(message)",
)
}
args[argName] = argValue
}
return .dictionary(args)
}


Expand All @@ -64,112 +82,95 @@ func getVariableValue(schema: GraphQLSchema, definitionAST: VariableDefinition,
let type = typeFromAST(schema: schema, inputTypeAST: definitionAST.type)
let variable = definitionAST.variable

if type == nil || !isInputType(type: type) {
guard let inputType = type as? GraphQLInputType else {
throw GraphQLError(
message:
"Variable \"$\(variable.name.value)\" expected value of type " +
"\"\(definitionAST.type)\" which cannot be used as an input type.",
nodes: [definitionAST]
)
}

let inputType = type as! GraphQLInputType
let errors = try isValidValue(value: input, type: inputType)

if errors.isEmpty {
if input == .null {
if let defaultValue = definitionAST.defaultValue {
return try valueFromAST(valueAST: defaultValue, type: inputType)
}
else if !(inputType is GraphQLNonNull) {
return .null
}
}

return try coerceValue(type: inputType, value: input)!

var toCoerce = input
if input == .undefined, let defaultValue = definitionAST.defaultValue {
toCoerce = try valueFromAST(valueAST: defaultValue, type: inputType)
}

guard input != .null else {

let errors = try validate(value: toCoerce, forType: inputType)
guard errors.isEmpty else {
let message = !errors.isEmpty ? "\n" + errors.joined(separator: "\n") : ""
throw GraphQLError(
message:
"Variable \"$\(variable.name.value)\" of required type " +
"\"\(definitionAST.type)\" was not provided.",
"Variable \"$\(variable.name.value)\" got invalid value \"\(toCoerce)\".\(message)", // TODO: "\(JSON.stringify(input)).\(message)",
nodes: [definitionAST]
)
}

let message = !errors.isEmpty ? "\n" + errors.joined(separator: "\n") : ""

throw GraphQLError(
message:
"Variable \"$\(variable.name.value)\" got invalid value " +
"\(input).\(message)", // TODO: "\(JSON.stringify(input)).\(message)",
nodes: [definitionAST]
)

return try coerceValue(value: toCoerce, type: inputType)
}

/**
* Given a type and any value, return a runtime value coerced to match the type.
*/
func coerceValue(type: GraphQLInputType, value: Map) throws -> Map? {
func coerceValue(value: Map, type: GraphQLInputType) throws -> Map {
if let nonNull = type as? GraphQLNonNull {
// Note: we're not checking that the result of coerceValue is non-null.
// We only call this function after calling isValidValue.
return try coerceValue(type: nonNull.ofType as! GraphQLInputType, value: value)!
// We only call this function after calling validate.
guard let nonNullType = nonNull.ofType as? GraphQLInputType else {
throw GraphQLError(message: "NonNull must wrap an input type")
}
return try coerceValue(value: value, type: nonNullType)
}

guard value != .null else {
return nil
guard value != .undefined && value != .null else {
return value
}

if let list = type as? GraphQLList {
let itemType = list.ofType
guard let itemType = list.ofType as? GraphQLInputType else {
throw GraphQLError(message: "Input list must wrap an input type")
}

if case .array(let value) = value {
var coercedValues: [Map] = []

for item in value {
coercedValues.append(try coerceValue(type: itemType as! GraphQLInputType, value: item)!)
let coercedValues = try value.map { item in
try coerceValue(value: item, type: itemType)
}

return .array(coercedValues)
}

return .array([try coerceValue(type: itemType as! GraphQLInputType, value: value)!])

// Convert solitary value into single-value array
return .array([try coerceValue(value: value, type: itemType)])
}

if let type = type as? GraphQLInputObjectType {
if let objectType = type as? GraphQLInputObjectType {
guard case .dictionary(let value) = value else {
return nil
throw GraphQLError(message: "Must be dictionary to extract to an input type")
}

let fields = type.fields

return try .dictionary(fields.keys.reduce([:]) { obj, fieldName in
var objCopy = obj
let field = fields[fieldName]

var fieldValue = try coerceValue(type: field!.type, value: value[fieldName] ?? .null)

if fieldValue == .null {
fieldValue = field.flatMap({ $0.defaultValue })
let fields = objectType.fields

var object = OrderedDictionary<String, Map>()
for (fieldName, field) in fields {
if let fieldValueMap = value[fieldName], fieldValueMap != .undefined {
object[fieldName] = try coerceValue(
value: fieldValueMap,
type: field.type
)
} else {
objCopy[fieldName] = fieldValue
// If AST doesn't contain field, it is undefined
if let defaultValue = field.defaultValue {
object[fieldName] = defaultValue
} else {
object[fieldName] = .undefined
}
}

return objCopy
})
}
return .dictionary(object)
}

guard let type = type as? GraphQLLeafType else {
throw GraphQLError(message: "Must be input type")
if let leafType = type as? GraphQLLeafType {
return try leafType.parseValue(value: value)
}

let parsed = try type.parseValue(value: value)

guard parsed != .null else {
return nil
}

return parsed
throw GraphQLError(message: "Provided type is not an input type")
}
2 changes: 2 additions & 0 deletions Sources/GraphQL/Map/Map.swift
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,8 @@ extension Map : Equatable {}

public func == (lhs: Map, rhs: Map) -> Bool {
switch (lhs, rhs) {
case (.undefined, .undefined):
return true
case (.null, .null):
return true
case let (.bool(l), .bool(r)) where l == r:
Expand Down
2 changes: 1 addition & 1 deletion Sources/GraphQL/Subscription/Subscribe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ func executeSubscription(

// Build a map of arguments from the field.arguments AST, using the
// variables scope to fulfill any variable references.
let args = try getArgumentValues(argDefs: fieldDef.args, argASTs: fieldNode.arguments, variableValues: context.variableValues)
let args = try getArgumentValues(argDefs: fieldDef.args, argASTs: fieldNode.arguments, variables: context.variableValues)

// The resolve function's optional third argument is a context value that
// is provided to every resolve function within an execution. It is commonly
Expand Down
Loading