From 8929302812e5baf5de5324e1bed10499e819e114 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 5 Sep 2024 22:17:48 +0200 Subject: [PATCH] Added Parent Projection Requirements (#7431) --- .../Core/src/Abstractions/ParentAttribute.cs | 6 +- .../src/Abstractions/WellKnownContextData.cs | 24 ++- ...uestExecutorServiceCollectionExtensions.cs | 17 +- .../src/Execution/Processing/Operation.cs | 6 +- .../Execution/Processing/OperationCompiler.cs | 3 +- .../Processing/OperationOptimizerContext.cs | 2 +- .../Projections/FieldRequirementsMetadata.cs | 28 +++ .../src/Execution/Projections/PropertyNode.cs | 170 ++++++++++++++++++ .../Projections/PropertyTreeBuilder.cs | 95 ++++++++++ .../RequirementsTypeInterceptor.cs | 69 +++++++ .../Projections/SelectionExpressionBuilder.cs | 162 +++++++++++++++-- .../Types.CursorPagination/ConnectionType.cs | 19 +- .../PagingObjectFieldDescriptorExtensions.cs | 1 - .../MutationConventionTypeInterceptor.cs | 3 +- .../CollectionSegmentType~1.cs | 11 +- ...etPagingObjectFieldDescriptorExtensions.cs | 1 - .../Types/Execution/Processing/IOperation.cs | 5 + .../Definitions/FieldDefinitionBase.cs | 13 +- .../Descriptors/Definitions/FieldFlags.cs | 5 + .../Definitions/ObjectFieldDefinition.cs | 36 ---- .../Descriptors/ObjectFieldDescriptor.cs | 19 ++ .../Types/Relay/QueryFieldTypeInterceptor.cs | 18 +- .../Projections/ProjectableDataLoaderTests.cs | 120 +++++++++++++ ...Tests.Brand_Details_Requires_Brand_Name.md | 23 +++ ...rand_Details_Requires_Brand_Name_NET7_0.md | 22 +++ ...ableDataLoaderTests.Brand_Only_TypeName.md | 23 +++ ...aLoaderTests.Brand_Only_TypeName_NET7_0.md | 22 +++ ...DataLoaderTests.Brand_Products_TypeName.md | 23 +++ ...derTests.Brand_Products_TypeName_NET7_0.md | 22 +++ .../Types.Analyzers.Tests/Types/AuthorNode.cs | 5 +- 30 files changed, 865 insertions(+), 108 deletions(-) create mode 100644 src/HotChocolate/Core/src/Execution/Projections/FieldRequirementsMetadata.cs create mode 100644 src/HotChocolate/Core/src/Execution/Projections/PropertyNode.cs create mode 100644 src/HotChocolate/Core/src/Execution/Projections/PropertyTreeBuilder.cs create mode 100644 src/HotChocolate/Core/src/Execution/Projections/RequirementsTypeInterceptor.cs create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Details_Requires_Brand_Name.md create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Details_Requires_Brand_Name_NET7_0.md create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Only_TypeName.md create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Only_TypeName_NET7_0.md create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Products_TypeName.md create mode 100644 src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Products_TypeName_NET7_0.md diff --git a/src/HotChocolate/Core/src/Abstractions/ParentAttribute.cs b/src/HotChocolate/Core/src/Abstractions/ParentAttribute.cs index 0a07a1a162b..6de2b5a28ce 100644 --- a/src/HotChocolate/Core/src/Abstractions/ParentAttribute.cs +++ b/src/HotChocolate/Core/src/Abstractions/ParentAttribute.cs @@ -4,6 +4,10 @@ namespace HotChocolate; /// Specifies that a resolver parameter represents the parent object. /// [AttributeUsage(AttributeTargets.Parameter)] -public sealed class ParentAttribute : Attribute +public sealed class ParentAttribute(string? requires = null) : Attribute { + /// + /// Gets a string representing the property requirements for the parent object. + /// + public string? Requires { get; } = requires; } diff --git a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs index 91076869d4f..d63960f7fd1 100644 --- a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs +++ b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs @@ -209,12 +209,6 @@ public static class WellKnownContextData /// public const string SkipDepthAnalysis = "HotChocolate.Execution.SkipDepthAnalysis"; - /// - /// The key of the marker setting that a field on the mutation type represents - /// the query field. - /// - public const string MutationQueryField = "HotChocolate.Relay.Mutations.QueryField"; - /// /// The key to the name of the data field when using the mutation convention. /// @@ -315,5 +309,23 @@ public static class WellKnownContextData /// public const string ValidateCost = "HotChocolate.CostAnalysis.ValidateCost"; + /// + /// The key to access the paging observers stored on the local resolver state. + /// public const string PagingObserver = "HotChocolate.Types.PagingObserver"; + + /// + /// The key to access the requirements syntax on an object field definition. + /// + public const string FieldRequirementsSyntax = "HotChocolate.Types.ObjectField.Requirements.Syntax"; + + /// + /// The key to access the requirements entity type on an object field definition. + /// + public const string FieldRequirementsEntity = "HotChocolate.Types.ObjectField.Requirements.EntityType"; + + /// + /// The key to access the compiled requirements. + /// + public const string FieldRequirements = "HotChocolate.Types.ObjectField.Requirements"; } diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs index f1ff52eccbe..6ef1a1e6f2b 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs @@ -6,6 +6,9 @@ using HotChocolate.Execution.Configuration; using HotChocolate.Execution.Options; using HotChocolate.Execution.Processing; +#if NET6_0_OR_GREATER +using HotChocolate.Execution.Projections; +#endif using HotChocolate.Fetching; using HotChocolate.Internal; using HotChocolate.Language; @@ -123,12 +126,8 @@ public static IRequestExecutorBuilder AddGraphQL( throw new ArgumentNullException(nameof(services)); } + services.AddGraphQLCore(); schemaName ??= Schema.DefaultName; - - services - .AddGraphQLCore() - .AddValidation(schemaName); - return CreateBuilder(services, schemaName); } @@ -155,9 +154,6 @@ public static IRequestExecutorBuilder AddGraphQL( } schemaName ??= Schema.DefaultName; - - builder.Services.AddValidation(schemaName); - return CreateBuilder(builder.Services, schemaName); } @@ -167,6 +163,8 @@ private static IRequestExecutorBuilder CreateBuilder( { var builder = new DefaultRequestExecutorBuilder(services, schemaName); + builder.Services.AddValidation(schemaName); + builder.Configure( (sp, e) => { @@ -178,6 +176,9 @@ private static IRequestExecutorBuilder CreateBuilder( builder.TryAddNoOpTransactionScopeHandler(); builder.TryAddTypeInterceptor(); +#if NET6_0_OR_GREATER + builder.TryAddTypeInterceptor(); +#endif return builder; } diff --git a/src/HotChocolate/Core/src/Execution/Processing/Operation.cs b/src/HotChocolate/Core/src/Execution/Processing/Operation.cs index ddebeeb1335..87ea5873070 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/Operation.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/Operation.cs @@ -19,13 +19,15 @@ public Operation( string id, DocumentNode document, OperationDefinitionNode definition, - ObjectType rootType) + ObjectType rootType, + ISchema schema) { Id = id; Document = document; Definition = definition; RootType = rootType; Type = definition.Operation; + Schema = schema; if (definition.Name?.Value is { } name) { @@ -57,6 +59,8 @@ public IReadOnlyList IncludeConditions public IReadOnlyDictionary ContextData => _contextData; + public ISchema Schema { get; } + public ISelectionSet GetSelectionSet(ISelection selection, IObjectType typeContext) { if (selection is null) diff --git a/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.cs b/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.cs index 634c70c6964..ed700e101d1 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.cs @@ -145,7 +145,8 @@ private Operation CreateOperation(OperationCompilerRequest request) request.Id, request.Document, request.Definition, - request.RootType); + request.RootType, + request.Schema); var variants = new SelectionVariants[_selectionVariants.Count]; diff --git a/src/HotChocolate/Core/src/Execution/Processing/OperationOptimizerContext.cs b/src/HotChocolate/Core/src/Execution/Processing/OperationOptimizerContext.cs index 9991c7dbf39..0983d6aa7be 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/OperationOptimizerContext.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/OperationOptimizerContext.cs @@ -125,7 +125,7 @@ public FieldDelegate CompileResolverPipeline(IObjectField field, FieldNode selec /// public IOperation CreateOperation() { - var operation = new Operation(Id, Document, Definition, _rootType); + var operation = new Operation(Id, Document, Definition, _rootType, Schema); operation.Seal(_contextData, _variants, _hasIncrementalParts, _includeConditions); return operation; } diff --git a/src/HotChocolate/Core/src/Execution/Projections/FieldRequirementsMetadata.cs b/src/HotChocolate/Core/src/Execution/Projections/FieldRequirementsMetadata.cs new file mode 100644 index 00000000000..1d657bcc887 --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/Projections/FieldRequirementsMetadata.cs @@ -0,0 +1,28 @@ +#if NET6_0_OR_GREATER +using System.Collections.Immutable; +using HotChocolate.Types; + +namespace HotChocolate.Execution.Projections; + +internal sealed class FieldRequirementsMetadata +{ + private readonly Dictionary> _allRequirements = new(); + private bool _sealed; + + public ImmutableArray? GetRequirements(IObjectField field) + => _allRequirements.TryGetValue(field.Coordinate, out var requirements) ? requirements : null; + + public void TryAddRequirements(SchemaCoordinate fieldCoordinate, ImmutableArray requirements) + { + if(_sealed) + { + throw new InvalidOperationException("The requirements are sealed."); + } + + _allRequirements.TryAdd(fieldCoordinate, requirements); + } + + public void Seal() + => _sealed = true; +} +#endif diff --git a/src/HotChocolate/Core/src/Execution/Projections/PropertyNode.cs b/src/HotChocolate/Core/src/Execution/Projections/PropertyNode.cs new file mode 100644 index 00000000000..56cd7e092ef --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/Projections/PropertyNode.cs @@ -0,0 +1,170 @@ +#if NET6_0_OR_GREATER +using System.Reflection; + +namespace HotChocolate.Execution.Projections; + +internal sealed class PropertyNode : PropertyNodeContainer +{ + public PropertyNode(PropertyInfo property, List? nodes = null) + : base(nodes) + { + Property = property; + IsArray = property.PropertyType.IsArray; + + if (IsArray) + { + ElementType = property.PropertyType.GetElementType(); + } + else + { + var collectionType = GetCollectionType(property.PropertyType); + if (collectionType != null) + { + IsCollection = true; + ElementType = collectionType.GetGenericArguments()[0]; + } + else + { + IsCollection = false; + ElementType = null; + } + } + } + + private PropertyNode( + PropertyInfo property, + List? nodes, + bool isArray, + bool isCollection, + Type? elementType) + : base(nodes) + { + Property = property; + IsArray = isArray; + IsCollection = isCollection; + ElementType = elementType; + } + + public PropertyInfo Property { get; } + + public bool IsCollection { get; } + + public bool IsArray { get; } + + public bool IsArrayOrCollection => IsArray || IsCollection; + + public Type? ElementType { get; } + + public PropertyNode Clone() + { + List? nodes = null; + + if (Nodes.Count > 0) + { + nodes = new(Nodes.Count); + foreach (var node in Nodes) + { + nodes.Add(node.Clone()); + } + } + + return new PropertyNode(Property, nodes, IsArray, IsCollection, ElementType); + } + + private static Type? GetCollectionType(Type type) + { + if (type.IsGenericType + && type.GetGenericTypeDefinition() == typeof(ICollection<>)) + { + return type; + } + + foreach (var interfaceType in type.GetInterfaces()) + { + if (interfaceType.IsGenericType + && interfaceType.GetGenericTypeDefinition() == typeof(ICollection<>)) + { + return interfaceType; + } + } + + return null; + } +} + +internal class PropertyNodeContainer( + List? nodes = null) + : IPropertyNodeProvider +{ + private static readonly IReadOnlyList _emptyNodes = Array.Empty(); + private List? _nodes = nodes; + private bool _sealed; + + public IReadOnlyList Nodes + => _nodes ?? _emptyNodes; + + public PropertyNode AddOrGetNode(PropertyInfo property) + { + if (_sealed) + { + throw new InvalidOperationException("The property node container is sealed."); + } + + _nodes ??= new(); + + foreach (var node in Nodes) + { + if (node.Property.Name.Equals(property.Name)) + { + return node; + } + } + + var newNode = new PropertyNode(property); + _nodes.Add(newNode); + return newNode; + } + + public void AddNode(PropertyNode newNode) + { + if (_sealed) + { + throw new InvalidOperationException("The property node container is sealed."); + } + + _nodes ??= new(); + + foreach (var node in Nodes) + { + if (node.Property.Name.Equals(node.Property.Name)) + { + throw new InvalidOperationException("Duplicate property."); + } + } + + _nodes.Add(newNode); + } + + public void Seal() + { + if (!_sealed) + { + foreach (var node in Nodes) + { + node.Seal(); + } + + _sealed = true; + } + } +} + +internal interface IPropertyNodeProvider +{ + IReadOnlyList Nodes { get; } + + PropertyNode AddOrGetNode(PropertyInfo property); + + void AddNode(PropertyNode newNode); +} +#endif diff --git a/src/HotChocolate/Core/src/Execution/Projections/PropertyTreeBuilder.cs b/src/HotChocolate/Core/src/Execution/Projections/PropertyTreeBuilder.cs new file mode 100644 index 00000000000..9448fd7e38d --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/Projections/PropertyTreeBuilder.cs @@ -0,0 +1,95 @@ +#if NET6_0_OR_GREATER +using System.Collections.Immutable; +using HotChocolate.Language; + +namespace HotChocolate.Execution.Projections; + +internal static class PropertyTreeBuilder +{ + public static ImmutableArray Build( + SchemaCoordinate fieldCoordinate, + Type type, + string requirements) + { + if (!requirements.Trim().StartsWith("{")) + { + requirements = "{" + requirements + "}"; + } + + var selectionSet = Utf8GraphQLParser.Syntax.ParseSelectionSet(requirements); + return Build(fieldCoordinate, type, selectionSet, Path.Root).ToImmutableArray(); + } + + private static List Build( + SchemaCoordinate fieldCoordinate, + Type type, + SelectionSetNode selectionSet, + Path path) + { + var nodes = new List(); + + foreach (var selection in selectionSet.Selections) + { + if (selection is FieldNode field) + { + if(field.Arguments.Count > 0) + { + throw new SchemaException( + SchemaErrorBuilder.New() + .SetMessage("Field arguments in the requirements syntax.") + .Build()); + } + + if(field.Directives.Count > 0) + { + throw new SchemaException( + SchemaErrorBuilder.New() + .SetMessage("Field directives in the requirements syntax.") + .Build()); + } + + if (field.Alias is not null) + { + throw new SchemaException( + SchemaErrorBuilder.New() + .SetMessage("Field aliases in the requirements syntax.") + .Build()); + + } + + var fieldPath = path.Append(field.Name.Value); + var property = type.GetProperty(field.Name.Value); + + if(property is null) + { + throw new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + "The field requirement `{0}` does not exist on `{1}`.", + fieldPath.ToString(), + fieldCoordinate.ToString()) + .Build()); + } + + var children = + field.SelectionSet is not null + ? Build(fieldCoordinate, property.PropertyType, field.SelectionSet, fieldPath) + : null; + + var node = new PropertyNode(property, children); + nodes.Add(node); + node.Seal(); + } + else + { + throw new SchemaException( + SchemaErrorBuilder.New() + .SetMessage("Only field selections are allowed in the requirements syntax.") + .Build()); + } + } + + return nodes; + } +} +#endif diff --git a/src/HotChocolate/Core/src/Execution/Projections/RequirementsTypeInterceptor.cs b/src/HotChocolate/Core/src/Execution/Projections/RequirementsTypeInterceptor.cs new file mode 100644 index 00000000000..ec5b92ad144 --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/Projections/RequirementsTypeInterceptor.cs @@ -0,0 +1,69 @@ +#if NET6_0_OR_GREATER +using System.Collections.Immutable; +using HotChocolate.Configuration; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Definitions; +using static HotChocolate.WellKnownContextData; + +namespace HotChocolate.Execution.Projections; + +internal sealed class RequirementsTypeInterceptor : TypeInterceptor +{ + private readonly FieldRequirementsMetadata _metadata = new(); + + public override void OnBeforeCompleteName( + ITypeCompletionContext completionContext, + DefinitionBase definition) + { + if (definition is SchemaTypeDefinition schema) + { + schema.Features.Set(_metadata); + } + } + + public override void OnBeforeCompleteType( + ITypeCompletionContext completionContext, + DefinitionBase definition) + { + if (definition is not ObjectTypeDefinition typeDef) + { + return; + } + + var runtimeType = typeDef.RuntimeType != typeof(object) ? typeDef.RuntimeType : null; + + foreach (var fieldDef in typeDef.Fields) + { + if((fieldDef.Flags & FieldFlags.WithRequirements) == FieldFlags.WithRequirements) + { + var fieldCoordinate = new SchemaCoordinate( + typeDef.Name, + fieldDef.Name); + + // if the source generator already compiled the + // requirements we will take it and skip compilation. + if (fieldDef.ContextData.TryGetValue(FieldRequirements, out var value)) + { + _metadata.TryAddRequirements(fieldCoordinate, (ImmutableArray)value!); + continue; + } + + var requirements = (string)fieldDef.ContextData[FieldRequirementsSyntax]!; + var entityType = runtimeType ?? (Type)fieldDef.ContextData[FieldRequirementsEntity]!; + + var propertyNodes = PropertyTreeBuilder.Build( + fieldCoordinate, + entityType, + requirements); + + _metadata.TryAddRequirements(fieldCoordinate, propertyNodes); + } + } + } + + internal override void OnAfterCreateSchemaInternal( + IDescriptorContext context, + ISchema schema) + => _metadata.Seal(); +} +#endif diff --git a/src/HotChocolate/Core/src/Execution/Projections/SelectionExpressionBuilder.cs b/src/HotChocolate/Core/src/Execution/Projections/SelectionExpressionBuilder.cs index 240fcffd308..69e6d7b3c65 100644 --- a/src/HotChocolate/Core/src/Execution/Projections/SelectionExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Execution/Projections/SelectionExpressionBuilder.cs @@ -3,7 +3,9 @@ using System.Linq.Expressions; using System.Reflection; using HotChocolate.Execution.Processing; +using HotChocolate.Features; using HotChocolate.Types; +using HotChocolate.Types.Descriptors.Definitions; namespace HotChocolate.Execution.Projections; @@ -13,9 +15,19 @@ public Expression> BuildExpression(ISelection selectio { var rootType = typeof(TRoot); var parameter = Expression.Parameter(rootType, "root"); - var context = new Context(selection.DeclaringOperation, parameter, rootType); + var requirements = selection.DeclaringOperation.Schema.Features.GetRequired(); + var context = new Context(selection.DeclaringOperation, parameter, rootType, requirements); + var root = new PropertyNodeContainer(); var selectionSet = context.GetSelectionSet(selection); - var selectionSetExpression = BuildSelectionSetExpression(selectionSet, context); + + CollectSelections(context, selectionSet, root); + + if (root.Nodes.Count == 0) + { + TryAddAnyLeafField(selection, root); + } + + var selectionSetExpression = BuildSelectionSetExpression(context, root); if (selectionSetExpression is null) { @@ -26,14 +38,14 @@ public Expression> BuildExpression(ISelection selectio } private MemberInitExpression? BuildSelectionSetExpression( - ISelectionSet selectionSet, - Context context) + Context context, + PropertyNodeContainer parent) { var assignments = ImmutableArray.CreateBuilder(); - foreach (var selection in selectionSet.Selections) + foreach (var property in parent.Nodes) { - var assignment = BuildSelectionExpression(selection, context); + var assignment = BuildExpression(property, context); if (assignment is not null) { assignments.Add(assignment); @@ -50,39 +62,155 @@ public Expression> BuildExpression(ISelection selectio assignments.ToImmutable()); } - private MemberAssignment? BuildSelectionExpression( + private void CollectSelection( + Context context, ISelection selection, - Context context) + PropertyNodeContainer parent) { var namedType = selection.Field.Type.NamedType(); if (namedType.IsAbstractType() - || (selection.Field.Type.IsListType() && !namedType.IsLeafType()) + || selection.Field.Type.IsListType() && !namedType.IsLeafType() + || selection.Field.PureResolver is null || selection.Field.ResolverMember?.ReflectedType != selection.Field.DeclaringType.RuntimeType) { - return null; + return; } if (selection.Field.Member is not PropertyInfo property) { - return null; + return; + } + + var flags = ((ObjectField)selection.Field).Flags; + if ((flags & FieldFlags.Connection) == FieldFlags.Connection + || (flags & FieldFlags.CollectionSegment) == FieldFlags.CollectionSegment) + { + return; } - var propertyAccessor = Expression.Property(context.Parent, property); + var propertyNode = parent.AddOrGetNode(property); if (namedType.IsLeafType()) { - return Expression.Bind(property, propertyAccessor); + return; } var selectionSet = context.GetSelectionSet(selection); - var newContext = context with { Parent = propertyAccessor, ParentType = property.PropertyType }; - var selectionSetExpression = BuildSelectionSetExpression(selectionSet, newContext); - return selectionSetExpression is null ? null : Expression.Bind(property, selectionSetExpression); + CollectSelections(context, selectionSet, propertyNode); + + if (propertyNode.Nodes.Count > 0) + { + return; + } + + TryAddAnyLeafField(selection, propertyNode); } - private readonly record struct Context(IOperation Operation, Expression Parent, Type ParentType) + private static void TryAddAnyLeafField( + ISelection selection, + PropertyNodeContainer parent) { + // if we could not collect anything it means that either all fields + // are skipped or that __typename is the only field that is selected. + // in this case we will try to select the id field or if that does + // not exist we will look for a leaf field that we can select. + var type = (ObjectType)selection.Type.NamedType(); + if (type.Fields.TryGetField("id", out var idField) + && idField.Member is PropertyInfo idProperty) + { + parent.AddOrGetNode(idProperty); + } + else + { + var anyProperty = type.Fields.FirstOrDefault(t => t.Type.IsLeafType() && t.Member is PropertyInfo); + if (anyProperty?.Member is PropertyInfo anyPropertyInfo) + { + parent.AddOrGetNode(anyPropertyInfo); + } + } + } + + private void CollectSelections( + Context context, + ISelectionSet selectionSet, + PropertyNodeContainer parent) + { + foreach (var selection in selectionSet.Selections) + { + var requirements = context.GetRequirements(selection); + if (requirements is not null) + { + foreach (var requirement in requirements) + { + parent.AddNode(requirement.Clone()); + } + } + + CollectSelection(context, selection, parent); + } + } + + private MemberAssignment? BuildExpression( + PropertyNode node, + Context context) + { + var propertyAccessor = Expression.Property(context.Parent, node.Property); + + if (node.Nodes.Count == 0) + { + return Expression.Bind(node.Property, propertyAccessor); + } + + if(node.IsArrayOrCollection) + { + throw new NotSupportedException("List projections are not supported."); + } + + var newContext = context with { Parent = propertyAccessor, ParentType = node.Property.PropertyType }; + var requirementsExpression = BuildExpression(node.Nodes, newContext); + return requirementsExpression is null ? null : Expression.Bind(node.Property, requirementsExpression); + } + + private MemberInitExpression? BuildExpression( + IReadOnlyList properties, + Context context) + { + var allAssignments = ImmutableArray.CreateBuilder(); + + foreach (var property in properties) + { + var assignment = BuildExpression(property, context); + if (assignment is not null) + { + allAssignments.Add(assignment); + } + } + + if (allAssignments.Count == 0) + { + return null; + } + + return Expression.MemberInit( + Expression.New(context.ParentType), + allAssignments.ToImmutable()); + } + + private readonly record struct Context( + IOperation Operation, + Expression Parent, + Type ParentType, + FieldRequirementsMetadata Requirements) + { + public ImmutableArray? GetRequirements(ISelection selection) + { + var flags = ((ObjectField)selection.Field).Flags; + return (flags & FieldFlags.WithRequirements) == FieldFlags.WithRequirements + ? Requirements.GetRequirements(selection.Field) + : null; + } + public ISelectionSet GetSelectionSet(ISelection selection) => Operation.GetSelectionSet(selection, (ObjectType)selection.Type.NamedType()); } diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs index 95041903ec0..625297ca8ae 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs @@ -180,15 +180,14 @@ private static ObjectTypeDefinition CreateTypeDefinition( ConnectionType_Edges_Description, edgesType, pureResolver: GetEdges) - { CustomSettings = { ContextDataKeys.Edges } }); - + { Flags = FieldFlags.EdgesField }); if (includeNodesField) { definition.Fields.Add(new( - Names.Nodes, - ConnectionType_Nodes_Description, - pureResolver: GetNodes) - { CustomSettings = { ContextDataKeys.Nodes } }); + Names.Nodes, + ConnectionType_Nodes_Description, + pureResolver: GetNodes) + { Flags = FieldFlags.NodesField }); } if (includeTotalCount) @@ -207,12 +206,10 @@ private static ObjectTypeDefinition CreateTypeDefinition( } private static bool IsEdgesField(ObjectFieldDefinition field) - => field.CustomSettings.Count > 0 && - field.CustomSettings[0].Equals(ContextDataKeys.Edges); + => (field.Flags & FieldFlags.EdgesField) == FieldFlags.EdgesField; private static bool IsNodesField(ObjectFieldDefinition field) - => field.CustomSettings.Count > 0 && - field.CustomSettings[0].Equals(ContextDataKeys.Nodes); + => (field.Flags & FieldFlags.NodesField) == FieldFlags.NodesField; private static IPageInfo GetPagingInfo(IResolverContext context) => context.Parent().Info; @@ -237,7 +234,5 @@ internal static class Names private static class ContextDataKeys { public const string EdgeType = "HotChocolate_Types_Edge"; - public const string Edges = "HotChocolate.Types.Connection.Edges"; - public const string Nodes = "HotChocolate.Types.Connection.Nodes"; } } diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/PagingObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/PagingObjectFieldDescriptorExtensions.cs index ce05ee31b87..5e085d5e4ce 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/PagingObjectFieldDescriptorExtensions.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/PagingObjectFieldDescriptorExtensions.cs @@ -165,7 +165,6 @@ d.Type is ExtendedTypeReference extendedTypeRef && var resolverMember = d.ResolverMember ?? d.Member; d.Type = CreateConnectionTypeRef(c, resolverMember, connectionName, typeRef, options); - d.CustomSettings.Add(typeof(Connection)); }); return descriptor; diff --git a/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs b/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs index 255dfaec74d..8c264ba6648 100644 --- a/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types.Mutations/MutationConventionTypeInterceptor.cs @@ -359,7 +359,8 @@ private void TryApplyPayloadConvention( { // if the field is the query mutation field we will allow it to stay non-nullable // since it does not need the parent. - if (resultField.Type is null || resultField.CustomSettingExists(MutationQueryField)) + if (resultField.Type is null + || (resultField.Flags & FieldFlags.MutationQueryField) == FieldFlags.MutationQueryField) { continue; } diff --git a/src/HotChocolate/Core/src/Types.OffsetPagination/CollectionSegmentType~1.cs b/src/HotChocolate/Core/src/Types.OffsetPagination/CollectionSegmentType~1.cs index f822f39b474..89e11731cb0 100644 --- a/src/HotChocolate/Core/src/Types.OffsetPagination/CollectionSegmentType~1.cs +++ b/src/HotChocolate/Core/src/Types.OffsetPagination/CollectionSegmentType~1.cs @@ -89,7 +89,9 @@ private static ObjectTypeDefinition CreateTypeDefinition(bool withTotalCount) definition.Fields.Add(new( Names.Items, CollectionSegmentType_Items_Description, - pureResolver: GetItems) {CustomSettings = {ContextDataKeys.Items, }, }); + pureResolver: GetItems) + { Flags = FieldFlags.ItemsField }); + if (withTotalCount) { @@ -115,7 +117,7 @@ private static object GetTotalCount(IResolverContext context) => context.Parent().TotalCount; private static bool IsItemsField(ObjectFieldDefinition field) - => field.CustomSettings.Count > 0 && field.CustomSettings[0].Equals(ContextDataKeys.Items); + => (field.Flags & FieldFlags.ItemsField) == FieldFlags.ItemsField; internal static class Names { @@ -123,9 +125,4 @@ internal static class Names public const string Items = "items"; public const string TotalCount = "totalCount"; } - - private static class ContextDataKeys - { - public const string Items = "HotChocolate.Types.CollectionSegment.Items"; - } } diff --git a/src/HotChocolate/Core/src/Types.OffsetPagination/Extensions/OffsetPagingObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Core/src/Types.OffsetPagination/Extensions/OffsetPagingObjectFieldDescriptorExtensions.cs index 73f7d5d9d6b..552451e026f 100644 --- a/src/HotChocolate/Core/src/Types.OffsetPagination/Extensions/OffsetPagingObjectFieldDescriptorExtensions.cs +++ b/src/HotChocolate/Core/src/Types.OffsetPagination/Extensions/OffsetPagingObjectFieldDescriptorExtensions.cs @@ -131,7 +131,6 @@ d.Type is SyntaxTypeReference syntaxTypeRef && var resolverMember = d.ResolverMember ?? d.Member; d.Type = CreateTypeRef(c, resolverMember, collectionSegmentName, typeRef, options); - d.CustomSettings.Add(typeof(CollectionSegment)); }); return descriptor; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/IOperation.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/IOperation.cs index d0eb8e6e8dc..e9b53d82a57 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/IOperation.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/IOperation.cs @@ -59,6 +59,11 @@ public interface IOperation : IHasReadOnlyContextData, IEnumerable bool HasIncrementalParts { get; } + /// + /// Gets the schema for which this operation is compiled. + /// + ISchema Schema { get; } + /// /// Gets the selection set for the specified and /// . diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldDefinitionBase.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldDefinitionBase.cs index c54565e4b3a..12a5bd245ea 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldDefinitionBase.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldDefinitionBase.cs @@ -12,11 +12,16 @@ public abstract class FieldDefinitionBase { private List? _directives; private string? _deprecationReason; + private FieldFlags _flags = FieldFlags.None; /// /// Gets the internal field flags from this field. /// - internal FieldFlags Flags { get; set; } = FieldFlags.None; + internal FieldFlags Flags + { + get => _flags; + set => _flags = value; + } /// /// Describes why this syntax node is deprecated. @@ -101,11 +106,11 @@ protected void CopyTo(FieldDefinitionBase target) if (_directives is { Count: > 0, }) { - target._directives = [.._directives,]; + target._directives = [.._directives]; } target.Type = Type; - target.Ignore = Ignore; + target.Flags = Flags; if (IsDeprecated) { @@ -128,7 +133,7 @@ protected void MergeInto(FieldDefinitionBase target) target.Type = Type; } - target.Ignore = Ignore; + target.Flags = Flags | target.Flags; if (IsDeprecated) { diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldFlags.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldFlags.cs index 716a4d3e872..e3daf087785 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldFlags.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/FieldFlags.cs @@ -22,4 +22,9 @@ internal enum FieldFlags SkipArgument = 32768, TotalCount = 65536, SourceGenerator = 131072, + MutationQueryField = 262144, + EdgesField = 524288, + NodesField = 1048576, + ItemsField = 2097152, + WithRequirements = 4194304, } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs index 0a6a23a6a4f..bdcd76588e2 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs @@ -19,7 +19,6 @@ public class ObjectFieldDefinition : OutputFieldDefinitionBase private List? _middlewareDefinitions; private List? _resultConverters; private List? _expressionBuilders; - private List? _customSettings; private bool _middlewareDefinitionsCleaned; private bool _resultConvertersCleaned; @@ -159,13 +158,6 @@ public IList ParameterExpressionBuilders } } - /// - /// A list of custom settings objects that can be used in the type interceptors. - /// Custom settings are not copied to the actual type system object. - /// - public IList CustomSettings - => _customSettings ??= []; - /// /// Defines if this field configuration represents an introspection field. /// @@ -273,20 +265,6 @@ internal IReadOnlyList GetParameterExpressionBuilde return _expressionBuilders; } - /// - /// A list of custom settings objects that can be user in the type interceptors. - /// Custom settings are not copied to the actual type system object. - /// - internal IReadOnlyList GetCustomSettings() - { - if (_customSettings is null) - { - return Array.Empty(); - } - - return _customSettings; - } - private FieldResolverDelegates GetResolvers() => new(Resolver, PureResolver); @@ -311,11 +289,6 @@ internal void CopyTo(ObjectFieldDefinition target) target._expressionBuilders = [.._expressionBuilders,]; } - if (_customSettings is { Count: > 0, }) - { - target._customSettings = [.._customSettings,]; - } - target.SourceType = SourceType; target.ResolverType = ResolverType; target.Member = Member; @@ -358,12 +331,6 @@ internal void MergeInto(ObjectFieldDefinition target) target._expressionBuilders.AddRange(_expressionBuilders); } - if (_customSettings is { Count: > 0, }) - { - target._customSettings ??= []; - target._customSettings.AddRange(_customSettings); - } - if (!IsParallelExecutable) { target.IsParallelExecutable = false; @@ -513,7 +480,4 @@ private static void CleanMiddlewareDefinitions( } } } - - internal bool CustomSettingExists(object value) - => _customSettings is not null && _customSettings.Contains(value); } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs index bb8ae45a0b3..8a977fce3c3 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs @@ -9,6 +9,7 @@ using HotChocolate.Utilities; using static System.Reflection.BindingFlags; using static HotChocolate.Properties.TypeResources; +using static HotChocolate.WellKnownContextData; #nullable enable @@ -213,6 +214,24 @@ private void CompleteArguments(ObjectFieldDefinition definition) definition.Member, _parameterInfos, definition.GetParameterExpressionBuilders()); + + foreach (var parameter in _parameterInfos) + { + if (!parameter.IsDefined(typeof(ParentAttribute))) + { + continue; + } + + var requirements = parameter.GetCustomAttribute()?.Requires; + if (!(requirements?.Length > 0)) + { + continue; + } + + Definition.Flags |= FieldFlags.WithRequirements; + Definition.ContextData[FieldRequirementsSyntax] = requirements; + Definition.ContextData[FieldRequirementsEntity] = parameter.ParameterType; + } } _argumentsInitialized = true; diff --git a/src/HotChocolate/Core/src/Types/Types/Relay/QueryFieldTypeInterceptor.cs b/src/HotChocolate/Core/src/Types/Types/Relay/QueryFieldTypeInterceptor.cs index 9aacd66aac6..2e3c1a3cc34 100644 --- a/src/HotChocolate/Core/src/Types/Types/Relay/QueryFieldTypeInterceptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Relay/QueryFieldTypeInterceptor.cs @@ -46,18 +46,18 @@ public override void OnBeforeCompleteTypes() TypeReference queryType = TypeReference.Parse($"{_queryType.Name}!"); - _queryField= new ObjectFieldDefinition( + _queryField = new ObjectFieldDefinition( options.QueryFieldName ?? _defaultFieldName, type: queryType, resolver: ctx => new(ctx.GetQueryRoot())); - _queryField.CustomSettings.Add(MutationQueryField); + _queryField.Flags |= FieldFlags.MutationQueryField; foreach (var field in _mutationDefinition.Fields) { - if (!field.IsIntrospectionField && - _context.TryGetType(field.Type!, out IType? returnType) && - returnType.NamedType() is ObjectType payloadType && - options.MutationPayloadPredicate.Invoke(payloadType)) + if (!field.IsIntrospectionField + && _context.TryGetType(field.Type!, out IType? returnType) + && returnType.NamedType() is ObjectType payloadType + && options.MutationPayloadPredicate.Invoke(payloadType)) { _payloads.Add(payloadType.Name); } @@ -69,9 +69,9 @@ public override void OnBeforeCompleteType( ITypeCompletionContext completionContext, DefinitionBase definition) { - if (completionContext.Type is ObjectType objectType && - definition is ObjectTypeDefinition objectTypeDef && - _payloads.Contains(objectType.Name)) + if (completionContext.Type is ObjectType objectType + && definition is ObjectTypeDefinition objectTypeDef + && _payloads.Contains(objectType.Name)) { if (objectTypeDef.Fields.Any(t => t.Name.EqualsOrdinal(_queryField.Name))) { diff --git a/src/HotChocolate/Core/test/Execution.Tests/Projections/ProjectableDataLoaderTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Projections/ProjectableDataLoaderTests.cs index 38de3237506..517f66350a0 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Projections/ProjectableDataLoaderTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Projections/ProjectableDataLoaderTests.cs @@ -398,6 +398,117 @@ public async Task Brand_Details_Country_Name_With_Details_As_Custom_Resolver() } """); +#if NET8_0_OR_GREATER + Snapshot.Create() +#else + Snapshot.Create(postFix: "NET7_0") +#endif + .AddSql(queries) + .AddResult(result) + .MatchMarkdownSnapshot(); + } + + [Fact] + public async Task Brand_Details_Requires_Brand_Name() + { + // Arrange + var queries = new List(); + var connectionString = CreateConnectionString(); + await CatalogContext.SeedAsync(connectionString); + + // Act + var result = await new ServiceCollection() + .AddScoped(_ => queries) + .AddTransient(_ => new CatalogContext(connectionString)) + .AddGraphQL() + .AddQueryType() + .AddTypeExtension() + .AddPagingArguments() + .ModifyRequestOptions(o => o.IncludeExceptionDetails = true) + .ExecuteRequestAsync( + """ + { + brandById(id: 1) { + details + } + } + """); + +#if NET8_0_OR_GREATER + Snapshot.Create() +#else + Snapshot.Create(postFix: "NET7_0") +#endif + .AddSql(queries) + .AddResult(result) + .MatchMarkdownSnapshot(); + } + + [Fact] + public async Task Brand_Products_TypeName() + { + // Arrange + var queries = new List(); + var connectionString = CreateConnectionString(); + await CatalogContext.SeedAsync(connectionString); + + // Act + var result = await new ServiceCollection() + .AddScoped(_ => queries) + .AddTransient(_ => new CatalogContext(connectionString)) + .AddGraphQL() + .AddQueryType() + .AddPagingArguments() + .ModifyRequestOptions(o => o.IncludeExceptionDetails = true) + .ExecuteRequestAsync( + """ + { + brandById(id: 1) { + products { + __typename + } + } + } + """); + + // at the moment we do not support projections on lists + // so products will be empty and we will just select the brand.Id +#if NET8_0_OR_GREATER + Snapshot.Create() +#else + Snapshot.Create(postFix: "NET7_0") +#endif + .AddSql(queries) + .AddResult(result) + .MatchMarkdownSnapshot(); + } + + [Fact] + public async Task Brand_Only_TypeName() + { + // Arrange + var queries = new List(); + var connectionString = CreateConnectionString(); + await CatalogContext.SeedAsync(connectionString); + + // Act + var result = await new ServiceCollection() + .AddScoped(_ => queries) + .AddTransient(_ => new CatalogContext(connectionString)) + .AddGraphQL() + .AddQueryType() + .AddTypeExtension() + .AddPagingArguments() + .ModifyRequestOptions(o => o.IncludeExceptionDetails = true) + .ExecuteRequestAsync( + """ + { + brandById(id: 1) { + __typename + } + } + """); + #if NET8_0_OR_GREATER Snapshot.Create() #else @@ -457,6 +568,15 @@ public BrandDetails GetDetails( => new() { Country = new Country { Name = "Germany" } }; } + [ExtendObjectType] + public class BrandExtensionsWithRequirement + { + [BindMember(nameof(Brand.Details))] + public string GetDetails( + [Parent(requires: nameof(Brand.Name))] Brand brand) + => "Brand Name:" + brand.Name; + } + [ExtendObjectType] public class ProductExtensions { diff --git a/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Details_Requires_Brand_Name.md b/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Details_Requires_Brand_Name.md new file mode 100644 index 00000000000..63a17456a51 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Details_Requires_Brand_Name.md @@ -0,0 +1,23 @@ +# Brand_Details_Requires_Brand_Name + +## SQL + +```text +-- @__keys_0={ '1' } (DbType = Object) +SELECT b."Name", b."Id" +FROM "Brands" AS b +WHERE b."Id" = ANY (@__keys_0) +``` + +## Result + +```json +{ + "data": { + "brandById": { + "details": "Brand Name:Brand0" + } + } +} +``` + diff --git a/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Details_Requires_Brand_Name_NET7_0.md b/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Details_Requires_Brand_Name_NET7_0.md new file mode 100644 index 00000000000..ba64a61032e --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Details_Requires_Brand_Name_NET7_0.md @@ -0,0 +1,22 @@ +# Brand_Details_Requires_Brand_Name + +## SQL + +```text +SELECT b."Name", b."Id" +FROM "Brands" AS b +WHERE b."Id" = 1 +``` + +## Result + +```json +{ + "data": { + "brandById": { + "details": "Brand Name:Brand0" + } + } +} +``` + diff --git a/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Only_TypeName.md b/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Only_TypeName.md new file mode 100644 index 00000000000..6fd95b5e531 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Only_TypeName.md @@ -0,0 +1,23 @@ +# Brand_Only_TypeName + +## SQL + +```text +-- @__keys_0={ '1' } (DbType = Object) +SELECT b."Id" +FROM "Brands" AS b +WHERE b."Id" = ANY (@__keys_0) +``` + +## Result + +```json +{ + "data": { + "brandById": { + "__typename": "Brand" + } + } +} +``` + diff --git a/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Only_TypeName_NET7_0.md b/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Only_TypeName_NET7_0.md new file mode 100644 index 00000000000..e2cf36cd390 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Only_TypeName_NET7_0.md @@ -0,0 +1,22 @@ +# Brand_Only_TypeName + +## SQL + +```text +SELECT b."Id" +FROM "Brands" AS b +WHERE b."Id" = 1 +``` + +## Result + +```json +{ + "data": { + "brandById": { + "__typename": "Brand" + } + } +} +``` + diff --git a/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Products_TypeName.md b/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Products_TypeName.md new file mode 100644 index 00000000000..3dcb885c606 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Products_TypeName.md @@ -0,0 +1,23 @@ +# Brand_Products_TypeName + +## SQL + +```text +-- @__keys_0={ '1' } (DbType = Object) +SELECT b."Id" +FROM "Brands" AS b +WHERE b."Id" = ANY (@__keys_0) +``` + +## Result + +```json +{ + "data": { + "brandById": { + "products": [] + } + } +} +``` + diff --git a/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Products_TypeName_NET7_0.md b/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Products_TypeName_NET7_0.md new file mode 100644 index 00000000000..5a9f23485ed --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Projections/__snapshots__/ProjectableDataLoaderTests.Brand_Products_TypeName_NET7_0.md @@ -0,0 +1,22 @@ +# Brand_Products_TypeName + +## SQL + +```text +SELECT b."Id" +FROM "Brands" AS b +WHERE b."Id" = 1 +``` + +## Result + +```json +{ + "data": { + "brandById": { + "products": [] + } + } +} +``` + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AuthorNode.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AuthorNode.cs index dd3898880eb..def608bd853 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AuthorNode.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AuthorNode.cs @@ -17,12 +17,12 @@ public static async Task> GetBooksAsync( => await dataLoader.LoadAsync(author.Id, cancellationToken); public static string GetAdditionalInfo( - [HotChocolate.Parent] Author author, + [Parent("Id")] Author author, string someArg) => someArg; public static string GetAdditionalInfo1( - [HotChocolate.Parent] Author author, + [Parent] Author author, string someArg1, string someArg2) => someArg1 + someArg2; @@ -43,3 +43,4 @@ public static Task> GetAuthorsHasPostProcessor() [Query] public static string QueryFieldCollocatedWithAuthor() => "hello"; } +