Skip to content

Adds Validation Rules #135

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
829e6ac
fix: Adds all graphql-js validation rules
NeedleInAJayStack Nov 19, 2023
3796949
test: Adds multi-location comparison function
NeedleInAJayStack Nov 12, 2023
2bcecfe
feature: Adds NoUnusedFragmentsRule
NeedleInAJayStack Nov 6, 2023
c2b52db
feature: Adds UniqueOperationNamesRule
NeedleInAJayStack Nov 12, 2023
79c7ca0
feature: Adds LoneAnonymousOperationRule
NeedleInAJayStack Nov 12, 2023
787b92d
fix: suggestionList ordering is deterministic
NeedleInAJayStack Nov 12, 2023
7a73e85
feature: Adds KnownTypeNamesRule
NeedleInAJayStack Nov 12, 2023
9ab0dfe
test: Adds ComplexInput & OneOfInput to example schema
NeedleInAJayStack Nov 13, 2023
073718b
feature: Adds FragmentsOnCompositeTypesRule
NeedleInAJayStack Nov 13, 2023
e26affc
feature: Adds VariablesAreInputTypesRule
NeedleInAJayStack Nov 13, 2023
5fad1f4
feature: Adds UniqueFragmentNamesRule
NeedleInAJayStack Nov 13, 2023
c5131f6
feature: Adds KnownFragmentNamesRule
NeedleInAJayStack Nov 13, 2023
0049176
fix: visitor unskips nodes correctly
NeedleInAJayStack Nov 20, 2023
062a975
feature: Adds NoFragmentCyclesRule
NeedleInAJayStack Nov 13, 2023
14949c7
feature: Adds UniqueVariableNamesRule
NeedleInAJayStack Nov 14, 2023
2c3abcc
feature: Adds NoUndefinedVariablesRule
NeedleInAJayStack Nov 14, 2023
70ffab6
feature: Adds UniqueArgumentNamesRule
NeedleInAJayStack Nov 14, 2023
0c87951
feature: Adds UniqueInputFieldNamesRule
NeedleInAJayStack Nov 14, 2023
db782bf
fix: Standard scalars deliver correct error messages
NeedleInAJayStack Nov 18, 2023
fe4ea0d
fix: Failed parseLiteral gets null
NeedleInAJayStack Nov 18, 2023
c2888ff
fix: Scalar parsing defaults match and reject objects correctly
NeedleInAJayStack Nov 18, 2023
e13ef31
fix: TypeInfo ListValue walking bug
NeedleInAJayStack Nov 18, 2023
f7bb35a
feature: Adds ValuesOfCorrectTypeRule
NeedleInAJayStack Nov 15, 2023
07d415f
feature: Adds VariablesInAllowedPositionRule
NeedleInAJayStack Nov 18, 2023
9922b2d
fix: Adds variable directive support
NeedleInAJayStack Nov 19, 2023
f62de68
test: Adds onField directive to example schema
NeedleInAJayStack Nov 19, 2023
3fe1484
test: Adds schema injection to validation tests
NeedleInAJayStack Nov 19, 2023
958cbea
feature: Adds KnownDirectivesRule
NeedleInAJayStack Nov 19, 2023
031d6c3
feature: Adds ProvidedRequiredArgumentsRuleTests
NeedleInAJayStack Nov 19, 2023
31795b0
feature: ExecutableDefinitionsRule
NeedleInAJayStack Nov 19, 2023
801026f
feature: Adds 'isRepeatable' field to Directives
NeedleInAJayStack Nov 19, 2023
3705d06
feature: Adds UniqueDirectivesPerLocationRule
NeedleInAJayStack Nov 19, 2023
699c6ac
fix: Ensures correct visitor ordering
NeedleInAJayStack Nov 20, 2023
a4063a1
fix: Fixes unpredictable fragment path matching
NeedleInAJayStack Nov 20, 2023
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
30 changes: 27 additions & 3 deletions Sources/GraphQL/Language/AST.swift
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ public final class OperationDefinition {
}
return .array(variableDefinitions)
case "directives":
guard !variableDefinitions.isEmpty else {
guard !directives.isEmpty else {
return nil
}
return .array(directives)
Expand Down Expand Up @@ -475,12 +475,20 @@ public final class VariableDefinition {
public private(set) var variable: Variable
public private(set) var type: Type
public private(set) var defaultValue: Value?
public private(set) var directives: [Directive]

init(loc: Location? = nil, variable: Variable, type: Type, defaultValue: Value? = nil) {
init(
loc: Location? = nil,
variable: Variable,
type: Type,
defaultValue: Value? = nil,
directives: [Directive] = []
) {
self.loc = loc
self.variable = variable
self.type = type
self.defaultValue = defaultValue
self.directives = directives
}

public func get(key: String) -> NodeResult? {
Expand All @@ -491,6 +499,11 @@ public final class VariableDefinition {
return .node(type)
case "defaultValue":
return defaultValue.map { .node($0) }
case "directives":
guard !directives.isEmpty else {
return nil
}
return .array(directives)
default:
return nil
}
Expand Down Expand Up @@ -525,6 +538,14 @@ public final class VariableDefinition {
return
}
self.defaultValue = defaultValue
case "directives":
guard
case let .array(values) = value,
let directives = values as? [Directive]
else {
return
}
self.directives = directives
default:
return
}
Expand Down Expand Up @@ -1748,7 +1769,10 @@ extension OperationTypeDefinition: Equatable {
}
}

public protocol TypeDefinition: TypeSystemDefinition {}
public protocol TypeDefinition: TypeSystemDefinition {
var name: Name { get }
}

extension ScalarTypeDefinition: TypeDefinition {}
extension ObjectTypeDefinition: TypeDefinition {}
extension InterfaceTypeDefinition: TypeDefinition {}
Expand Down
3 changes: 2 additions & 1 deletion Sources/GraphQL/Language/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,8 @@ func parseVariableDefinition(lexer: Lexer) throws -> VariableDefinition {
variable: parseVariable(lexer: lexer),
type: (expect(lexer: lexer, kind: .colon), parseTypeReference(lexer: lexer)).1,
defaultValue: skip(lexer: lexer, kind: .equals) ?
parseValueLiteral(lexer: lexer, isConst: true) : nil
parseValueLiteral(lexer: lexer, isConst: true) : nil,
directives: parseDirectives(lexer: lexer)
)
}

Expand Down
3 changes: 1 addition & 2 deletions Sources/GraphQL/Language/Printer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ extension OperationDefinition: Printable {
extension VariableDefinition: Printable {
var printed: String {
variable + ": " + type.printed + wrap(" = ", defaultValue?.printed)
// + wrap(" ", join(directives, " "))
// TODO: variable directives are currently not supported
+ wrap(" ", join(directives, " "))
}
}

Expand Down
13 changes: 9 additions & 4 deletions Sources/GraphQL/Language/Visitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ let QueryDocumentKeys: [Kind: [String]] = [

.document: ["definitions"],
.operationDefinition: ["name", "variableDefinitions", "directives", "selectionSet"],
.variableDefinition: ["variable", "type", "defaultValue"],
.variableDefinition: ["variable", "type", "defaultValue", "directives"],
.variable: ["name"],
.selectionSet: ["selections"],
.field: ["alias", "name", "arguments", "directives", "selectionSet"],
Expand Down Expand Up @@ -304,9 +304,14 @@ func visitInParallel(visitors: [Visitor]) -> Visitor {
} else if case .node = result {
return result
}
} // else if case let .node(skippedNode) = skipping[i], skippedNode == node {
// skipping[i] = nil
// }
} else if
case let .node(skippedNodeValue) = skipping[i],
let skippedNode = skippedNodeValue,
skippedNode.kind == node.kind,
skippedNode.loc == node.loc
{
skipping[i] = nil
}
}

return .continue
Expand Down
7 changes: 6 additions & 1 deletion Sources/GraphQL/SwiftUtilities/SuggestionList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ func suggestionList(
}
return optionsByDistance.keys.sorted {
// Values are guaranteed non-nil since the keys come from the object itself
optionsByDistance[$0]! - optionsByDistance[$1]! != 0
let distanceDiff = optionsByDistance[$0]! - optionsByDistance[$1]!
if distanceDiff != 0 {
return distanceDiff < 0
} else {
return $0.lexicographicallyPrecedes($1)
}
}
}

Expand Down
69 changes: 36 additions & 33 deletions Sources/GraphQL/Type/Definition.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import NIO
import OrderedCollections

/**
* These are all of the possible kinds of types.
Expand Down Expand Up @@ -171,35 +172,22 @@ public final class GraphQLScalarType {
public let kind: TypeKind = .scalar

let serialize: (Any) throws -> Map
let parseValue: ((Map) throws -> Map)?
let parseLiteral: ((Value) throws -> Map)?

public init(
name: String,
description: String? = nil,
serialize: @escaping (Any) throws -> Map
) throws {
try assertValid(name: name)
self.name = name
self.description = description
self.serialize = serialize
parseValue = nil
parseLiteral = nil
}
let parseValue: (Map) throws -> Map
let parseLiteral: (Value) throws -> Map

public init(
name: String,
description: String? = nil,
serialize: @escaping (Any) throws -> Map,
parseValue: @escaping (Map) throws -> Map,
parseLiteral: @escaping (Value) throws -> Map
parseValue: ((Map) throws -> Map)? = nil,
parseLiteral: ((Value) throws -> Map)? = nil
Comment on lines -177 to +183
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the combination of these two initializer changes is that the Scalar public API is preserved.

) throws {
try assertValid(name: name)
self.name = name
self.description = description
self.serialize = serialize
self.parseValue = parseValue
self.parseLiteral = parseLiteral
self.parseValue = parseValue ?? defaultParseValue
self.parseLiteral = parseLiteral ?? defaultParseLiteral
}

// Serializes an internal value to include in a response.
Expand All @@ -209,15 +197,23 @@ public final class GraphQLScalarType {

// Parses an externally provided value to use as an input.
public func parseValue(value: Map) throws -> Map {
return try parseValue?(value) ?? Map.null
return try parseValue(value)
}

// Parses an externally provided literal value to use as an input.
public func parseLiteral(valueAST: Value) throws -> Map {
return try parseLiteral?(valueAST) ?? Map.null
return try parseLiteral(valueAST)
}
}

let defaultParseValue: ((Map) throws -> Map) = { value in
value
}

let defaultParseLiteral: ((Value) throws -> Map) = { value in
try valueFromASTUntyped(valueAST: value)
}

extension GraphQLScalarType: Encodable {
private enum CodingKeys: String, CodingKey {
case name
Expand Down Expand Up @@ -513,7 +509,7 @@ public struct GraphQLResolveInfo {
public let variableValues: [String: Any]
}

public typealias GraphQLFieldMap = [String: GraphQLField]
public typealias GraphQLFieldMap = OrderedDictionary<String, GraphQLField>

public struct GraphQLField {
public let type: GraphQLOutputType
Expand Down Expand Up @@ -573,7 +569,7 @@ public struct GraphQLField {
}
}

public typealias GraphQLFieldDefinitionMap = [String: GraphQLFieldDefinition]
public typealias GraphQLFieldDefinitionMap = OrderedDictionary<String, GraphQLFieldDefinition>

public final class GraphQLFieldDefinition {
public let name: String
Expand Down Expand Up @@ -659,7 +655,7 @@ extension GraphQLFieldDefinition: KeySubscriptable {
}
}

public typealias GraphQLArgumentConfigMap = [String: GraphQLArgument]
public typealias GraphQLArgumentConfigMap = OrderedDictionary<String, GraphQLArgument>

public struct GraphQLArgument {
public let type: GraphQLInputType
Expand Down Expand Up @@ -1018,7 +1014,7 @@ public final class GraphQLEnumType {
let mapValue = try map(from: value)
guard let enumValue = valueLookup[mapValue] else {
throw GraphQLError(
message: "Enum '\(name)' cannot represent value '\(mapValue)'."
message: "Enum \"\(name)\" cannot represent value: \(mapValue)."
)
}
return .string(enumValue.name)
Expand All @@ -1027,13 +1023,13 @@ public final class GraphQLEnumType {
public func parseValue(value: Map) throws -> Map {
guard let valueStr = value.string else {
throw GraphQLError(
message: "Enum '\(name)' cannot represent non-string value '\(value)'." +
message: "Enum \"\(name)\" cannot represent non-string value: \(value)." +
didYouMeanEnumValue(unknownValueStr: value.description)
)
}
guard let enumValue = nameLookup[valueStr] else {
throw GraphQLError(
message: "Value '\(valueStr)' does not exist in '\(name)' enum." +
message: "Value \"\(valueStr)\" does not exist in \"\(name)\" enum." +
didYouMeanEnumValue(unknownValueStr: valueStr)
)
}
Expand All @@ -1043,14 +1039,14 @@ public final class GraphQLEnumType {
public func parseLiteral(valueAST: Value) throws -> Map {
guard let enumNode = valueAST as? EnumValue else {
throw GraphQLError(
message: "Enum '\(name)' cannot represent non-enum value '\(valueAST)'." +
didYouMeanEnumValue(unknownValueStr: "\(valueAST)"),
message: "Enum \"\(name)\" cannot represent non-enum value: \(print(ast: valueAST))." +
didYouMeanEnumValue(unknownValueStr: print(ast: valueAST)),
nodes: [valueAST]
)
}
guard let enumValue = nameLookup[enumNode.value] else {
throw GraphQLError(
message: "Value '\(enumNode)' does not exist in '\(name)' enum." +
message: "Value \"\(enumNode.value)\" does not exist in \"\(name)\" enum." +
didYouMeanEnumValue(unknownValueStr: enumNode.value),
nodes: [valueAST]
)
Expand Down Expand Up @@ -1136,7 +1132,7 @@ func defineEnumValues(
return definitions
}

public typealias GraphQLEnumValueMap = [String: GraphQLEnumValue]
public typealias GraphQLEnumValueMap = OrderedDictionary<String, GraphQLEnumValue>

public struct GraphQLEnumValue {
public let value: Map
Expand Down Expand Up @@ -1317,7 +1313,7 @@ public struct InputObjectField {
}
}

public typealias InputObjectFieldMap = [String: InputObjectField]
public typealias InputObjectFieldMap = OrderedDictionary<String, InputObjectField>

public final class InputObjectFieldDefinition {
public let name: String
Expand Down Expand Up @@ -1384,7 +1380,14 @@ extension InputObjectFieldDefinition: KeySubscriptable {
}
}

public typealias InputObjectFieldDefinitionMap = [String: InputObjectFieldDefinition]
public func isRequiredInputField(_ field: InputObjectFieldDefinition) -> Bool {
return field.type is GraphQLNonNull && field.defaultValue == nil
}

public typealias InputObjectFieldDefinitionMap = OrderedDictionary<
String,
InputObjectFieldDefinition
>

/**
* List Modifier
Expand Down
8 changes: 6 additions & 2 deletions Sources/GraphQL/Type/Directives.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public enum DirectiveLocation: String, Encodable {
case fragmentDefinition = "FRAGMENT_DEFINITION"
case fragmentSpread = "FRAGMENT_SPREAD"
case inlineFragment = "INLINE_FRAGMENT"
case variableDefinition = "VARIABLE_DEFINITION"
// Schema Definitions
case schema = "SCHEMA"
case scalar = "SCALAR"
Expand All @@ -30,18 +31,21 @@ public struct GraphQLDirective: Encodable {
public let description: String
public let locations: [DirectiveLocation]
public let args: [GraphQLArgumentDefinition]
public let isRepeatable: Bool

public init(
name: String,
description: String,
description: String = "",
locations: [DirectiveLocation],
args: GraphQLArgumentConfigMap = [:]
args: GraphQLArgumentConfigMap = [:],
isRepeatable: Bool = false
) throws {
try assertValid(name: name)
self.name = name
self.description = description
self.locations = locations
self.args = try defineArgumentMap(args: args)
self.isRepeatable = isRepeatable
}
}

Expand Down
1 change: 1 addition & 0 deletions Sources/GraphQL/Type/Introspection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ let __Directive = try! GraphQLObjectType(
fields: [
"name": GraphQLField(type: GraphQLNonNull(GraphQLString)),
"description": GraphQLField(type: GraphQLString),
"isRepeatable": GraphQLField(type: GraphQLNonNull(GraphQLBoolean)),
"locations": GraphQLField(type: GraphQLNonNull(GraphQLList(GraphQLNonNull(__DirectiveLocation)))),
"args": GraphQLField(
type: GraphQLNonNull(GraphQLList(GraphQLNonNull(__InputValue))),
Expand Down
Loading