/// <summary> /// Validate if the GraphQL <paramref name="selectionSet"/> is valid for the specified <paramref name="graphQLIntrospectionSchema"/> and <paramref name="operationType"/> /// </summary> /// <param name="graphQLIntrospectionSchema">The introspectionSchema to validate against</param> /// <param name="operationType">The operationType to validate against</param> /// <param name="selectionSet">The selectionSet which should be validated</param> /// <returns>An empty list if no errors were found or a <see cref="ValidationError"/> for each error found</returns> public IEnumerable <ValidationError> ValidateGraphQLSelectionSet(GraphQLIntrospectionSchema graphQLIntrospectionSchema, GraphQLOperationType operationType, IEnumerable <GraphQLField> selectionSet) { if (graphQLIntrospectionSchema is null) { throw new ArgumentNullException(nameof(graphQLIntrospectionSchema)); } if (selectionSet is null) { throw new ArgumentNullException(nameof(selectionSet)); } // Get implicit types var graphQLIntrospectionSchemaWithImplicitTypes = graphQLIntrospectionSchema.WithImplicitFields(); GraphQLIntrospectionFullType type = null; switch (operationType) { case GraphQLOperationType.Query: if (graphQLIntrospectionSchemaWithImplicitTypes.QueryType != null) { type = GetTypeByName(graphQLIntrospectionSchemaWithImplicitTypes, graphQLIntrospectionSchemaWithImplicitTypes.QueryType.Name); } break; case GraphQLOperationType.Mutation: if (graphQLIntrospectionSchemaWithImplicitTypes.MutationType != null) { type = GetTypeByName(graphQLIntrospectionSchemaWithImplicitTypes, graphQLIntrospectionSchemaWithImplicitTypes.MutationType.Name); } break; case GraphQLOperationType.Subscription: if (graphQLIntrospectionSchemaWithImplicitTypes.SubscriptionType != null) { type = GetTypeByName(graphQLIntrospectionSchemaWithImplicitTypes, graphQLIntrospectionSchemaWithImplicitTypes.SubscriptionType.Name); } break; default: throw new ArgumentOutOfRangeException(nameof(operationType), $"Operationtype {operationType} is not supported"); } if (type == null) { return(new List <ValidationError> { new ValidationError(ValidationType.Operation_Type_Not_Found, operationType) }); } return(ValidateSelectionSet(graphQLIntrospectionSchemaWithImplicitTypes, selectionSet, type, operationType, rootLevel: true)); }
private static IEnumerable <ValidationError> ValidateSelectionSet(GraphQLIntrospectionSchema graphQLIntrospectionSchema, IEnumerable <GraphQLField> selectionSet, GraphQLIntrospectionFullType graphQLIntrospectionType, GraphQLOperationType operationType, string fieldPath = null, bool rootLevel = false) { foreach (var selection in selectionSet) { // Get fieldPath var selectionFieldPath = fieldPath == null ? selection.Field : string.Join(".", fieldPath, selection.Field); // Get field from introspection var introspectionField = graphQLIntrospectionType.Fields.SingleOrDefault(e => e.Name == selection.Field); if (introspectionField == null) { yield return(new ValidationError(selectionFieldPath, ValidationType.Field_Not_Found, selection)); continue; } // IsDeprecated if (introspectionField.IsDeprecated) { yield return(new ValidationError(selectionFieldPath, ValidationType.Field_Deprecated, selection)); } // Validate arguments foreach (var argument in selection.Arguments) { // Get argument path var argumentPath = $"{selectionFieldPath}({argument.ArgumentName})"; // Get argument var introspectionArgument = introspectionField.Args.SingleOrDefault(arg => arg.Name == argument.ArgumentName); if (introspectionArgument == null) { yield return(new ValidationError(selectionFieldPath, ValidationType.Argument_Not_Found, selection)); continue; } // Validate type var typeName = GetTypeName(introspectionArgument.Type); if (!string.Equals(typeName, argument.ArgumentType, StringComparison.OrdinalIgnoreCase)) { yield return(new ValidationError(selectionFieldPath, ValidationType.Argument_Invalid_Type, selection, introspectionArgument.Type.Name, argument.ArgumentType)); } } // Switch on type kind (ignore __typename since union can only have __typename as selectionSet) var hasSelectionSet = selection.SelectionSet.Any(f => f.Field != "__typename"); // Get concrete type GraphQLIntrospectionFullType graphQLType = GetSubtype(graphQLIntrospectionSchema, introspectionField.Type); // Get kind switch (graphQLType.Kind) { case GraphQLTypeKind.Scalar: case GraphQLTypeKind.Union: case GraphQLTypeKind.Enum: // If Scalar/Union/Enum we cannot have selectionSet if (hasSelectionSet) { yield return(new ValidationError(selectionFieldPath, ValidationType.Field_Cannot_Have_SelectionSet, selection)); } break; case GraphQLTypeKind.Object: case GraphQLTypeKind.Interface: // Object/Interface should have selectionSet if (!hasSelectionSet) { yield return(new ValidationError(selectionFieldPath, ValidationType.Field_Should_Have_SelectionSet, selection)); } break; default: // Report error throw new NotImplementedException($"{nameof(GraphQLTypeKind)} {introspectionField.Type.Kind} not implemented for fields"); } // TODO: Validation should also validate type is correct for instance if GraphQLString is having the type String // Validate type if (selection.BaseType != null) { if (graphQLType.Kind == GraphQLTypeKind.Enum || graphQLType.Kind == GraphQLTypeKind.Scalar) { foreach (var error in ValidateNonNullScalar(selectionFieldPath, selection, IsListType(introspectionField.Type), IsNonNull(introspectionField.Type), graphQLType)) { yield return(error); } } switch (graphQLType.Kind) { case GraphQLTypeKind.Enum: foreach (var error in ValidateEnum(selectionFieldPath, selection, IsListType(introspectionField.Type), IsNonNull(introspectionField.Type), graphQLType)) { yield return(error); } break; case GraphQLTypeKind.Scalar: foreach (var error in ValidateScalar(selectionFieldPath, selection, IsListType(introspectionField.Type), IsNonNull(introspectionField.Type), graphQLType)) { yield return(error); } break; } } // Validate selectionSet if (hasSelectionSet) { // Validate selectionSet foreach (var result in ValidateSelectionSet(graphQLIntrospectionSchema, selection.SelectionSet, graphQLType, operationType, selectionFieldPath)) { yield return(result); } // Validate possible types foreach (var possibleType in selection.TargetTypes) { // Get fieldPath for possible type var possibleTypeFieldPath = $"{selectionFieldPath}[{possibleType.Key}]"; // Get type var introspectionPossibleType = GetTypeByName(graphQLIntrospectionSchema, possibleType.Key); if (introspectionPossibleType == null) { yield return(new ValidationError(possibleTypeFieldPath, ValidationType.PossibleType_Not_Found, selection)); continue; } // Validate selectionSet foreach (var result in ValidateSelectionSet(graphQLIntrospectionSchema, possibleType.Value.SelectionSet, introspectionPossibleType, operationType, possibleTypeFieldPath)) { yield return(result); } } } } }
private static IEnumerable <ValidationError> ValidateNonNullScalar(string selectionFieldPath, GraphQLField selection, bool isListType, bool isNonNull, GraphQLIntrospectionFullType graphQLType) { var type = selection.BaseType; if (isListType) { type = GetIEnumerableType(type); } // If type is class it is always nullable if (type.GetTypeInfo().IsClass) { yield break; } var isNullable = IsNullableType(type); // Validate nullable if (!isNullable != isNonNull) { if (isNonNull) { yield return(new ValidationError(selectionFieldPath, ValidationType.Field_Should_Be_NonNull, selection)); } else { yield return(new ValidationError(selectionFieldPath, ValidationType.Field_Should_Be_Nullable, selection)); } } }
private static IEnumerable <ValidationError> ValidateEnum(string selectionFieldPath, GraphQLField selection, bool isListType, bool isNonNull, GraphQLIntrospectionFullType graphQLType) { var type = GetSpecificType(selection, isListType); if (type.GetTypeInfo().IsEnum) { // Get all enum names var memberNames = Enum.GetNames(type); // Get enum values foreach (var member in type.GetTypeInfo().DeclaredMembers) { if (memberNames.Contains(member.Name)) { var enumMemberValue = member.Name; // Detect if an attribute overrides the name var attribute = member.GetCustomAttribute <EnumMemberAttribute>(); if (attribute != null) { enumMemberValue = attribute.Value; } // Validate if the enum member could be found var enumValue = graphQLType.EnumValues.SingleOrDefault(introspectionMember => string.Equals(introspectionMember.Name, enumMemberValue, StringComparison.OrdinalIgnoreCase)); if (enumValue == null) { yield return(new ValidationError($"{selectionFieldPath}[{enumMemberValue}]", ValidationType.EnumValue_Not_Found, selection)); continue; } // Validate that if the enum member is deprecated if (enumValue.IsDeprecated) { yield return(new ValidationError($"{selectionFieldPath}[{enumMemberValue}]", ValidationType.EnumValue_Deprecated, selection)); continue; } } } } else { yield return(new ValidationError(selectionFieldPath, ValidationType.Field_Type_Not_Enum, selection)); } }
private static IEnumerable <ValidationError> ValidateScalar(string selectionFieldPath, GraphQLField selection, bool isListType, bool isNonNull, GraphQLIntrospectionFullType graphQLType) { var type = GetSpecificType(selection, isListType); switch (graphQLType.Name) { case "String": if (!IsValidStringType(type)) { yield return(new ValidationError( selectionFieldPath, ValidationType.Field_Invalid_Type, selection, typeof(string).Name, type.Name)); } break; case "Boolean": if (!IsValidBooleanType(type, isNonNull)) { yield return(new ValidationError( selectionFieldPath, ValidationType.Field_Invalid_Type, selection, typeof(bool).Name, type.Name)); } break; case "Float": case "Decimal": if (!IsValidFloatOrDecimalType(type, isNonNull)) { yield return(new ValidationError( selectionFieldPath, ValidationType.Field_Invalid_Type, selection, $"{typeof(float).Name} or {typeof(double).Name} or {typeof(decimal).Name}", type.Name)); } break; } }
private static GraphQLIntrospectionFullType GetIntrospectionType(GraphQLIntrospectionFullType graphQLType, GraphQLIntrospectionSchema schema) { List <GraphQLIntrospectionField> fields = new List <GraphQLIntrospectionField>(); if (graphQLType.Fields != null) { fields.AddRange(graphQLType.Fields); } // If it's the query type if (graphQLType.Name == schema.QueryType.Name) { // Add schema fields.Add(new GraphQLIntrospectionField { Name = Implicit_Schema_Name, Type = new GraphQLIntrospectionTypeRef { Kind = GraphQLTypeKind.NonNull, OfType = new GraphQLIntrospectionTypeRef { Kind = GraphQLTypeKind.Object, Name = Implicit_Schema_Type } } }); // Add type fields.Add(new GraphQLIntrospectionField { Name = Implicit_Type_Name, Type = new GraphQLIntrospectionTypeRef { Kind = GraphQLTypeKind.Object, Name = Implicit_Type_Type }, Args = new List <GraphQLIntrospectionInputValue> { new GraphQLIntrospectionInputValue { Name = Implicit_Type_Argument_Name, Type = new GraphQLIntrospectionTypeRef { Kind = GraphQLTypeKind.NonNull, OfType = new GraphQLIntrospectionTypeRef { Kind = GraphQLTypeKind.Scalar, Name = Implicit_Type_Argument_Type } } } } }); } // Add typename fields.Add(new GraphQLIntrospectionField { Name = Implicit_TypeName_Name, Type = new GraphQLIntrospectionTypeRef { Kind = GraphQLTypeKind.Scalar, Name = Implicit_TypeName_Type } }); return(new GraphQLIntrospectionFullType { Description = graphQLType.Description, EnumValues = graphQLType.EnumValues, Fields = fields.AsEnumerable(), InputFields = graphQLType.InputFields, Interfaces = graphQLType.Interfaces, Kind = graphQLType.Kind, Name = graphQLType.Name, PossibleTypes = graphQLType.PossibleTypes }); }