diff --git a/src/All.slnx b/src/All.slnx index 994d5d8b2ba..1be3ede579a 100644 --- a/src/All.slnx +++ b/src/All.slnx @@ -118,6 +118,7 @@ + @@ -130,9 +131,7 @@ - - - + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index bbd9056be49..1ef93dc8c70 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -13,6 +13,7 @@ + @@ -132,4 +133,4 @@ - + \ No newline at end of file diff --git a/src/HotChocolate/Core/HotChocolate.Core.slnx b/src/HotChocolate/Core/HotChocolate.Core.slnx index cadf7faeb97..3fe231c6326 100644 --- a/src/HotChocolate/Core/HotChocolate.Core.slnx +++ b/src/HotChocolate/Core/HotChocolate.Core.slnx @@ -80,8 +80,9 @@ + - - + + - \ No newline at end of file + diff --git a/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.Rules.cs b/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.Rules.cs index 2a4c85e0a7b..0f1dff3cec7 100644 --- a/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.Rules.cs +++ b/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.Rules.cs @@ -121,6 +121,14 @@ public static IValidationBuilder AddDocumentRules( /// public static IValidationBuilder AddFieldRules( this IValidationBuilder builder) + { + return builder + .TryAddValidationRule() + .TryAddValidationRule(); + } + + public static IValidationBuilder AddFieldRulesLegacy( + this IValidationBuilder builder) { return builder.TryAddValidationVisitor(); } diff --git a/src/HotChocolate/Core/src/Validation/HotChocolate.Validation.csproj b/src/HotChocolate/Core/src/Validation/HotChocolate.Validation.csproj index 139cbd4a666..e8a6b1637c3 100644 --- a/src/HotChocolate/Core/src/Validation/HotChocolate.Validation.csproj +++ b/src/HotChocolate/Core/src/Validation/HotChocolate.Validation.csproj @@ -4,7 +4,6 @@ HotChocolate.Validation HotChocolate.Validation HotChocolate.Validation - @@ -13,6 +12,7 @@ + diff --git a/src/HotChocolate/Core/src/Validation/Rules/LeafFieldSelectionsRule.cs b/src/HotChocolate/Core/src/Validation/Rules/LeafFieldSelectionsRule.cs new file mode 100644 index 00000000000..b1fb5fe5555 --- /dev/null +++ b/src/HotChocolate/Core/src/Validation/Rules/LeafFieldSelectionsRule.cs @@ -0,0 +1,94 @@ +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Validation.Rules; + +internal sealed class LeafFieldSelectionsRule : IDocumentValidatorRule +{ + public ushort Priority => ushort.MaxValue; + + public bool IsCacheable => true; + + public void Validate(IDocumentValidatorContext context, DocumentNode document) + { + foreach (var operation in document.Definitions) + { + if(operation is not OperationDefinitionNode operationDef) + { + continue; + } + + if (context.Schema.GetOperationType(operationDef.Operation) is { } rootType) + { + ValidateSelectionSet(context, operationDef.SelectionSet, rootType); + } + } + } + + private void ValidateSelectionSet(IDocumentValidatorContext context, SelectionSetNode selectionSet, IType type) + { + foreach (var selection in selectionSet.Selections) + { + if (selection is FieldNode field) + { + ValidateField(context, field, type); + } + else if (selection is InlineFragmentNode inlineFrag) + { + var typeCondition = inlineFrag.TypeCondition is null + ? type + : context.Schema.GetType(inlineFrag.TypeCondition.Name.Value); + ValidateSelectionSet(context, inlineFrag.SelectionSet, typeCondition); + } + else if (selection is FragmentSpreadNode spread + && context.Fragments.TryGetValue(spread.Name.Value, out var frag)) + { + if (context.VisitedFragments.Add(spread.Name.Value)) + { + var typeCondition = context.Schema.GetType(frag.TypeCondition.Name.Value); + ValidateSelectionSet(context, frag.SelectionSet, typeCondition); + context.VisitedFragments.Remove(spread.Name.Value); + } + } + } + } + + private void ValidateField(IDocumentValidatorContext context, FieldNode field, IType parentType) + { + if (parentType is not IComplexOutputType complex || + !complex.Fields.TryGetField(field.Name.Value, out var fieldDef)) + { + // handled by other rules like KnownFieldNames + return; + } + + var namedType = fieldDef.Type.NamedType(); + + if (namedType.IsLeafType()) + { + if (field.SelectionSet is not null) + { + context.ReportError( + ErrorBuilder.New() + .SetMessage($"Field \"{field.Name.Value}\" must not have a selection since type \"{namedType.Name}\" has no subfields.") + .AddLocation(field) + .Build()); + } + } + else + { + if (field.SelectionSet is null) + { + context.ReportError( + ErrorBuilder.New() + .SetMessage($"Field \"{field.Name.Value}\" of type \"{namedType.Name}\" must have a selection of subfields. Did you mean \"{field.Name.Value} {{ ... }}\"?") + .AddLocation(field) + .Build()); + } + else + { + ValidateSelectionSet(context, field.SelectionSet, fieldDef.Type); + } + } + } +} diff --git a/src/HotChocolate/Core/src/Validation/Rules/OverlappingFieldsCanBeMergedRule.cs b/src/HotChocolate/Core/src/Validation/Rules/OverlappingFieldsCanBeMergedRule.cs new file mode 100644 index 00000000000..8521da0130f --- /dev/null +++ b/src/HotChocolate/Core/src/Validation/Rules/OverlappingFieldsCanBeMergedRule.cs @@ -0,0 +1,713 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Language; +using HotChocolate.Types; +using Microsoft.Extensions.ObjectPool; + +namespace HotChocolate.Validation.Rules; + +/// +/// Implements the field selection merging rule as described in the GraphQL Spec. +/// https://spec.graphql.org/June2018/#sec-Field-Selection-Merging +/// +/// +/// The algorithm implemented here is not the one from the Spec, but is based on +/// https://tech.xing.com/graphql-overlapping-fields-can-be-merged-fast-ea6e92e0a01 +/// It is not the final version (Listing 11), but Listing 10 adopted to this code base. +/// +internal sealed class OverlappingFieldsCanBeMergedRule : IDocumentValidatorRule +{ + public ushort Priority => ushort.MaxValue; + + public bool IsCacheable => true; + + public void Validate(IDocumentValidatorContext context, DocumentNode document) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + ValidateInternal(new MergeContext(context), document); + } + + private static void ValidateInternal(MergeContext context, DocumentNode document) + { + foreach (var operation in document.Definitions) + { + if (operation is not OperationDefinitionNode operationDef) + { + continue; + } + + if (context.Schema.GetOperationType(operationDef.Operation) is not { } rootType) + { + continue; + } + + var fieldMap = new Dictionary>(); + var visitedFragmentSpreads = new HashSet(); + CollectFields(context, fieldMap, operationDef.SelectionSet, rootType, visitedFragmentSpreads); + var conflicts = FindConflicts(context, fieldMap); + List? fieldNodes = null; + + foreach (var conflict in conflicts) + { + if (context.ConflictsReported.Any(r => r.SetEquals(conflict.Fields))) + { + continue; + } + + context.ConflictsReported.Add(conflict.Fields); + + fieldNodes ??= []; + fieldNodes.Clear(); + fieldNodes.AddRange(conflict.Fields); + fieldNodes.Order(FieldLocationComparer.Instance); + + context.ReportError( + ErrorBuilder.New() + .SetMessage(conflict.Reason) + .SetLocations(fieldNodes) + .SetPath(conflict.Path) + .SpecifiedBy("sec-Field-Selection-Merging") + .Build()); + } + } + } + + private static void CollectFields( + MergeContext context, + Dictionary> fieldMap, + SelectionSetNode selectionSet, IType parentType, + HashSet visitedFragmentSpreads) + { + foreach (var selection in selectionSet.Selections) + { + switch (selection) + { + case FieldNode field: + CollectFieldsForField(fieldMap, parentType, field); + break; + + case InlineFragmentNode inlineFragment: + var type = inlineFragment.TypeCondition is null + ? parentType + : context.Schema.GetType(inlineFragment.TypeCondition.Name.Value); + CollectFields(context, fieldMap, inlineFragment.SelectionSet, type, visitedFragmentSpreads); + break; + + case FragmentSpreadNode spread: + if (!visitedFragmentSpreads.Add(spread.Name.Value)) + { + continue; + } + + if (context.Fragments.TryGetValue(spread.Name.Value, out var fragment)) + { + var fragType = context.Schema.GetType(fragment.TypeCondition.Name.Value); + CollectFields(context, fieldMap, fragment.SelectionSet, fragType, visitedFragmentSpreads); + } + + break; + } + } + } + + private static void CollectFieldsForField( + Dictionary> fieldMap, + IType parentType, + FieldNode field) + { + var responseName = field.Alias?.Value ?? field.Name.Value; + if (!fieldMap.TryGetValue(responseName, out var fields)) + { + fields = []; + fieldMap[responseName] = fields; + } + + var unwrappedParentType = parentType.NamedType(); + + if (unwrappedParentType is IComplexOutputType complexType + && complexType.Fields.TryGetField(field.Name.Value, out var fieldDef)) + { + fields.Add(new FieldAndType(field, fieldDef.Type, complexType)); + } + else + { + fields.Add(new FieldAndType(field, null, unwrappedParentType)); + } + } + + private static List FindConflicts( + MergeContext context, + Dictionary> fieldMap) + { + var result = new List(); + + SameResponseShapeByName(context, fieldMap, Path.Root, result); + SameForCommonParentsByName(context, fieldMap, Path.Root, result); + + return result; + } + + private static void SameResponseShapeByName( + MergeContext context, + Dictionary> fieldMap, + Path path, + List conflictsResult) + { + foreach (var entry in fieldMap) + { + if (context.SameResponseShapeChecked.Any(set => set.SetEquals(entry.Value))) + { + continue; + } + + context.SameResponseShapeChecked.Add(entry.Value); + + var newPath = path.Append(entry.Key); + var conflict = RequireSameOutputTypeShape(entry.Value, newPath); + + if (conflict != null) + { + conflictsResult.Add(conflict); + continue; + } + + var subSelections = MergeSubSelections(context, entry.Value); + SameResponseShapeByName(context, subSelections, newPath, conflictsResult); + } + } + + private static void SameForCommonParentsByName( + MergeContext context, + Dictionary> fieldMap, + Path path, + List conflictsResult) + { + foreach (var entry in fieldMap) + { + var groups = GroupByCommonParents(entry.Value); + var newPath = path.Append(entry.Key); + + foreach (var group in groups) + { + if (context.SameForCommonParentsChecked.Any(g => g.SetEquals(group))) + { + continue; + } + + context.SameForCommonParentsChecked.Add(group); + + var conflict = RequireSameNameAndArguments(newPath, group); + if (conflict is not null) + { + conflictsResult.Add(conflict); + continue; + } + + conflict = RequireStreamDirectiveMergeable(context, group, newPath); + if (conflict is not null) + { + conflictsResult.Add(conflict); + continue; + } + + var subSelections = MergeSubSelections(context, group); + SameForCommonParentsByName(context, subSelections, newPath, conflictsResult); + } + } + } + + private static List> GroupByCommonParents(HashSet fields) + { + // Separate abstract and concrete parent types + var abstractFields = new List(); + var concreteGroups = new Dictionary>(); + + foreach (var field in fields) + { + var parent = field.ParentType.NamedType(); + + switch (parent.Kind) + { + case TypeKind.Interface or TypeKind.Union: + abstractFields.Add(field); + break; + + case TypeKind.Object: + { + if (!concreteGroups.TryGetValue(parent, out var list)) + { + list = []; + concreteGroups[parent] = list; + } + + list.Add(field); + break; + } + } + } + + // If there are no concrete groups, just return one group with all abstract fields + if (concreteGroups.Count == 0) + { + return [new HashSet(abstractFields)]; + } + + var result = new List>(); + + foreach (var group in concreteGroups.Values) + { + var set = new HashSet(); + + foreach (var field in group) + { + set.Add(field); + } + + foreach (var abstractField in abstractFields) + { + set.Add(abstractField); + } + + result.Add(set); + } + + return result; + } + + private static Conflict? RequireSameNameAndArguments(Path path, HashSet fieldGroup) + { + if (fieldGroup.Count <= 1) + { + return null; + } + + var first = fieldGroup.First(); + var name = first.Field.Name.Value; + var arguments = first.Field.Arguments; + var responseName = first.Field.Alias?.Value ?? name; + + foreach (var (field, _, _) in fieldGroup.Skip(1)) + { + if (field.Name.Value != name) + { + return NameMismatchConflict(name, field.Name.Value, path, fieldGroup); + } + + if (!SameArguments(arguments, field.Arguments)) + { + return ArgumentMismatchConflict(responseName, path, fieldGroup); + } + } + + return null; + } + + private static bool SameArguments(IReadOnlyList a, IReadOnlyList b) + { + if (a.Count != b.Count) + { + return false; + } + + if (a.Count == 0) + { + return true; + } + + for (var i = 0; i < a.Count; i++) + { + var argA = a[i]; + var argB = b.FirstOrDefault(x => x.Name.Value == argA.Name.Value); + if (argB is null || !argA.Value.Equals(argB.Value, SyntaxComparison.Syntax)) + { + return false; + } + } + + return true; + } + + private static Dictionary> MergeSubSelections( + MergeContext context, + HashSet fields) + { + var merged = new Dictionary>(); + HashSet? visited = null; + + foreach (var item in fields) + { + if (item.Field.SelectionSet is null || item.Type is null) + { + continue; + } + + visited ??= []; + visited.Clear(); + + CollectFields(context, merged, item.Field.SelectionSet, item.Type, visited); + } + + return merged; + } + + private static Conflict? RequireSameOutputTypeShape( + HashSet fields, + Path path) + { + if (fields.Count <= 1) + { + return null; + } + + var baseField = fields.First(); + + foreach (var current in fields.Skip(1)) + { + var a = baseField.Type; + var b = current.Type; + + while (true) + { + if (a is NonNullType || b is NonNullType) + { + if (a is not NonNullType || b is not NonNullType) + { + return TypeMismatchConflict( + current.Field.Name.Value, + path, + a, + b, + fields); + } + } + + if (a is ListType || b is ListType) + { + if (a is not ListType || b is not ListType) + { + return TypeMismatchConflict( + current.Field.Name.Value, + path, + a, + b, + fields); + } + } + + if (a is not (NonNullType or ListType) && b is not (NonNullType or ListType)) + { + break; + } + + a = a?.InnerType(); + b = b?.InnerType(); + } + + if (!SameType(a, b)) + { + return TypeMismatchConflict( + current.Field.Name.Value, + path, + a, + b, + fields); + } + } + + return null; + } + + private static bool SameType(IType? a, IType? b) + { + if (a is null) + { + return b is null; + } + + if (b is null) + { + return false; + } + + // if the return type of field is a union type, interface type or an object type we can merge + // the selection set. + if (b.Kind is TypeKind.Interface or TypeKind.Union or TypeKind.Object) + { + return true; + } + + return a.Equals(b); + } + + private static Conflict TypeMismatchConflict( + string responseName, + Path path, + IType? typeA, + IType? typeB, + HashSet fields) + { + var typeNameA = typeA?.Print() ?? "null"; + var typeNameB = typeB?.Print() ?? "null"; + + return new Conflict( + string.Format( + "Fields `{0}` conflict because they return conflicting types `{1}` and `{2}`. " + + "Use different aliases on the fields to fetch both if this was intentional.", + responseName, + typeNameA, + typeNameB), + GetFieldNodes(fields), + path); + } + + private static Conflict ArgumentMismatchConflict( + string responseName, + Path path, + HashSet fields) + { + return new Conflict( + string.Format( + "Fields `{0}` conflict because they have differing arguments. " + + "Use different aliases on the fields to fetch both if this was intentional. ", + responseName), + GetFieldNodes(fields), + path); + } + + private static Conflict NameMismatchConflict( + string fieldName1, + string fieldName2, + Path path, + HashSet fields) + { + return new Conflict( + string.Format( + "Fields `{0}` and `{1}` conflict because they have differing names. " + + "Use different aliases on the fields to fetch both if this was intentional.", + fieldName1, + fieldName2), + GetFieldNodes(fields), + path); + } + + private static Conflict? RequireStreamDirectiveMergeable( + MergeContext context, + HashSet fields, + Path path) + { + // if the stream directive is disabled we can skip this check. + if (!context.IsStreamEnabled) + { + return null; + } + + var baseField = fields.FirstOrDefault( + f => f.Field.Directives.Any( + d => d.Name.Value == WellKnownDirectives.Stream)); + + // if there is no stream directive on any field in this group we can skip this check. + if (baseField is null) + { + return null; + } + + var baseInitialCount = GetStreamInitialCount(baseField.Field); + + foreach (var (field, _, _) in fields) + { + if (!TryGetStreamDirective(field, out var streamDirective)) + { + return StreamDirectiveMismatch(field.Name.Value, path, fields); + } + + var initialCount = GetStreamInitialCount(streamDirective); + + if (!SyntaxComparer.BySyntax.Equals(baseInitialCount, initialCount)) + { + return StreamDirectiveMismatch(field.Name.Value, path, fields); + } + } + + return null; + } + + + private static IntValueNode? GetStreamInitialCount(FieldNode field) + { + var streamDirective = field.Directives.First( + d => d.Name.Value == WellKnownDirectives.Stream); + + return GetStreamInitialCount(streamDirective); + } + + private static bool TryGetStreamDirective(FieldNode field, [NotNullWhen(true)] out DirectiveNode? streamDirective) + { + streamDirective = field.Directives.FirstOrDefault( + d => d.Name.Value == WellKnownDirectives.Stream); + return streamDirective is not null; + } + + private static IntValueNode? GetStreamInitialCount(DirectiveNode streamDirective) + { + var initialCountArgument = streamDirective.Arguments.FirstOrDefault( + a => a.Name.Value == WellKnownDirectives.InitialCount); + return initialCountArgument?.Value as IntValueNode; + } + + private static Conflict StreamDirectiveMismatch( + string fieldName, + Path path, + HashSet fields) + { + return new Conflict( + string.Format( + "Fields `{0}` conflict because they have differing stream directives. ", + fieldName), + GetFieldNodes(fields), + path); + } + + private static HashSet GetFieldNodes(HashSet fields) + { + var fieldNodes = new HashSet(fields.Count); + + foreach (var field in fields) + { + fieldNodes.Add(field.Field); + } + + return fieldNodes; + } + + private sealed class FieldAndType + { + public FieldAndType(FieldNode field, IType? type, IType parentType) + { + Field = field; + Type = type; + ParentType = parentType; + } + + public FieldNode Field { get; } + + public IType? Type { get; } + + public IType ParentType { get; } + + public override bool Equals(object? obj) + => obj is FieldAndType other && Field.Equals(other.Field); + + public override int GetHashCode() + => SyntaxComparer.BySyntax.GetHashCode(Field); + + public void Deconstruct(out FieldNode field, out IType? type, out IType parentType) + { + field = Field; + type = Type; + parentType = ParentType; + } + } + + private sealed class Conflict(string reason, HashSet fields, Path? path = null) + { + public string Reason { get; } = reason; + + public HashSet Fields { get; } = fields; + + public Path? Path { get; } = path; + } + + private class HashSetComparer : IEqualityComparer> where T : notnull + { + public static readonly HashSetComparer Instance = new(); + + public bool Equals(HashSet? x, HashSet? y) + => x != null && y != null && x.SetEquals(y); + + public int GetHashCode(HashSet obj) + { + var hash = new HashCode(); + + foreach (var item in obj) + { + hash.Add(item); + } + + return hash.ToHashCode(); + } + } + + private sealed class MergeContext(IDocumentValidatorContext context) + { + public ISchema Schema => context.Schema; + + public HashSet> SameResponseShapeChecked { get; } + = new(HashSetComparer.Instance); + + public HashSet> SameForCommonParentsChecked { get; } + = new(HashSetComparer.Instance); + + public HashSet> ConflictsReported { get; } + = new(HashSetComparer.Instance); + + public IDictionary Fragments => context.Fragments; + + public bool IsStreamEnabled { get; } = context.Schema.TryGetDirectiveType(WellKnownDirectives.Stream, out _); + + public void ReportError(IError error) + { + context.ReportError(error); + } + } + + private sealed class FieldLocationComparer : IComparer + { + public static FieldLocationComparer Instance { get; } = new(); + + public int Compare(FieldNode? x, FieldNode? y) + { + if (x is null && y is null) + { + return 0; + } + + if (x is null) + { + return -1; + } + + if (y is null) + { + return 1; + } + + var xLocation = x.Location; + var yLocation = y.Location; + + if (xLocation is null && yLocation is null) + { + return 0; + } + + if (xLocation is null) + { + return -1; + } + + if (yLocation is null) + { + return 1; + } + + return xLocation.CompareTo(yLocation); + } + } +} diff --git a/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/HotChocolate.Types.Analyzers.Tests.csproj b/src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/HotChocolate.Types.Analyzer.Integration.Tests.csproj similarity index 100% rename from src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/HotChocolate.Types.Analyzers.Tests.csproj rename to src/HotChocolate/Core/test/Types.Analyzer.Integration.Tests/HotChocolate.Types.Analyzer.Integration.Tests.csproj diff --git a/src/HotChocolate/Core/test/Validation.Benchmarks/Directory.Build.props b/src/HotChocolate/Core/test/Validation.Benchmarks/Directory.Build.props new file mode 100644 index 00000000000..501ff72b9c9 --- /dev/null +++ b/src/HotChocolate/Core/test/Validation.Benchmarks/Directory.Build.props @@ -0,0 +1,4 @@ + + + + diff --git a/src/HotChocolate/Core/test/Validation.Benchmarks/Directory.Build.targets b/src/HotChocolate/Core/test/Validation.Benchmarks/Directory.Build.targets new file mode 100644 index 00000000000..501ff72b9c9 --- /dev/null +++ b/src/HotChocolate/Core/test/Validation.Benchmarks/Directory.Build.targets @@ -0,0 +1,4 @@ + + + + diff --git a/src/HotChocolate/Core/test/Validation.Benchmarks/HotChocolate.Validation.Benchmarks.csproj b/src/HotChocolate/Core/test/Validation.Benchmarks/HotChocolate.Validation.Benchmarks.csproj new file mode 100644 index 00000000000..75bc9b9e3d4 --- /dev/null +++ b/src/HotChocolate/Core/test/Validation.Benchmarks/HotChocolate.Validation.Benchmarks.csproj @@ -0,0 +1,19 @@ + + + + Exe + net9.0 + enable + enable + false + + + + + + + + + + + diff --git a/src/HotChocolate/Core/test/Validation.Benchmarks/OverlappingFieldsCanBeMergedRuleBenchmarks.cs b/src/HotChocolate/Core/test/Validation.Benchmarks/OverlappingFieldsCanBeMergedRuleBenchmarks.cs new file mode 100644 index 00000000000..b1834585454 --- /dev/null +++ b/src/HotChocolate/Core/test/Validation.Benchmarks/OverlappingFieldsCanBeMergedRuleBenchmarks.cs @@ -0,0 +1,61 @@ +using BenchmarkDotNet.Attributes; +using HotChocolate.Language; +using HotChocolate.Validation.Rules; + +namespace HotChocolate.Validation.Benchmarks; + +[RPlotExporter, CategoriesColumn, RankColumn, MeanColumn, MedianColumn, MemoryDiagnoser] +public class OverlappingFieldsCanBeMergedRuleBenchmarks +{ + private readonly DocumentValidatorRule _currentRule = new(new FieldVisitor()); + private readonly OverlappingFieldsCanBeMergedRule _newRule = new(); + private readonly DocumentValidatorContext _context = new(); + private readonly Dictionary _contextData = new(); + private readonly ISchema _schema; + private readonly DocumentNode _document; + + public OverlappingFieldsCanBeMergedRuleBenchmarks() + { + _schema = CreateTestSchema("/Users/michael/local/hc-0/src/HotChocolate/Core/test/Validation.Benchmarks/schema.graphqls"); + var query = File.ReadAllBytes("/Users/michael/local/hc-0/src/HotChocolate/Core/test/Validation.Benchmarks/query.graphql"); + _document = Utf8GraphQLParser.Parse(query); + } + + [Benchmark] + public void Field_Merge_Current() + { + // clear last run data + _context.Clear(); + _contextData.Clear(); + + // prepare context for this run + _context.Schema = _schema; + _context.ContextData = _contextData; + _context.Prepare(_document); + + // run the rule + _currentRule.Validate(_context, _document); + } + + [Benchmark] + public void Field_Merge_New() + { + // clear last run data + _context.Clear(); + _contextData.Clear(); + + // prepare context for this run + _context.Schema = _schema; + _context.ContextData = _contextData; + _context.Prepare(_document); + + // run the rule + _newRule.Validate(_context, _document); + } + + private static ISchema CreateTestSchema(string fileName) => + SchemaBuilder.New() + .AddDocumentFromFile(fileName) + .Use(next => next) + .Create(); +} diff --git a/src/HotChocolate/Core/test/Validation.Benchmarks/Program.cs b/src/HotChocolate/Core/test/Validation.Benchmarks/Program.cs new file mode 100644 index 00000000000..8d9c0065471 --- /dev/null +++ b/src/HotChocolate/Core/test/Validation.Benchmarks/Program.cs @@ -0,0 +1,4 @@ +using BenchmarkDotNet.Running; +using HotChocolate.Validation.Benchmarks; + +BenchmarkRunner.Run(); diff --git a/src/HotChocolate/Core/test/Validation.Benchmarks/ValidationUtils.cs b/src/HotChocolate/Core/test/Validation.Benchmarks/ValidationUtils.cs new file mode 100644 index 00000000000..f72140fa854 --- /dev/null +++ b/src/HotChocolate/Core/test/Validation.Benchmarks/ValidationUtils.cs @@ -0,0 +1,30 @@ +using HotChocolate.Language; +using HotChocolate.Types; +using DirectiveLocation = HotChocolate.Types.DirectiveLocation; + +namespace HotChocolate.Validation; + +public static class ValidationUtils +{ + public static DocumentValidatorContext CreateContext(ISchema schema) + => new() + { + Schema = schema, + ContextData = new Dictionary(), + }; + + public static void Prepare(this IDocumentValidatorContext context, DocumentNode document) + { + context.Fragments.Clear(); + + for (var i = 0; i < document.Definitions.Count; i++) + { + var definitionNode = document.Definitions[i]; + if (definitionNode.Kind == SyntaxKind.FragmentDefinition) + { + var fragmentDefinition = (FragmentDefinitionNode)definitionNode; + context.Fragments[fragmentDefinition.Name.Value] = fragmentDefinition; + } + } + } +} diff --git a/src/HotChocolate/Core/test/Validation.Benchmarks/query.graphql b/src/HotChocolate/Core/test/Validation.Benchmarks/query.graphql new file mode 100644 index 00000000000..cd00c563708 --- /dev/null +++ b/src/HotChocolate/Core/test/Validation.Benchmarks/query.graphql @@ -0,0 +1,406 @@ +query useOrgExplorerQueryRef_OrganizationQuery( + $aadObjectId: String! + $jobTitleFilter: [String] + $hybridLocationFilter: String + $directsToLoad: Int! + $worksWithToLoad: Int! + $withHighlights: Boolean = true + $withFiltering: Boolean = true +) { + orgexplorer { + personInFocusV2(aadObjectId: $aadObjectId) { + debugInformation { + dataSource + isYggdrasilCacheEnabled + } + fullName + ...OrganizationNova + id + } + } +} +fragment ColumnPerson on PersonInFocusV2Type { + fullName + directReportsCount + ...Direct + ...ExpandedDirects +} +fragment Direct on PersonInFocusV2Type { + aadObjectId + email + fullName + jobTitle + profilePictureUrl + transitiveReportsCount + directReportsCount + location + ...PersonaPhotoBlobResolver + ...DirectsOverview + ...HighlightHint_DataFragment + @defer( + label: "Direct$defer$HighlightHint_DataFragment" + if: $withHighlights + ) + ...PersonaPresenceBadge_PresenceFragment + @defer(label: "Direct$defer$PersonaPresenceBadge_PresenceFragment") + ...HybridLocation_PresenceFragment + @defer(label: "Direct$defer$HybridLocation_PresenceFragment") +} +fragment DirectsColonnade on PersonInFocusV2Type { + id + directReportsCount + isManager + directs( + first: $directsToLoad + jobTitleFilter: $jobTitleFilter + hybridLocationFilter: $hybridLocationFilter + ) { + edges { + node { + aadObjectId + directReportsCount + id + __typename + } + cursor + } + ...DirectsColonnadeList + pageInfo { + endCursor + hasNextPage + } + } + ...DirectsColonnadeHeader + ...DirectsColonnadeFiltering +} +fragment DirectsColonnadeFiltering on PersonInFocusV2Type { + ...NextLevelReportsTogglePersonInFocus + ...FilterDropDownMenu + @defer( + label: "DirectsColonnadeFiltering$defer$FilterDropDownMenu" + if: $withFiltering + ) +} +fragment DirectsColonnadeHeader on PersonInFocusV2Type { + fullName + isMe +} +fragment DirectsColonnadeList on DirectsConnection { + edges { + node { + aadObjectId + ...ColumnPerson + directs(first: $directsToLoad) { + edges { + node { + aadObjectId + id + } + } + } + id + } + } +} +fragment DirectsOverview on PersonInFocusV2Type { + fullName + directReportsCount + transitiveReportsCount +} +fragment ExpandedDirects on PersonInFocusV2Type { + directs(first: $directsToLoad) { + edges { + node { + aadObjectId + ...Direct + id + __typename + } + cursor + } + pageInfo { + endCursor + hasNextPage + } + } + id +} +fragment FilterDropDownMenu on PersonInFocusV2Type { + directsJobTitles @include(if: $withFiltering) +} +fragment HighlightHint_DataFragment on PersonInFocusV2Type { + highlights @include(if: $withHighlights) { + type + ...HighlightsFragment + ...HighlightPopupHintFragmentForTelemetry + } +} +fragment HighlightPopupHintFragmentForTelemetry on PersonaHighlightType { + type + id + metadata { + occurrenceType + displayRing + } +} +fragment HighlightsFragment on PersonaHighlightType { + hash + type + id + socialDistance + vieweeDisplayName + vieweeEmail + metadata { + occurrenceType + } + highlight { + __typename + ... on BirthdayType { + year + month + day + } + ... on PendingRSVPToMeetingType { + id + meetingId + meetingStartDateTime + meetingSubject + sendResponse + } + ... on PositionChangeType { + newPosition { + title + company { + companyName + } + } + positionChangeType + profileUrl + } + ... on NewHireType { + company { + companyName + } + } + ... on WorkAnniversaryType { + company { + companyName + } + yearsCompleted + } + } +} +fragment HybridLocation_PresenceFragment on PersonInFocusV2Type { + location + presence { + workLocation { + location + } + id + } +} +fragment ManagerChainToggleButton_PersonFragment on PersonInFocusV2Type { + fullName +} +fragment ManagerChain_PersonFragment on PersonInFocusV2Type { + managerChainTopToBottom(first: 100) { + nodes { + aadObjectId + fullName + profilePictureUrl + ...PersonaPhotoBlobResolver + ...Manager_PersonFragment + id + } + } + ...ManagerChainToggleButton_PersonFragment +} +fragment Manager_PersonFragment on PersonInFocusV2Type { + aadObjectId + department + fullName + jobTitle + profilePictureUrl + transitiveReportsCount + directReportsCount + ...PersonaPhotoBlobResolver + ...HighlightHint_DataFragment + @defer( + label: "Manager_PersonFragment$defer$HighlightHint_DataFragment" + if: $withHighlights + ) + ...PersonaPresenceBadge_PresenceFragment + @defer( + label: "Manager_PersonFragment$defer$PersonaPresenceBadge_PresenceFragment" + ) +} +fragment NextLevelReportsTogglePersonInFocus on PersonInFocusV2Type { + fullName +} +fragment OrganizationNova on PersonInFocusV2Type { + ...ManagerChain_PersonFragment + ...PeerPillsDirectManager + ...PersonInFocusView_PersonFragment + ...DirectsColonnade + ...WorksWithView + managersChain: managerChainTopToBottom(first: 1) { + nodes { + id + } + } + directsList: directs(first: 1) { + nodes { + id + } + } + workswith: workingWith(first: 1) { + nodes { + id + } + } +} +fragment PeerPillsDirectManager on PersonInFocusV2Type { + aadObjectId + fullName + manager: managerChainTopToBottom(last: 1) { + nodes { + aadObjectId + fullName + isMe + id + } + } + peers(first: $directsToLoad) { + edges { + node { + aadObjectId + fullName + ...PersonaPillPerson + id + __typename + } + cursor + } + pageInfo { + endCursor + hasNextPage + } + } + id +} +fragment PersonInFocusCopyLinkButton on PersonInFocusV2Type { + aadObjectId +} +fragment PersonInFocusPersona_PersonFragment on PersonInFocusV2Type { + department + fullName + jobTitle + profilePictureUrl + location + ...PersonaPhotoBlobResolver + ...HighlightHint_DataFragment + @defer( + label: "PersonInFocusPersona_PersonFragment$defer$HighlightHint_DataFragment" + if: $withHighlights + ) + ...PersonaPresenceBadge_PresenceFragment + @defer( + label: "PersonInFocusPersona_PersonFragment$defer$PersonaPresenceBadge_PresenceFragment" + ) + ...HybridLocation_PresenceFragment + @defer( + label: "PersonInFocusPersona_PersonFragment$defer$HybridLocation_PresenceFragment" + ) +} +fragment PersonInFocusView_PersonFragment on PersonInFocusV2Type { + aadObjectId + department + directReportsCount + fullName + jobTitle + isManager + email + manager: managerChainTopToBottom(last: 1) { + nodes { + aadObjectId + fullName + id + } + } + ...PersonInFocusPersona_PersonFragment + ...PersonInFocusCopyLinkButton + transitiveReportsCount +} +fragment PersonaPhotoBlobResolver on PersonInFocusV2Type { + profilePictureUrl +} +fragment PersonaPillPerson on PersonInFocusV2Type { + aadObjectId + fullName + profilePictureUrl + transitiveReportsCount + directReportsCount + ...PersonaPhotoBlobResolver +} +fragment PersonaPresenceBadge_PresenceFragment on PersonInFocusV2Type { + presence { + availability + calendarData { + isOutOfOffice + } + id + } +} +fragment PresenceOrHighlightsBadge_PresenceFragment on PersonInFocusV2Type { + highlights @include(if: $withHighlights) { + id + } + ...HighlightHint_DataFragment @include(if: $withHighlights) + presence { + availability + calendarData { + isOutOfOffice + } + id + } + ...PersonaPresenceBadge_PresenceFragment +} +fragment WorksWithCarousel on PersonInFocusV2Type { + fullName + workingWith(first: $worksWithToLoad) { + nodes { + aadObjectId + ...WorksWithPerson + id + } + } +} +fragment WorksWithHeader on PersonInFocusV2Type { + fullName + isMe + isManager +} +fragment WorksWithPerson on PersonInFocusV2Type { + aadObjectId + fullName + email + jobTitle + profilePictureUrl + ...PersonaPhotoBlobResolver + location + ...HybridLocation_PresenceFragment + @defer(label: "WorksWithPerson$defer$HybridLocation_PresenceFragment") + ...PresenceOrHighlightsBadge_PresenceFragment + @defer( + label: "WorksWithPerson$defer$PresenceOrHighlightsBadge_PresenceFragment" + ) +} +fragment WorksWithView on PersonInFocusV2Type { + workingWith(first: $worksWithToLoad) { + nodes { + id + } + } + ...WorksWithHeader + ...WorksWithCarousel +} diff --git a/src/HotChocolate/Core/test/Validation.Benchmarks/schema.graphqls b/src/HotChocolate/Core/test/Validation.Benchmarks/schema.graphqls new file mode 100644 index 00000000000..c95bcbc4db2 --- /dev/null +++ b/src/HotChocolate/Core/test/Validation.Benchmarks/schema.graphqls @@ -0,0 +1,786 @@ +schema { + query: Query +} + +interface CollabNetDocumentActivity { + activityType: CollabNetDocumentActivityTypeEnumType! + timestamp: DateTime + "Preview text of the activity." + preview: String + file: DocumentFileType! + associatedActors: [CollabNetActorType!]! +} + +"The node interface is implemented by entities that have a global unique identifier." +interface Node { + id: ID! +} + +interface OrgExplorerPersonType { + id: ID! + aadObjectId: UUID + fullName: String + profilePictureUrl: String + email: String + jobTitle: String + department: String + location: String + directReportsCount: Int + transitiveReportsCount: Int + presence: PresenceType + "Gets the person highlights data" + highlights: [PersonaHighlightType!] + "Gets the person's directs with a limit" + directsV2("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): DirectsV2Connection +} + +type ActorType { + displayName: String! +} + +type BasicUserInfoType implements OrgExplorerPersonType & Node { + id: ID! + aadObjectId: UUID! + fullName: String! + email: String + jobTitle: String + department: String + location: String + profilePictureUrl: String + directReportsCount: Int + transitiveReportsCount: Int + presence: PresenceType + "Gets the person highlights data" + highlights: [PersonaHighlightType!] + "Gets the person's directs with paging" + directsV2("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): DirectsV2Connection +} + +type BirthdayType { + day: Int + month: Int + year: Int +} + +type CalendarDataType { + isOutOfOffice: Boolean! +} + +type CollabNetActorType { + aadObjectId: String! + emailAddress: String! + displayName: String! +} + +type CollabNetDocumentCommentType implements CollabNetDocumentActivity { + activityType: CollabNetDocumentActivityTypeEnumType! + timestamp: DateTime + preview: String + file: DocumentFileType! + associatedActors: [CollabNetActorType!]! + "Deep link URL to specific activity in the file." + deepLinkUrl: String! +} + +type CollabNetDocumentMentionedMeActivity implements CollabNetDocumentActivity { + activityType: CollabNetDocumentActivityTypeEnumType! + timestamp: DateTime + preview: String + file: DocumentFileType! + associatedActors: [CollabNetActorType!]! + "Deep link URL to specific activity in the file." + deepLinkUrl: String! +} + +type CollabNetDocumentModifyType implements CollabNetDocumentActivity { + activityType: CollabNetDocumentActivityTypeEnumType! + timestamp: DateTime + preview: String + file: DocumentFileType! + associatedActors: [CollabNetActorType!]! +} + +type CollabNetDocumentMultipleModifyType implements CollabNetDocumentActivity { + activityType: CollabNetDocumentActivityTypeEnumType! + timestamp: DateTime + preview: String + file: DocumentFileType! + associatedActors: [CollabNetActorType!]! +} + +type CollabNetDocumentReplyType implements CollabNetDocumentActivity { + activityType: CollabNetDocumentActivityTypeEnumType! + timestamp: DateTime + preview: String + file: DocumentFileType! + associatedActors: [CollabNetActorType!]! + "Deep link URL to specific activity in the file." + deepLinkUrl: String! +} + +type CollabNetDocumentSharingType implements CollabNetDocumentActivity { + activityType: CollabNetDocumentActivityTypeEnumType! + timestamp: DateTime + preview: String + file: DocumentFileType! + associatedActors: [CollabNetActorType!]! +} + +"A connection to a list of items." +type DirectsConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [DirectsEdge!] + "A flattened list of the nodes." + nodes: [PersonInFocusV2Type!] +} + +"An edge in a connection." +type DirectsEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: PersonInFocusV2Type! +} + +"A connection to a list of items." +type DirectsV2Connection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [DirectsV2Edge!] + "A flattened list of the nodes." + nodes: [BasicUserInfoType!] +} + +"An edge in a connection." +type DirectsV2Edge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: BasicUserInfoType! +} + +type DocumentFileType { + name: String! + extension: String! + webUrl: String! +} + +type EmailMessageType { + "This ID is in EWS format." + id: String! + subject: String! + bodyPreview: String! +} + +type EmailWithLotsOfReactionsType { + reactionSummary: ReactionSummaryType! + actors: [ActorType]! + message: EmailMessageType! +} + +type EmploymentType { + title: String! + startEndDate: TimeframeType + company: LinkedInCompanyType +} + +type HighlightMetadataType { + occurrenceType: OccurrenceType + displayRing: Boolean +} + +type Identity { + displayName: String + id: String +} + +type IdentitySet { + user: Identity + application: Identity + device: Identity +} + +type InferenceData { + confidenceScore: Float! + userHasVerifiedAccuracy: Boolean +} + +type LanguageProficiencyType { + id: String! + displayName: String! + tag: String! + written: String! + reading: String! + allowedAudiences: String! + inference: InferenceData + lastModifiedBy: IdentitySet +} + +type LinkedInCompanyType { + companyName: String! + companyLocation: String! + companyLogo: String! + linkedInUrl: String! +} + +type LinkedInDurationType { + numDays: Int + numMonths: Int + numYears: Int +} + +type LinkedInProfileSuggestionType { + displayName: String + emails: [String] + headline: String + photoUrl: String + linkedInUrl: String + linkedInId: String +} + +type LinkedInSchoolType { + schoolName: String! + linkedInUrl: String + schoolLocation: String + schoolLogo: String +} + +"A connection to a list of items." +type ManagerChainTopToBottomConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [ManagerChainTopToBottomEdge!] + "A flattened list of the nodes." + nodes: [PersonInFocusV2Type!] +} + +"An edge in a connection." +type ManagerChainTopToBottomEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: PersonInFocusV2Type! +} + +"A connection to a list of items." +type ManagersConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [ManagersEdge!] + "A flattened list of the nodes." + nodes: [BasicUserInfoType!] +} + +"An edge in a connection." +type ManagersEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: BasicUserInfoType! +} + +type MediaPostType { + title: String! + description: String! + url: String! + createdOn: TimestampType + thumbnails: [String]! + mediaCategory: ShareMediaCategory +} + +type MissedMeetingType { + meetingRecapTeamsUrl: String + meetingName: String +} + +type NamePronunciationType { + name: String + pronunciation: String +} + +type NewHireType { + company: LinkedInCompanyType + hiredOn: TimestampType +} + +type NewSmartNotesAvailableType { + notesCount: Int! +} + +type NotFirstDegreeConnectionType { + linkedInMemberId: String + connectionStatus: ConnectionStatusEnumType + connectionDegree: ConnectionDegreeEnumType +} + +type NoteBasedActionType { + text: String +} + +type NoteContent { + text: String + mri: String +} + +type NoteType { + message: String + messageV2: [NoteContent] + publishTime: DateTime! + expiry: DateTime! +} + +type OrgExplorerDebugInformationType { + dataSource: OrgExplorerDataSource + isYggdrasilCacheEnabled: Boolean +} + +type OrgExplorerType { + "Gets the person in focus' data using Yggdrasil" + personInFocusV2(aadObjectId: String!): PersonInFocusV2Type +} + +type OverlapInfoDetailType { + startEndDate: TimeframeType + duration: LinkedInDurationType +} + +type OverlapInfoType { + detail: OverlapInfoDetailType + overlapType: OverlapType +} + +"Information about pagination in a connection." +type PageInfo { + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! + "When paginating backwards, the cursor to continue." + startCursor: String + "When paginating forwards, the cursor to continue." + endCursor: String +} + +"A connection to a list of items." +type PeersConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [PeersEdge!] + "A flattened list of the nodes." + nodes: [PersonInFocusV2Type!] +} + +"An edge in a connection." +type PeersEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: PersonInFocusV2Type! +} + +"A connection to a list of items." +type PeersV2Connection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [PeersV2Edge!] + "A flattened list of the nodes." + nodes: [BasicUserInfoType!] +} + +"An edge in a connection." +type PeersV2Edge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: BasicUserInfoType! +} + +type PendingLinkedInInvitationType { + linkedInMemberId: String + invitationId: String + validationToken: String +} + +type PendingRSVPToMeetingType { + id: String! + meetingId: String! + meetingSubject: String! + sendResponse: Boolean! + meetingStartDateTime: DateTime +} + +type PersonInFocusV2Type implements OrgExplorerPersonType & Node { + id: ID! + aadObjectId: UUID + profilePictureUrl: String + fullName: String + email: String + jobTitle: String + department: String + location: String + phoneNumber: String + directReportsCount: Int + transitiveReportsCount: Int + isMe: Boolean + isManager: Boolean + "Gets the person in focus' managers chain top to bottom with paging" + managerChainTopToBottom("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): ManagerChainTopToBottomConnection + "Gets the person in focus' managers chain data with paging" + managers("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): ManagersConnection + "Gets the person in focus' directs data with paging" + directs("Job titles used as a filter for matching directs" jobTitleFilter: [String] "Viva Skills used as a filter for matching directs" vivaSkillsFilter: [String] "Hybrid location used as a filter for matching directs" hybridLocationFilter: String "Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): DirectsConnection + "Gets the person in focus' directs data with paging" + directsV2("Job titles used as a filter for matching directs" jobTitleFilter: [String] "Hybrid location used as a filter for matching directs" hybridLocationFilter: String "Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): DirectsV2Connection + "Gets the person in focus' peers data with paging" + peers("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): PeersConnection + "Gets the person in focus' peers data with paging" + peersV2("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): PeersV2Connection + presence: PresenceType + "Gets the person in focus' highlights data" + highlights: [PersonaHighlightType!] + "Gets the person in focus' working with data with paging" + workingWith("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): WorkingWithConnection + "Gets the person in focus' working with data with paging" + workingWithV2("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): WorkingWithV2Connection + isCallingYggdrasilEnabled: Boolean @deprecated + debugInformation: OrgExplorerDebugInformationType + "Gets the person in focus' directs' job titles" + directsJobTitles: [String!] + "Gets the person in focus' directs' Viva Skills" + directsVivaSkills: [String!] + "Get the Viva Skills for the user. For other users returns only visible and confirmed skills, for the user gives all skills" + vivaSkills("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): VivaSkillsConnection +} + +type PersonaHighlightType { + vieweeDisplayName: String! + vieweeEmail: String! + socialDistance: SocialDistance + metadata: HighlightMetadataType + id: String! + hash: String! + highlight: HighlightUnionType + type: InsightType +} + +type PositionChangeType { + newPosition: EmploymentType + positionChangeType: PositionChangeTypeEnumType + profileUrl: String! +} + +type PostedAnnouncementType { + id: String + announcementTitle: String + subject: String + preview: String + channelName: String + teamName: String + clientThreadId: String + internetMessageId: String + dateTimeCreated: DateTime! +} + +type PresenceType implements Node { + id: ID! + aadObjectId: String! + activity: String! + availability: String! + lastActiveTime: DateTime! + calendarData: CalendarDataType + workLocation: WorkLocationType + note: NoteType +} + +type PromotePeopleNoteType { + text: String +} + +type Query { + orgexplorer: OrgExplorerType +} + +type ReactionSummaryType { + totalReactionCount: Int! + reactionTypeCount: [ReactionTypeCountType]! +} + +type ReactionTypeCountType { + reactionType: ReactionEnumType! + reactionCount: Int! +} + +type SharedEducationType { + school: LinkedInSchoolType + overlapInfo: OverlapInfoType +} + +type SharedExperienceType { + company: LinkedInCompanyType + overlapInfo: OverlapInfoType +} + +type StorylinePostInYammerType { + postType: StorylinePostType! + deeplinkUrl: String + body: String +} + +type SuggestedWorkingHoursType { + workingHoursStart: String + workingHoursEnd: String + currentWorkingHours: WorkingHours +} + +type TimeZoneBase { + name: String + bias: String +} + +type TimeframeType { + start: TimestampType + end: TimestampType +} + +type TimestampType { + day: Int + month: Int + year: Int +} + +type UnifiedPresenceLocationDetailsType { + id: String + name: String + idType: String +} + +type UploadPhotoNudgeType { + linkedInPhotoUrl: String +} + +type VivaSkillType implements Node { + id: ID! + displayName: String +} + +"A connection to a list of items." +type VivaSkillsConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [VivaSkillsEdge!] + "A flattened list of the nodes." + nodes: [VivaSkillType!] +} + +"An edge in a connection." +type VivaSkillsEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: VivaSkillType! +} + +type WorkAnniversaryType { + company: LinkedInCompanyType + startDate: TimestampType + yearsCompleted: Int! +} + +type WorkLocationType { + location: String + subLocation: String + locationSource: String + expiry: DateTime! + isForced: Boolean! + approximateDetails: UnifiedPresenceLocationDetailsType +} + +type WorkingHours { + daysOfWeek: [String] + startTime: String + endTime: String + timeZone: TimeZoneBase +} + +"A connection to a list of items." +type WorkingWithConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [WorkingWithEdge!] + "A flattened list of the nodes." + nodes: [PersonInFocusV2Type!] +} + +"An edge in a connection." +type WorkingWithEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: PersonInFocusV2Type! +} + +"A connection to a list of items." +type WorkingWithV2Connection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [WorkingWithV2Edge!] + "A flattened list of the nodes." + nodes: [BasicUserInfoType!] +} + +"An edge in a connection." +type WorkingWithV2Edge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: BasicUserInfoType! +} + +union HighlightUnionType = BirthdayType | NewHireType | SharedEducationType | SharedExperienceType | PendingRSVPToMeetingType | PositionChangeType | MediaPostType | WorkAnniversaryType | NotFirstDegreeConnectionType | PendingLinkedInInvitationType | StorylinePostInYammerType | PostedAnnouncementType | SuggestedWorkingHoursType | UploadPhotoNudgeType | CollabNetDocumentCommentType | CollabNetDocumentMentionedMeActivity | CollabNetDocumentModifyType | CollabNetDocumentMultipleModifyType | CollabNetDocumentReplyType | CollabNetDocumentSharingType | EmailWithLotsOfReactionsType | LanguageProficiencyType | LinkedInProfileSuggestionType | NamePronunciationType | NoteBasedActionType | PromotePeopleNoteType | NewSmartNotesAvailableType | MissedMeetingType + +enum CollabNetDocumentActivityTypeEnumType { + COMMENT + MENTIONED_ME + MODIFY + MULTIPLE_MODIFY + REPLY + SHARING +} + +enum ConnectionDegreeEnumType { + SELF + DISTANCE_1 + DISTANCE_2 + DISTANCE_3 + OUT_OF_NETWORK +} + +enum ConnectionStatusEnumType { + NOT_CONNECTED + CONNECTED + INVITATION_SENT_FROM_VIEWER + PENDING_INVITATION_SENT_FROM_VIEWEE +} + +enum InsightType { + ARTICLE_POST + BIRTHDAY + NEW_HIRE + SHARED_EDUCATION + SHARED_EXPERIENCE + PENDING_RSVP_TO_MEETING + POSITION_CHANGE + VIDEO_POST + WORK_ANNIVERSARY + NOT_FIRST_DEGREE_CONNECTION + PENDING_LINKED_IN_INVITATION + STORYLINE_POST + POSTED_ANNOUNCEMENT + PENDING_TASK + SUGGESTED_WORKING_HOURS_NUDGE + UPLOAD_PHOTO_NUDGE + COLLAB_NET_DOCUMENT_COMMENT + COLLAB_NET_DOCUMENT_MENTIONED_ME + COLLAB_NET_DOCUMENT_MODIFY + COLLAB_NET_DOCUMENT_MULTIPLE_MODIFY + COLLAB_NET_DOCUMENT_REPLY + COLLAB_NET_DOCUMENT_SHARING + EMAIL_WITH_LOTS_OF_REACTIONS + LANGUAGE_PROFICIENCY + LINKED_IN_PROFILE_SUGGESTION + NAME_PRONUNCIATION + NOTE_BASED_ACTION + PROMOTE_PEOPLE_NOTE + NEW_SMART_NOTES_AVAILABLE + MISSED_MEETING +} + +enum OccurrenceType { + NOT_SET + NEW + REPEAT +} + +enum OrgExplorerDataSource { + YGGDRASIL + DSAPI +} + +enum OverlapType { + BOTH_CURRENT_VIEWER_STARTED_FIRST + BOTH_CURRENT_VIEWEE_STARTED_FIRST + BOTH_CURRENT_STARTED_IN_SAME_MONTH + NOT_BOTH_CURRENT_NO_OVERLAP_VIEWER_STARTED_FIRST + NOT_BOTH_CURRENT_NO_OVERLAP_VIEWEE_STARTED_FIRST + NOT_BOTH_CURRENT_OVERLAP +} + +enum PositionChangeTypeEnumType { + PROMOTION + COMPANY_CHANGE + POSITION_CHANGE + ADDITIONAL_POSITION +} + +enum ReactionEnumType { + CELEBRATE + HEART + LAUGH + LIKE + SURPRISED + SAD + UNLIKE + NONE +} + +enum ShareMediaCategory { + ARTICLE + IMAGE + NONE + RICH + VIDEO + LEARNING_COURSE + JOB + QUESTION + ANSWER + CAROUSEL + TOPIC + NATIVE_DOCUMENT + URN_REFERENCE + LIVE_VIDEO +} + +enum SocialDistance { + CLOSE_COLLEAGUE + FORMER_RECONNECT + STRANGER + RECENT_RECONNECT +} + +enum StorylinePostType { + NONE + DISCUSSION + QUESTION + PRAISE + POLL +} + +directive @domain(name: String) on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT + +"The `DateTime` scalar represents an ISO-8601 compliant date time type." +scalar DateTime @specifiedBy(url: "https:\/\/www.graphql-scalars.com\/date-time") + +scalar UUID @specifiedBy(url: "https:\/\/tools.ietf.org\/html\/rfc4122") + +"The `Upload` scalar type represents a file upload." +scalar Upload \ No newline at end of file diff --git a/src/HotChocolate/Core/test/Validation.Tests/DocumentValidatorVisitorTestBase.cs b/src/HotChocolate/Core/test/Validation.Tests/DocumentValidatorVisitorTestBase.cs index c07b8f08aec..c967e535789 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/DocumentValidatorVisitorTestBase.cs +++ b/src/HotChocolate/Core/test/Validation.Tests/DocumentValidatorVisitorTestBase.cs @@ -37,7 +37,7 @@ protected DocumentValidatorVisitorTestBase(Action configure) public void ContextIsNull() { // arrange - var query = Utf8GraphQLParser.Parse(@"{ foo }"); + var query = Utf8GraphQLParser.Parse("{ foo }"); // act var a = () => Rule.Validate(null!, query); @@ -101,6 +101,19 @@ protected void ExpectErrors( Assert.Collection(context.Errors, elementInspectors); } - context.Errors.MatchSnapshot(); + var snapshot = Snapshot.Create(); + + foreach (var error in context.Errors) + { + snapshot.Add(new + { + error.Message, + error.Locations, + Path = error.Path?.ToString(), + error.Extensions + }); + } + + snapshot.Match(); } } diff --git a/src/HotChocolate/Core/test/Validation.Tests/FieldSelectionMergingRuleTests.cs b/src/HotChocolate/Core/test/Validation.Tests/FieldSelectionMergingRuleTests.cs index 456136db73c..60f748080e4 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/FieldSelectionMergingRuleTests.cs +++ b/src/HotChocolate/Core/test/Validation.Tests/FieldSelectionMergingRuleTests.cs @@ -1,10 +1,11 @@ using HotChocolate.Types; +using HotChocolate.Validation.Rules; using Microsoft.Extensions.DependencyInjection; namespace HotChocolate.Validation; public class FieldSelectionMergingRuleTests() - : DocumentValidatorVisitorTestBase(builder => builder.AddFieldRules()) + : DocumentValidatorVisitorTestBase(builder => builder.TryAddValidationRule()) { [Fact] public void MergeIdenticalFields() @@ -57,10 +58,7 @@ fragment conflictingBecauseAlias on Dog { name: nickname name } - """, - t => Assert.Equal( - "Encountered fields for the same object that cannot be merged.", - t.Message)); + """); } [Fact] @@ -114,10 +112,7 @@ fragment conflictingArgsOnValues on Dog { doesKnowCommand(dogCommand: SIT) doesKnowCommand(dogCommand: HEEL) } - """, - t => Assert.Equal( - "Encountered fields for the same object that cannot be merged.", - t.Message)); + """); } [Fact] @@ -135,10 +130,7 @@ fragment conflictingArgsValueAndVar on Dog { doesKnowCommand(dogCommand: SIT) doesKnowCommand(dogCommand: $dogCommand) } - """, - t => Assert.Equal( - "Encountered fields for the same object that cannot be merged.", - t.Message)); + """); } [Fact] @@ -156,10 +148,7 @@ fragment conflictingArgsWithVars on Dog { doesKnowCommand(dogCommand: $varOne) doesKnowCommand(dogCommand: $varTwo) } - """, - t => Assert.Equal( - "Encountered fields for the same object that cannot be merged.", - t.Message)); + """); } [Fact] @@ -177,10 +166,7 @@ fragment differingArgs on Dog { doesKnowCommand(dogCommand: SIT) doesKnowCommand } - """, - t => Assert.Equal( - "Encountered fields for the same object that cannot be merged.", - t.Message)); + """); } [Fact] @@ -200,10 +186,7 @@ ... dog fragment dog on Dog { doesKnowCommand } - """, - t => Assert.Equal( - "Encountered fields for the same object that cannot be merged.", - t.Message)); + """); } [Fact] @@ -269,10 +252,7 @@ ... on Cat { someValue: meowVolume } } - """, - t => Assert.Equal( - "Encountered fields for the same object that cannot be merged.", - t.Message)); + """); } [Fact] @@ -333,10 +313,7 @@ fields @stream(initialCount: 2) { } } } - """, - t => Assert.Equal( - "Encountered fields for the same object that cannot be merged.", - t.Message)); + """); } [Fact] @@ -357,10 +334,7 @@ fields @stream(initialCount: 1) { } } } - """, - t => Assert.Equal( - "Encountered fields for the same object that cannot be merged.", - t.Message)); + """); } [Fact] @@ -382,10 +356,7 @@ ... FooLevel2 fragment FooLevel2 on Dog { doesKnowCommand(dogCommand: HEEL) } - """, - t => Assert.Equal( - "Encountered fields for the same object that cannot be merged.", - t.Message)); + """); } [Fact] @@ -949,7 +920,7 @@ public void CompatibleReturnShapesOnDifferentReturnTypes() """ { someBox { - ... on SomeBox { + ... on IntBox { deepBox { unrelatedField } @@ -1072,8 +1043,7 @@ ... on StringBox { """); } - // TODO : Fix this issue - [Fact(Skip = "This one needs fixing!")] + [Fact] public void DisallowsDifferingDeepReturnTypesDespiteNoOverlap() { ExpectErrors( @@ -1115,12 +1085,10 @@ ... on StringBox { """); } - // TODO : we need to analyze this validation issue further. - [Fact(Skip = "This one needs to be analyzed further.")] + [Fact] public void SameWrappedScalarReturnTypes() { - ExpectErrors( - TestSchema, + ExpectValid( """ { someBox { @@ -1189,7 +1157,7 @@ ... sameAliasesWithDifferentFieldTargets } fragment sameAliasesWithDifferentFieldTargets on Dog { - ...sameAliasesWithDifferentFieldTargets + ... sameAliasesWithDifferentFieldTargets fido: name fido: nickname } diff --git a/src/HotChocolate/Core/test/Validation.Tests/IntrospectionDepthRuleTests.cs b/src/HotChocolate/Core/test/Validation.Tests/IntrospectionDepthRuleTests.cs index d48f598e275..46df8176cd6 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/IntrospectionDepthRuleTests.cs +++ b/src/HotChocolate/Core/test/Validation.Tests/IntrospectionDepthRuleTests.cs @@ -7,7 +7,8 @@ namespace HotChocolate.Validation; public class IntrospectionDepthRuleTests() : DocumentValidatorVisitorTestBase(b => b.AddIntrospectionDepthRule()) { - [Fact] public void Introspection_With_Cycles_Will_Fail() + [Fact] + public void Introspection_With_Cycles_Will_Fail() { // arrange IDocumentValidatorContext context = ValidationUtils.CreateContext(); diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.AliasMaskingDirectFieldAccess.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.AliasMaskingDirectFieldAccess.snap index fd6d1d1a68d..0341c2dfad5 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.AliasMaskingDirectFieldAccess.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.AliasMaskingDirectFieldAccess.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 8, - "Column": 5 - }, - { - "Line": 9, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Dog", - "fieldA": "nickname", - "fieldB": "name", - "typeA": "String", - "typeB": "String!", - "responseNameA": "name", - "responseNameB": "name", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `name` conflict because they return conflicting types `String` and `String!`. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 8, + "Column": 5 }, - "Exception": null + { + "Line": 9, + "Column": 5 + } + ], + "Path": "/catOrDog/name", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ComparesDeepTypesIncludingList.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ComparesDeepTypesIncludingList.snap index 9875ffb368c..0fe4eb8034c 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ComparesDeepTypesIncludingList.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ComparesDeepTypesIncludingList.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 15, - "Column": 13 - }, - { - "Line": 6, - "Column": 17 - } - ], - "Extensions": { - "declaringTypeA": "Node", - "declaringTypeB": "Node", - "fieldA": "id", - "fieldB": "name", - "typeA": "ID", - "typeB": "String", - "responseNameA": "id", - "responseNameB": "id", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `name` conflict because they return conflicting types `ID` and `String`. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 15, + "Column": 13 }, - "Exception": null + { + "Line": 6, + "Column": 17 + } + ], + "Path": "/connection/edges/node/id", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgNames.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgNames.snap index 05eefd6fbcb..d2e1856f5a8 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgNames.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgNames.snap @@ -1,56 +1,17 @@ -[ - { - "Message": "The field `isAtLocation` does not exist on the type `Dog`.", - "Code": null, - "Path": { - "Name": "catOrDog", - "Parent": { - "Parent": null, - "Length": 0, - "IsRoot": true - }, - "Length": 1, - "IsRoot": false +{ + "Message": "Fields `isAtLocation` conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional. ", + "Locations": [ + { + "Line": 8, + "Column": 5 }, - "Locations": [ - { - "Line": 8, - "Column": 5 - } - ], - "Extensions": { - "type": "Dog", - "field": "isAtLocation", - "responseName": "isAtLocation", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selections-on-Objects-Interfaces-and-Unions-Types" - }, - "Exception": null - }, - { - "Message": "The field `isAtLocation` does not exist on the type `Dog`.", - "Code": null, - "Path": { - "Name": "catOrDog", - "Parent": { - "Parent": null, - "Length": 0, - "IsRoot": true - }, - "Length": 1, - "IsRoot": false - }, - "Locations": [ - { - "Line": 9, - "Column": 5 - } - ], - "Extensions": { - "type": "Dog", - "field": "isAtLocation", - "responseName": "isAtLocation", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selections-on-Objects-Interfaces-and-Unions-Types" - }, - "Exception": null + { + "Line": 9, + "Column": 5 + } + ], + "Path": "/catOrDog/isAtLocation", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgValues.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgValues.snap index d369071123b..cd5a01b19ce 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgValues.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgValues.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 8, - "Column": 5 - }, - { - "Line": 9, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Dog", - "fieldA": "doesKnowCommand", - "fieldB": "doesKnowCommand", - "typeA": "Boolean!", - "typeB": "Boolean!", - "responseNameA": "doesKnowCommand", - "responseNameB": "doesKnowCommand", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `doesKnowCommand` conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional. ", + "Locations": [ + { + "Line": 8, + "Column": 5 }, - "Exception": null + { + "Line": 9, + "Column": 5 + } + ], + "Path": "/catOrDog/doesKnowCommand", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgsOnValues.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgsOnValues.snap index d369071123b..bf85c1a312d 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgsOnValues.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgsOnValues.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 8, - "Column": 5 - }, - { - "Line": 9, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Dog", - "fieldA": "doesKnowCommand", - "fieldB": "doesKnowCommand", - "typeA": "Boolean!", - "typeB": "Boolean!", - "responseNameA": "doesKnowCommand", - "responseNameB": "doesKnowCommand", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `doesKnowCommand` conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional. ", + "Locations": [ + { + "Line": 8, + "Column": 5 }, - "Exception": null + { + "Line": 9, + "Column": 5 + } + ], + "Path": "/dog/doesKnowCommand", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgsValueAndVar.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgsValueAndVar.snap index d369071123b..bf85c1a312d 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgsValueAndVar.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgsValueAndVar.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 8, - "Column": 5 - }, - { - "Line": 9, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Dog", - "fieldA": "doesKnowCommand", - "fieldB": "doesKnowCommand", - "typeA": "Boolean!", - "typeB": "Boolean!", - "responseNameA": "doesKnowCommand", - "responseNameB": "doesKnowCommand", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `doesKnowCommand` conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional. ", + "Locations": [ + { + "Line": 8, + "Column": 5 }, - "Exception": null + { + "Line": 9, + "Column": 5 + } + ], + "Path": "/dog/doesKnowCommand", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgsWithVars.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgsWithVars.snap index d369071123b..bf85c1a312d 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgsWithVars.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingArgsWithVars.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 8, - "Column": 5 - }, - { - "Line": 9, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Dog", - "fieldA": "doesKnowCommand", - "fieldB": "doesKnowCommand", - "typeA": "Boolean!", - "typeB": "Boolean!", - "responseNameA": "doesKnowCommand", - "responseNameB": "doesKnowCommand", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `doesKnowCommand` conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional. ", + "Locations": [ + { + "Line": 8, + "Column": 5 }, - "Exception": null + { + "Line": 9, + "Column": 5 + } + ], + "Path": "/dog/doesKnowCommand", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingBecauseAlias.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingBecauseAlias.snap index fd6d1d1a68d..46ac7b71651 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingBecauseAlias.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingBecauseAlias.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 8, - "Column": 5 - }, - { - "Line": 9, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Dog", - "fieldA": "nickname", - "fieldB": "name", - "typeA": "String", - "typeB": "String!", - "responseNameA": "name", - "responseNameB": "name", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `name` conflict because they return conflicting types `String` and `String!`. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 8, + "Column": 5 }, - "Exception": null + { + "Line": 9, + "Column": 5 + } + ], + "Path": "/dog/name", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingDifferingResponses.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingDifferingResponses.snap index 7e3b69d30bf..8afc2f81fe4 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingDifferingResponses.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingDifferingResponses.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 9, - "Column": 9 - }, - { - "Line": 12, - "Column": 9 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Cat", - "fieldA": "nickname", - "fieldB": "meowVolume", - "typeA": "String", - "typeB": "Int", - "responseNameA": "someValue", - "responseNameB": "someValue", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `meowVolume` conflict because they return conflicting types `String` and `Int`. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 9, + "Column": 9 }, - "Exception": null + { + "Line": 12, + "Column": 9 + } + ], + "Path": "/dog/someValue", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingReturnTypesWhichPotentiallyOverlap.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingReturnTypesWhichPotentiallyOverlap.snap index 75664b2b0c5..99d39fc0fd1 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingReturnTypesWhichPotentiallyOverlap.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ConflictingReturnTypesWhichPotentiallyOverlap.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 4, - "Column": 13 - }, - { - "Line": 7, - "Column": 13 - } - ], - "Extensions": { - "declaringTypeA": "IntBox", - "declaringTypeB": "NonNullStringBox1", - "fieldA": "scalar", - "fieldB": "scalar", - "typeA": "Int", - "typeB": "String!", - "responseNameA": "scalar", - "responseNameB": "scalar", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `scalar` conflict because they return conflicting types `Int` and `String!`. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 4, + "Column": 13 }, - "Exception": null + { + "Line": 7, + "Column": 13 + } + ], + "Path": "/someBox/scalar", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DeepConflict.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DeepConflict.snap index a8fd51041ce..e88fc0aef54 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DeepConflict.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DeepConflict.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 3, - "Column": 9 - }, - { - "Line": 6, - "Column": 9 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "a", - "fieldB": "b", - "typeA": "String", - "typeB": "String", - "responseNameA": "x", - "responseNameB": "x", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `a` and `b` conflict because they have differing names. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 3, + "Column": 9 }, - "Exception": null + { + "Line": 6, + "Column": 9 + } + ], + "Path": "/f1/x", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DeepConflictWithMultipleIssues.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DeepConflictWithMultipleIssues.snap index 9d64ec38e5d..cac3658b471 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DeepConflictWithMultipleIssues.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DeepConflictWithMultipleIssues.snap @@ -1,56 +1,39 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 3, - "Column": 9 - }, - { - "Line": 7, - "Column": 9 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "a", - "fieldB": "b", - "typeA": "String", - "typeB": "String", - "responseNameA": "x", - "responseNameB": "x", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +--------------- +{ + "Message": "Fields `a` and `b` conflict because they have differing names. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 3, + "Column": 9 }, - "Exception": null - }, - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 4, - "Column": 9 - }, - { - "Line": 8, - "Column": 9 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "c", - "fieldB": "d", - "typeA": "String", - "typeB": "String", - "responseNameA": "y", - "responseNameB": "y", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" + { + "Line": 7, + "Column": 9 + } + ], + "Path": "/f1/x", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" + } +} +--------------- + +--------------- +{ + "Message": "Fields `c` and `d` conflict because they have differing names. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 4, + "Column": 9 }, - "Exception": null + { + "Line": 8, + "Column": 9 + } + ], + "Path": "/f1/y", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} +--------------- diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DifferentArgsSecondAddsAnArgument.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DifferentArgsSecondAddsAnArgument.snap index c2d166bc2b9..4ea9441e96f 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DifferentArgsSecondAddsAnArgument.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DifferentArgsSecondAddsAnArgument.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 7, - "Column": 5 - }, - { - "Line": 8, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Dog", - "fieldA": "doesKnowCommand", - "fieldB": "doesKnowCommand", - "typeA": "Boolean!", - "typeB": "Boolean!", - "responseNameA": "doesKnowCommand", - "responseNameB": "doesKnowCommand", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `doesKnowCommand` conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional. ", + "Locations": [ + { + "Line": 7, + "Column": 5 }, - "Exception": null + { + "Line": 8, + "Column": 5 + } + ], + "Path": "/catOrDog/doesKnowCommand", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DifferentArgsSecondMissingAnArgument.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DifferentArgsSecondMissingAnArgument.snap index d369071123b..cd5a01b19ce 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DifferentArgsSecondMissingAnArgument.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DifferentArgsSecondMissingAnArgument.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 8, - "Column": 5 - }, - { - "Line": 9, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Dog", - "fieldA": "doesKnowCommand", - "fieldB": "doesKnowCommand", - "typeA": "Boolean!", - "typeB": "Boolean!", - "responseNameA": "doesKnowCommand", - "responseNameB": "doesKnowCommand", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `doesKnowCommand` conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional. ", + "Locations": [ + { + "Line": 8, + "Column": 5 }, - "Exception": null + { + "Line": 9, + "Column": 5 + } + ], + "Path": "/catOrDog/doesKnowCommand", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DifferingArgs.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DifferingArgs.snap index d369071123b..bf85c1a312d 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DifferingArgs.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DifferingArgs.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 8, - "Column": 5 - }, - { - "Line": 9, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Dog", - "fieldA": "doesKnowCommand", - "fieldB": "doesKnowCommand", - "typeA": "Boolean!", - "typeB": "Boolean!", - "responseNameA": "doesKnowCommand", - "responseNameB": "doesKnowCommand", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `doesKnowCommand` conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional. ", + "Locations": [ + { + "Line": 8, + "Column": 5 }, - "Exception": null + { + "Line": 9, + "Column": 5 + } + ], + "Path": "/dog/doesKnowCommand", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingDeepReturnTypesDespiteNoOverlap.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingDeepReturnTypesDespiteNoOverlap.snap new file mode 100644 index 00000000000..08fe2006ec4 --- /dev/null +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingDeepReturnTypesDespiteNoOverlap.snap @@ -0,0 +1,17 @@ +{ + "Message": "Fields `scalar` conflict because they return conflicting types `String` and `Int`. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 5, + "Column": 17 + }, + { + "Line": 10, + "Column": 17 + } + ], + "Path": "/someBox/box/scalar", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" + } +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypeListDespiteNoOverlap.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypeListDespiteNoOverlap.snap index 4f383b2b360..7da138e303d 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypeListDespiteNoOverlap.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypeListDespiteNoOverlap.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 4, - "Column": 13 - }, - { - "Line": 9, - "Column": 13 - } - ], - "Extensions": { - "declaringTypeA": "IntBox", - "declaringTypeB": "StringBox", - "fieldA": "listStringBox", - "fieldB": "stringBox", - "typeA": "[StringBox]", - "typeB": "StringBox", - "responseNameA": "box", - "responseNameB": "box", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `stringBox` conflict because they return conflicting types `[StringBox]` and `StringBox`. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 4, + "Column": 13 }, - "Exception": null + { + "Line": 9, + "Column": 13 + } + ], + "Path": "/someBox/box", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypeListDespiteNoOverlapReverse.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypeListDespiteNoOverlapReverse.snap index 2fd580a227f..8fd58a3c00d 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypeListDespiteNoOverlapReverse.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypeListDespiteNoOverlapReverse.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 4, - "Column": 13 - }, - { - "Line": 9, - "Column": 13 - } - ], - "Extensions": { - "declaringTypeA": "IntBox", - "declaringTypeB": "StringBox", - "fieldA": "stringBox", - "fieldB": "listStringBox", - "typeA": "StringBox", - "typeB": "[StringBox]", - "responseNameA": "box", - "responseNameB": "box", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `listStringBox` conflict because they return conflicting types `StringBox` and `[StringBox]`. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 4, + "Column": 13 }, - "Exception": null + { + "Line": 9, + "Column": 13 + } + ], + "Path": "/someBox/box", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypeNullabilityDespiteNoOverlap.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypeNullabilityDespiteNoOverlap.snap index 5e7c942bc77..aee850cb0dd 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypeNullabilityDespiteNoOverlap.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypeNullabilityDespiteNoOverlap.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 4, - "Column": 13 - }, - { - "Line": 7, - "Column": 13 - } - ], - "Extensions": { - "declaringTypeA": "NonNullStringBox1", - "declaringTypeB": "StringBox", - "fieldA": "scalar", - "fieldB": "scalar", - "typeA": "String!", - "typeB": "String", - "responseNameA": "scalar", - "responseNameB": "scalar", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `scalar` conflict because they return conflicting types `String!` and `String`. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 4, + "Column": 13 }, - "Exception": null + { + "Line": 7, + "Column": 13 + } + ], + "Path": "/someBox/scalar", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypesDespiteNoOverlap.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypesDespiteNoOverlap.snap index fd986040091..f8259974629 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypesDespiteNoOverlap.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingReturnTypesDespiteNoOverlap.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 4, - "Column": 13 - }, - { - "Line": 7, - "Column": 13 - } - ], - "Extensions": { - "declaringTypeA": "IntBox", - "declaringTypeB": "StringBox", - "fieldA": "scalar", - "fieldB": "scalar", - "typeA": "Int", - "typeB": "String", - "responseNameA": "scalar", - "responseNameB": "scalar", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `scalar` conflict because they return conflicting types `Int` and `String`. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 4, + "Column": 13 }, - "Exception": null + { + "Line": 7, + "Column": 13 + } + ], + "Path": "/someBox/scalar", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingSubfields.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingSubfields.snap index 78f177e2a67..49a61efdeb6 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingSubfields.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.DisallowsDifferingSubfields.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 5, - "Column": 17 - }, - { - "Line": 6, - "Column": 17 - } - ], - "Extensions": { - "declaringTypeA": "StringBox", - "declaringTypeB": "StringBox", - "fieldA": "scalar", - "fieldB": "unrelatedField", - "typeA": "String", - "typeB": "String", - "responseNameA": "val", - "responseNameB": "val", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `scalar` and `unrelatedField` conflict because they have differing names. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 5, + "Column": 17 }, - "Exception": null + { + "Line": 6, + "Column": 17 + } + ], + "Path": "/someBox/box/val", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.EncountersConflictInFragments.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.EncountersConflictInFragments.snap index 32da67820a8..4f41bcdd710 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.EncountersConflictInFragments.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.EncountersConflictInFragments.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 7, - "Column": 5 - }, - { - "Line": 11, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "a", - "fieldB": "b", - "typeA": "String", - "typeB": "String", - "responseNameA": "x", - "responseNameB": "x", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `a` and `b` conflict because they have differing names. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 7, + "Column": 5 }, - "Exception": null + { + "Line": 11, + "Column": 5 + } + ], + "Path": "/x", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.FindsInvalidCaseEvenWithImmediatelyRecursiveFragment.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.FindsInvalidCaseEvenWithImmediatelyRecursiveFragment.snap index 91c6874c13a..401e9c0729a 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.FindsInvalidCaseEvenWithImmediatelyRecursiveFragment.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.FindsInvalidCaseEvenWithImmediatelyRecursiveFragment.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 9, - "Column": 5 - }, - { - "Line": 10, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Dog", - "fieldA": "name", - "fieldB": "nickname", - "typeA": "String!", - "typeB": "String", - "responseNameA": "fido", - "responseNameB": "fido", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `nickname` conflict because they return conflicting types `String!` and `String`. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 9, + "Column": 5 }, - "Exception": null + { + "Line": 10, + "Column": 5 + } + ], + "Path": "/dogOrHuman/fido", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsDeepConflictInNestedFragments.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsDeepConflictInNestedFragments.snap index ab73308a09f..5513583dc3a 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsDeepConflictInNestedFragments.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsDeepConflictInNestedFragments.snap @@ -1,56 +1,39 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 11, - "Column": 5 - }, - { - "Line": 25, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "a", - "fieldB": "b", - "typeA": "String", - "typeB": "String", - "responseNameA": "x", - "responseNameB": "x", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +--------------- +{ + "Message": "Fields `a` and `b` conflict because they have differing names. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 11, + "Column": 5 }, - "Exception": null - }, - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 16, - "Column": 5 - }, - { - "Line": 20, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "c", - "fieldB": "d", - "typeA": "String", - "typeB": "String", - "responseNameA": "y", - "responseNameB": "y", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" + { + "Line": 25, + "Column": 5 + } + ], + "Path": "/f1/x", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" + } +} +--------------- + +--------------- +{ + "Message": "Fields `c` and `d` conflict because they have differing names. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 16, + "Column": 5 }, - "Exception": null + { + "Line": 20, + "Column": 5 + } + ], + "Path": "/f1/y", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} +--------------- diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsDeepConflictToNearestCommonAncestor.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsDeepConflictToNearestCommonAncestor.snap index f2c0776ec2b..39919153d1e 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsDeepConflictToNearestCommonAncestor.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsDeepConflictToNearestCommonAncestor.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 4, - "Column": 13 - }, - { - "Line": 7, - "Column": 13 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "a", - "fieldB": "b", - "typeA": "String", - "typeB": "String", - "responseNameA": "x", - "responseNameB": "x", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `a` and `b` conflict because they have differing names. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 4, + "Column": 13 }, - "Exception": null + { + "Line": 7, + "Column": 13 + } + ], + "Path": "/f1/f2/x", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsDeepConflictToNearestCommonAncestorInFragments.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsDeepConflictToNearestCommonAncestorInFragments.snap index 7bc912ca2cb..158cf31a83f 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsDeepConflictToNearestCommonAncestorInFragments.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsDeepConflictToNearestCommonAncestorInFragments.snap @@ -1,56 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 12, - "Column": 13 - }, - { - "Line": 15, - "Column": 13 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "a", - "fieldB": "b", - "typeA": "String", - "typeB": "String", - "responseNameA": "x", - "responseNameB": "x", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `a` and `b` conflict because they have differing names. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 12, + "Column": 13 }, - "Exception": null - }, - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 15, - "Column": 13 - }, - { - "Line": 12, - "Column": 13 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "b", - "fieldB": "a", - "typeA": "String", - "typeB": "String", - "responseNameA": "x", - "responseNameB": "x", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" - }, - "Exception": null + { + "Line": 15, + "Column": 13 + } + ], + "Path": "/f1/f2/f3/x", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsEachConflictOnce.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsEachConflictOnce.snap index a77010c0561..7c927409360 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsEachConflictOnce.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ReportsEachConflictOnce.snap @@ -1,110 +1,43 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 18, - "Column": 5 - }, - { - "Line": 22, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "a", - "fieldB": "b", - "typeA": "String", - "typeB": "String", - "responseNameA": "x", - "responseNameB": "x", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +--------------- +{ + "Message": "Fields `a` and `b` conflict because they have differing names. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 18, + "Column": 5 }, - "Exception": null - }, - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 22, - "Column": 5 - }, - { - "Line": 18, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "b", - "fieldB": "a", - "typeA": "String", - "typeB": "String", - "responseNameA": "x", - "responseNameB": "x", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" - }, - "Exception": null - }, - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 18, - "Column": 5 - }, - { - "Line": 13, - "Column": 9 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "a", - "fieldB": "c", - "typeA": "String", - "typeB": "String", - "responseNameA": "x", - "responseNameB": "x", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" + { + "Line": 22, + "Column": 5 + } + ], + "Path": "/f1/x", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" + } +} +--------------- + +--------------- +{ + "Message": "Fields `a` and `b` conflict because they have differing names. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 18, + "Column": 5 }, - "Exception": null - }, - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 22, - "Column": 5 - }, - { - "Line": 13, - "Column": 9 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "b", - "fieldB": "c", - "typeA": "String", - "typeB": "String", - "responseNameA": "x", - "responseNameB": "x", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" + { + "Line": 22, + "Column": 5 }, - "Exception": null + { + "Line": 13, + "Column": 9 + } + ], + "Path": "/f3/x", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} +--------------- diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.SameAliasesAllowedOnNonOverlappingFields.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.SameAliasesAllowedOnNonOverlappingFields.snap index 178d2d45d29..eaffae3976f 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.SameAliasesAllowedOnNonOverlappingFields.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.SameAliasesAllowedOnNonOverlappingFields.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 9, - "Column": 9 - }, - { - "Line": 12, - "Column": 9 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Cat", - "fieldA": "name", - "fieldB": "nickname", - "typeA": "String!", - "typeB": "String", - "responseNameA": "name", - "responseNameB": "name", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `nickname` conflict because they return conflicting types `String!` and `String`. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 9, + "Column": 9 }, - "Exception": null + { + "Line": 12, + "Column": 9 + } + ], + "Path": "/catOrDog/name", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.SameAliasesWithDifferentFieldTargets.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.SameAliasesWithDifferentFieldTargets.snap index e445b6e464b..79daaed18b0 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.SameAliasesWithDifferentFieldTargets.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.SameAliasesWithDifferentFieldTargets.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 8, - "Column": 5 - }, - { - "Line": 9, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Dog", - "fieldA": "name", - "fieldB": "nickname", - "typeA": "String!", - "typeB": "String", - "responseNameA": "fido", - "responseNameB": "fido", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `nickname` conflict because they return conflicting types `String!` and `String`. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 8, + "Column": 5 }, - "Exception": null + { + "Line": 9, + "Column": 5 + } + ], + "Path": "/catOrDog/fido", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.SameResponseNameDifferentFieldName.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.SameResponseNameDifferentFieldName.snap index 63503fbdb37..5626b1b4f75 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.SameResponseNameDifferentFieldName.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.SameResponseNameDifferentFieldName.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 2, - "Column": 5 - }, - { - "Line": 5, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "catOrDog", - "fieldB": "dogOrHuman", - "typeA": "CatOrDog", - "typeB": "DogOrHuman", - "responseNameA": "catOrDog", - "responseNameB": "catOrDog", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `catOrDog` and `dogOrHuman` conflict because they have differing names. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 2, + "Column": 5 }, - "Exception": null + { + "Line": 5, + "Column": 5 + } + ], + "Path": "/catOrDog", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ShortHandQueryWithDuplicateFieldInSecondLevelFragment.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ShortHandQueryWithDuplicateFieldInSecondLevelFragment.snap index d47bb97303c..1c71e3e2e15 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ShortHandQueryWithDuplicateFieldInSecondLevelFragment.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.ShortHandQueryWithDuplicateFieldInSecondLevelFragment.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 3, - "Column": 9 - }, - { - "Line": 13, - "Column": 5 - } - ], - "Extensions": { - "declaringTypeA": "Dog", - "declaringTypeB": "Dog", - "fieldA": "doesKnowCommand", - "fieldB": "doesKnowCommand", - "typeA": "Boolean!", - "typeB": "Boolean!", - "responseNameA": "doesKnowCommand", - "responseNameB": "doesKnowCommand", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `doesKnowCommand` conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional. ", + "Locations": [ + { + "Line": 3, + "Column": 9 }, - "Exception": null + { + "Line": 13, + "Column": 5 + } + ], + "Path": "/dog/doesKnowCommand", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.Stream_Argument_Mismatch.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.Stream_Argument_Mismatch.snap index 37b48a660fc..3d14c8316e6 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.Stream_Argument_Mismatch.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.Stream_Argument_Mismatch.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 4, - "Column": 9 - }, - { - "Line": 9, - "Column": 9 - } - ], - "Extensions": { - "declaringTypeA": "__Type", - "declaringTypeB": "__Type", - "fieldA": "fields", - "fieldB": "fields", - "typeA": "[__Field!]", - "typeB": "[__Field!]", - "responseNameA": "fields", - "responseNameB": "fields", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `fields` conflict because they have differing stream directives. ", + "Locations": [ + { + "Line": 4, + "Column": 9 }, - "Exception": null + { + "Line": 9, + "Column": 9 + } + ], + "Path": "/__type/fields", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.Stream_On_Some_Fields.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.Stream_On_Some_Fields.snap index 37b48a660fc..3d14c8316e6 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.Stream_On_Some_Fields.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.Stream_On_Some_Fields.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 4, - "Column": 9 - }, - { - "Line": 9, - "Column": 9 - } - ], - "Extensions": { - "declaringTypeA": "__Type", - "declaringTypeB": "__Type", - "fieldA": "fields", - "fieldB": "fields", - "typeA": "[__Field!]", - "typeB": "[__Field!]", - "responseNameA": "fields", - "responseNameB": "fields", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `fields` conflict because they have differing stream directives. ", + "Locations": [ + { + "Line": 4, + "Column": 9 }, - "Exception": null + { + "Line": 9, + "Column": 9 + } + ], + "Path": "/__type/fields", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.VeryDeepConflict.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.VeryDeepConflict.snap index 66698e8c799..80fdbf2e632 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.VeryDeepConflict.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FieldSelectionMergingRuleTests.VeryDeepConflict.snap @@ -1,29 +1,17 @@ -[ - { - "Message": "Encountered fields for the same object that cannot be merged.", - "Code": null, - "Path": null, - "Locations": [ - { - "Line": 4, - "Column": 13 - }, - { - "Line": 9, - "Column": 13 - } - ], - "Extensions": { - "declaringTypeA": "Query", - "declaringTypeB": "Query", - "fieldA": "a", - "fieldB": "b", - "typeA": "String", - "typeB": "String", - "responseNameA": "x", - "responseNameB": "x", - "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" +{ + "Message": "Fields `a` and `b` conflict because they have differing names. Use different aliases on the fields to fetch both if this was intentional.", + "Locations": [ + { + "Line": 4, + "Column": 13 }, - "Exception": null + { + "Line": 9, + "Column": 13 + } + ], + "Path": "/f1/f2/x", + "Extensions": { + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging" } -] +} diff --git a/src/HotChocolate/Language/src/Language.SyntaxTree/Location.cs b/src/HotChocolate/Language/src/Language.SyntaxTree/Location.cs index 9c8963f5bd7..85229797f38 100644 --- a/src/HotChocolate/Language/src/Language.SyntaxTree/Location.cs +++ b/src/HotChocolate/Language/src/Language.SyntaxTree/Location.cs @@ -3,7 +3,7 @@ namespace HotChocolate.Language; /// /// The location of a . /// -public sealed class Location : IEquatable +public sealed class Location : IEquatable, IComparable { /// /// Initializes a new instance of . @@ -110,4 +110,25 @@ public override int GetHashCode() public static bool operator !=(Location? left, Location? right) => !Equals(left, right); + + public int CompareTo(Location? other) + { + if (ReferenceEquals(this, other)) + { + return 0; + } + + if (other is null) + { + return 1; + } + + var startComparison = Start.CompareTo(other.Start); + if (startComparison != 0) + { + return startComparison; + } + + return End.CompareTo(other.End); + } }