diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 0fe90d7..2302670 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -414,7 +414,8 @@ func defineArgumentMap(args: GraphQLArgumentConfigMap) throws -> [GraphQLArgumen name: name, type: config.type, defaultValue: config.defaultValue, - description: config.description + description: config.description, + deprecationReason: config.deprecationReason ) arguments.append(argument) } @@ -661,15 +662,18 @@ public struct GraphQLArgument { public let type: GraphQLInputType public let description: String? public let defaultValue: Map? + public let deprecationReason: String? public init( type: GraphQLInputType, description: String? = nil, - defaultValue: Map? = nil + defaultValue: Map? = nil, + deprecationReason: String? = nil ) { self.type = type self.description = description self.defaultValue = defaultValue + self.deprecationReason = deprecationReason } } @@ -678,17 +682,20 @@ public struct GraphQLArgumentDefinition { public let type: GraphQLInputType public let defaultValue: Map? public let description: String? + public let deprecationReason: String? init( name: String, type: GraphQLInputType, defaultValue: Map? = nil, - description: String? = nil + description: String? = nil, + deprecationReason: String? = nil ) { self.name = name self.type = type self.defaultValue = defaultValue self.description = description + self.deprecationReason = deprecationReason } } @@ -702,6 +709,7 @@ extension GraphQLArgumentDefinition: Encodable { case description case type case defaultValue + case deprecationReason } public func encode(to encoder: Encoder) throws { @@ -710,6 +718,7 @@ extension GraphQLArgumentDefinition: Encodable { try container.encode(description, forKey: .description) try container.encode(AnyEncodable(type), forKey: .type) try container.encode(defaultValue, forKey: .defaultValue) + try container.encode(deprecationReason, forKey: .deprecationReason) } } @@ -724,6 +733,8 @@ extension GraphQLArgumentDefinition: KeySubscriptable { return type case CodingKeys.defaultValue.rawValue: return defaultValue + case CodingKeys.deprecationReason.rawValue: + return deprecationReason default: return nil } @@ -1292,7 +1303,8 @@ func defineInputObjectFieldMap( name: name, type: field.type, description: field.description, - defaultValue: field.defaultValue + defaultValue: field.defaultValue, + deprecationReason: field.deprecationReason ) definitionMap[name] = definition @@ -1305,11 +1317,18 @@ public struct InputObjectField { public let type: GraphQLInputType public let defaultValue: Map? public let description: String? + public let deprecationReason: String? - public init(type: GraphQLInputType, defaultValue: Map? = nil, description: String? = nil) { + public init( + type: GraphQLInputType, + defaultValue: Map? = nil, + description: String? = nil, + deprecationReason: String? = nil + ) { self.type = type self.defaultValue = defaultValue self.description = description + self.deprecationReason = deprecationReason } } @@ -1320,17 +1339,20 @@ public final class InputObjectFieldDefinition { public internal(set) var type: GraphQLInputType public let description: String? public let defaultValue: Map? + public let deprecationReason: String? init( name: String, type: GraphQLInputType, description: String? = nil, - defaultValue: Map? = nil + defaultValue: Map? = nil, + deprecationReason: String? = nil ) { self.name = name self.type = type self.description = description self.defaultValue = defaultValue + self.deprecationReason = deprecationReason } func replaceTypeReferences(typeMap: TypeMap) throws { @@ -1352,6 +1374,7 @@ extension InputObjectFieldDefinition: Encodable { case description case type case defaultValue + case deprecationReason } public func encode(to encoder: Encoder) throws { @@ -1360,6 +1383,7 @@ extension InputObjectFieldDefinition: Encodable { try container.encode(description, forKey: .description) try container.encode(AnyEncodable(type), forKey: .type) try container.encode(defaultValue, forKey: .defaultValue) + try container.encode(deprecationReason, forKey: .deprecationReason) } } @@ -1374,6 +1398,8 @@ extension InputObjectFieldDefinition: KeySubscriptable { return type case CodingKeys.defaultValue.rawValue: return defaultValue + case CodingKeys.deprecationReason.rawValue: + return deprecationReason default: return nil } diff --git a/Sources/GraphQL/Type/Directives.swift b/Sources/GraphQL/Type/Directives.swift index decc901..941977a 100644 --- a/Sources/GraphQL/Type/Directives.swift +++ b/Sources/GraphQL/Type/Directives.swift @@ -105,6 +105,8 @@ public let GraphQLDeprecatedDirective = try! GraphQLDirective( "Marks an element of a GraphQL schema as no longer supported.", locations: [ .fieldDefinition, + .argumentDefinition, + .inputFieldDefinition, .enumValue, ], args: [ diff --git a/Sources/GraphQL/Type/Introspection.swift b/Sources/GraphQL/Type/Introspection.swift index 1e0efd4..2389299 100644 --- a/Sources/GraphQL/Type/Introspection.swift +++ b/Sources/GraphQL/Type/Introspection.swift @@ -476,3 +476,18 @@ let TypeNameMetaFieldDef = GraphQLFieldDefinition( eventLoopGroup.next().makeSucceededFuture(info.parentType.name) } ) + +let introspectionTypeNames = [ + __Schema.name, + __Directive.name, + __DirectiveLocation.name, + __Type.name, + __Field.name, + __InputValue.name, + __EnumValue.name, + __TypeKind.name, +] + +func isIntrospectionType(type: GraphQLNamedType) -> Bool { + return introspectionTypeNames.contains(type.name) +} diff --git a/Sources/GraphQL/Validation/Rules/Custom/NoDeprecatedCustomRule.swift b/Sources/GraphQL/Validation/Rules/Custom/NoDeprecatedCustomRule.swift new file mode 100644 index 0000000..f0e9faa --- /dev/null +++ b/Sources/GraphQL/Validation/Rules/Custom/NoDeprecatedCustomRule.swift @@ -0,0 +1,87 @@ + +/** + * No deprecated + * + * A GraphQL document is only valid if all selected fields and all used enum values have not been + * deprecated. + * + * Note: This rule is optional and is not part of the Validation section of the GraphQL + * Specification. The main purpose of this rule is detection of deprecated usages and not + * necessarily to forbid their use when querying a service. + */ +public func NoDeprecatedCustomRule(context: ValidationContext) -> Visitor { + return Visitor( + enter: { node, _, _, _, _ in + if let node = node as? Field { + if + let fieldDef = context.fieldDef, + let deprecationReason = fieldDef.deprecationReason, + let parentType = context.parentType + { + context.report( + error: GraphQLError( + message: "The field \(parentType.name).\(fieldDef.name) is deprecated. \(deprecationReason)", + nodes: [node] + ) + ) + } + } + if let node = node as? Argument { + if + let argDef = context.argument, + let deprecationReason = argDef.deprecationReason + { + if let directiveDef = context.typeInfo.directive { + context.report( + error: GraphQLError( + message: "Directive \"@\(directiveDef.name)\" argument \"\(argDef.name)\" is deprecated. \(deprecationReason)", + nodes: [node] + ) + ) + } else if + let fieldDef = context.fieldDef, + let parentType = context.parentType + { + context.report( + error: GraphQLError( + message: "Field \"\(parentType.name).\(fieldDef.name)\" argument \"\(argDef.name)\" is deprecated. \(deprecationReason)", + nodes: [node] + ) + ) + } + } + } + if let node = node as? ObjectField { + let inputObjectDef = context.parentInputType as? GraphQLInputObjectType + + if + let inputObjectDef = context.parentInputType as? GraphQLInputObjectType, + let inputFieldDef = inputObjectDef.fields[node.name.value], + let deprecationReason = inputFieldDef.deprecationReason + { + context.report( + error: GraphQLError( + message: "The input field \(inputObjectDef.name).\(inputFieldDef.name) is deprecated. \(deprecationReason)", + nodes: [node] + ) + ) + } + } + if let node = node as? EnumValue { + if + let enumValueDef = context.typeInfo.enumValue, + let deprecationReason = enumValueDef.deprecationReason, + let enumTypeDef = getNamedType(type: context.inputType) + { + context.report( + error: GraphQLError( + message: "The enum value \"\(enumTypeDef.name).\(enumValueDef.name)\" is deprecated. \(deprecationReason)", + nodes: [node] + ) + ) + } + } + return .continue + } + ) +} diff --git a/Sources/GraphQL/Validation/Rules/Custom/NoSchemaIntrospectionCustomRule.swift b/Sources/GraphQL/Validation/Rules/Custom/NoSchemaIntrospectionCustomRule.swift new file mode 100644 index 0000000..c3cbd48 --- /dev/null +++ b/Sources/GraphQL/Validation/Rules/Custom/NoSchemaIntrospectionCustomRule.swift @@ -0,0 +1,31 @@ + +/** + * Prohibit introspection queries + * + * A GraphQL document is only valid if all fields selected are not fields that + * return an introspection type. + * + * Note: This rule is optional and is not part of the Validation section of the + * GraphQL Specification. This rule effectively disables introspection, which + * does not reflect best practices and should only be done if absolutely necessary. + */ +public func NoSchemaIntrospectionCustomRule(context: ValidationContext) -> Visitor { + return Visitor( + enter: { node, _, _, _, _ in + if let node = node as? Field { + if + let type = getNamedType(type: context.type), + isIntrospectionType(type: type) + { + context.report( + error: GraphQLError( + message: "GraphQL introspection has been disabled, but the requested query contained the field \(node.name.value)", + nodes: [node] + ) + ) + } + } + return .continue + } + ) +} diff --git a/Tests/GraphQLTests/ValidationTests/NoDeprecatedCustomRuleTests.swift b/Tests/GraphQLTests/ValidationTests/NoDeprecatedCustomRuleTests.swift new file mode 100644 index 0000000..36736bd --- /dev/null +++ b/Tests/GraphQLTests/ValidationTests/NoDeprecatedCustomRuleTests.swift @@ -0,0 +1,345 @@ +@testable import GraphQL +import XCTest + +class NoDeprecatedCustomRuleTests: ValidationTestCase { + override func setUp() { + rule = NoDeprecatedCustomRule + } + + // MARK: no deprecated fields + + let deprecatedFieldSchema = try! GraphQLSchema( + query: .init(name: "Query", fields: [ + "normalField": .init(type: GraphQLString), + "deprecatedField": .init(type: GraphQLString, deprecationReason: "Some field reason."), + ]) + ) + + func testIgnoresFieldsThatAreNotDeprecated() throws { + try assertValid( + """ + { + normalField + } + """, + schema: deprecatedFieldSchema + ) + } + + func testIgnoresUnknownFields() throws { + try assertValid( + """ + { + unknownField + } + + fragment UnknownFragment on UnknownType { + deprecatedField + } + """, + schema: deprecatedFieldSchema + ) + } + + func testReportsErrorWhenADeprecatedFieldIsSelected() throws { + let errors = try assertInvalid( + errorCount: 2, + query: """ + { + deprecatedField + } + + fragment QueryFragment on Query { + deprecatedField + } + """, + schema: deprecatedFieldSchema + ) + try assertValidationError( + error: errors[0], + locations: [(line: 2, column: 3)], + message: #"The field Query.deprecatedField is deprecated. Some field reason."# + ) + try assertValidationError( + error: errors[1], + locations: [(line: 6, column: 3)], + message: #"The field Query.deprecatedField is deprecated. Some field reason."# + ) + } + + // MARK: no deprecated arguments on fields + + let deprecatedFieldArgumentSchema = try! GraphQLSchema( + query: .init(name: "Query", fields: [ + "someField": .init(type: GraphQLString, args: [ + "normalArg": .init(type: GraphQLString), + "deprecatedArg": .init(type: GraphQLString, deprecationReason: "Some arg reason."), + ]), + ]) + ) + + func testIgnoresFieldArgumentsThatAreNotDeprecated() throws { + try assertValid( + """ + { + normalField(normalArg: "") + } + """, + schema: deprecatedFieldArgumentSchema + ) + } + + func testIgnoresUnknownFieldArguments() throws { + try assertValid( + """ + { + someField(unknownArg: "") + unknownField(deprecatedArg: "") + } + """, + schema: deprecatedFieldArgumentSchema + ) + } + + func testReportsErrorWhenADeprecatedFieldArgumentIsUsed() throws { + let errors = try assertInvalid( + errorCount: 1, + query: """ + { + someField(deprecatedArg: "") + } + """, + schema: deprecatedFieldArgumentSchema + ) + try assertValidationError( + error: errors[0], + locations: [(line: 2, column: 13)], + message: #"Field "Query.someField" argument "deprecatedArg" is deprecated. Some arg reason."# + ) + } + + // MARK: no deprecated arguments on directives + + let deprecatedDirectiveArgumentSchema = try! GraphQLSchema( + query: .init(name: "Query", fields: [ + "someField": .init(type: GraphQLString), + ]), + directives: [ + .init( + name: "someDirective", + locations: [ + .field, + ], + args: [ + "normalArg": .init(type: GraphQLString), + "deprecatedArg": .init(type: GraphQLString, + deprecationReason: "Some arg reason."), + ] + ), + ] + ) + + func testIgnoresDirectiveArgumentsThatAreNotDeprecated() throws { + try assertValid( + """ + { + someField @someDirective(normalArg: "") + } + """, + schema: deprecatedDirectiveArgumentSchema + ) + } + + func testIgnoresUnknownDirectiveArguments() throws { + try assertValid( + """ + { + someField @someDirective(unknownArg: "") + someField @unknownDirective(deprecatedArg: "") + } + """, + schema: deprecatedDirectiveArgumentSchema + ) + } + + func testReportsErrorWhenADeprecatedDirectiveArgumentIsUsed() throws { + let errors = try assertInvalid( + errorCount: 1, + query: """ + { + someField @someDirective(deprecatedArg: "") + } + """, + schema: deprecatedDirectiveArgumentSchema + ) + try assertValidationError( + error: errors[0], + locations: [(line: 2, column: 28)], + message: #"Directive "@someDirective" argument "deprecatedArg" is deprecated. Some arg reason."# + ) + } + + // MARK: no deprecated input fields + + let deprecatedInputFieldSchema: GraphQLSchema = { + let inputType = try! GraphQLInputObjectType(name: "InputType", fields: [ + "normalField": .init(type: GraphQLString), + "deprecatedField": .init(type: GraphQLString, + deprecationReason: "Some input field reason."), + ]) + return try! GraphQLSchema( + query: .init(name: "Query", fields: [ + "someField": .init(type: GraphQLString, args: [ + "someArg": .init(type: inputType), + ]), + ]), + types: [ + inputType, + ], + directives: [ + .init( + name: "someDirective", + locations: [ + .field, + ], + args: [ + "someArg": .init(type: inputType), + ] + ), + ] + ) + }() + + func testIgnoresInputFieldsThatAreNotDeprecated() throws { + try assertValid( + """ + { + someField( + someArg: { normalField: "" } + ) @someDirective(someArg: { normalField: "" }) + } + """, + schema: deprecatedInputFieldSchema + ) + } + + func testIgnoresUnknownInputFields() throws { + try assertValid( + """ + { + someField( + someArg: { unknownField: "" } + ) + + someField( + unknownArg: { unknownField: "" } + ) + + unknownField( + unknownArg: { unknownField: "" } + ) + } + """, + schema: deprecatedInputFieldSchema + ) + } + + func testReportsErrorWhenADeprecatedInputFieldIsUsed() throws { + let errors = try assertInvalid( + errorCount: 2, + query: """ + { + someField( + someArg: { deprecatedField: "" } + ) @someDirective(someArg: { deprecatedField: "" }) + } + """, + schema: deprecatedInputFieldSchema + ) + try assertValidationError( + error: errors[0], + locations: [(line: 3, column: 16)], + message: #"The input field InputType.deprecatedField is deprecated. Some input field reason."# + ) + try assertValidationError( + error: errors[1], + locations: [(line: 4, column: 31)], + message: #"The input field InputType.deprecatedField is deprecated. Some input field reason."# + ) + } + + // MARK: no deprecated enum values + + let deprecatedEnumValueSchema: GraphQLSchema = { + let enumType = try! GraphQLEnumType(name: "EnumType", values: [ + "NORMAL_VALUE": .init(value: .string("NORMAL_VALUE")), + "DEPRECATED_VALUE": .init(value: .string("DEPRECATED_VALUE"), + deprecationReason: "Some enum reason."), + ]) + return try! GraphQLSchema( + query: .init(name: "Query", fields: [ + "someField": .init(type: GraphQLString, args: [ + "enumArg": .init(type: enumType), + ]), + ]), + types: [ + enumType, + ] + ) + }() + + func testIgnoresEnumValuesThatAreNotDeprecated() throws { + try assertValid( + """ + { + normalField(enumArg: NORMAL_VALUE) + } + """, + schema: deprecatedEnumValueSchema + ) + } + + func testIgnoresUnknownEnumValues() throws { + try assertValid( + """ + query ( + $unknownValue: EnumType = UNKNOWN_VALUE + $unknownType: UnknownType = UNKNOWN_VALUE + ) { + someField(enumArg: UNKNOWN_VALUE) + someField(unknownArg: UNKNOWN_VALUE) + unknownField(unknownArg: UNKNOWN_VALUE) + } + + fragment SomeFragment on Query { + someField(enumArg: UNKNOWN_VALUE) + } + """, + schema: deprecatedEnumValueSchema + ) + } + + func testReportsErrorWhenADeprecatedEnumValueIsUsed() throws { + let errors = try assertInvalid( + errorCount: 2, + query: """ + query ( + $variable: EnumType = DEPRECATED_VALUE + ) { + someField(enumArg: DEPRECATED_VALUE) + } + """, + schema: deprecatedEnumValueSchema + ) + try assertValidationError( + error: errors[0], + locations: [(line: 2, column: 25)], + message: #"The enum value "EnumType.DEPRECATED_VALUE" is deprecated. Some enum reason."# + ) + try assertValidationError( + error: errors[1], + locations: [(line: 4, column: 22)], + message: #"The enum value "EnumType.DEPRECATED_VALUE" is deprecated. Some enum reason."# + ) + } +}