diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/FieldPlanNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/FieldPlanNode.cs index 70997e1665c..4db8fe3ba21 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/FieldPlanNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/FieldPlanNode.cs @@ -1,6 +1,6 @@ -using System.Collections.Immutable; using System.Diagnostics; using HotChocolate.Fusion.Types; +using HotChocolate.Fusion.Types.Collections; using HotChocolate.Language; namespace HotChocolate.Fusion.Planning.Nodes; @@ -11,19 +11,31 @@ public sealed class FieldPlanNode : SelectionPlanNode public FieldPlanNode( FieldNode fieldNode, - CompositeOutputField field) + OutputFieldInfo field) : base(field.Type.NamedType(), fieldNode.SelectionSet?.Selections) { FieldNode = fieldNode; Field = field; ResponseName = FieldNode.Alias?.Value ?? field.Name; + + foreach (var argument in fieldNode.Arguments) + { + AddArgument(new ArgumentAssignment(argument.Name.Value, argument.Value)); + } + } + + public FieldPlanNode( + FieldNode fieldNode, + CompositeOutputField field) + : this(fieldNode, new OutputFieldInfo(field)) + { } public string ResponseName { get; } public FieldNode FieldNode { get; } - public CompositeOutputField Field { get; } + public OutputFieldInfo Field { get; } public IReadOnlyList Arguments => _arguments ?? (IReadOnlyList)Array.Empty(); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OperationPlanNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OperationPlanNode.cs index 43e252fcb17..3854a31c536 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OperationPlanNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OperationPlanNode.cs @@ -8,7 +8,10 @@ namespace HotChocolate.Fusion.Planning.Nodes; /// public sealed class OperationPlanNode : SelectionPlanNode, IOperationPlanNodeProvider { + private static readonly IReadOnlyDictionary _emptyVariableMap = + new Dictionary(); private List? _operations; + private Dictionary? _variables; public OperationPlanNode( string schemaName, @@ -37,9 +40,18 @@ public OperationPlanNode( // todo: variable representations are missing. // todo: how to we represent state? + public IReadOnlyDictionary VariableDefinitions + => _variables ?? _emptyVariableMap; + public IReadOnlyList Operations => _operations ?? (IReadOnlyList)Array.Empty(); + public void AddVariableDefinition(VariableDefinitionNode variable) + { + ArgumentNullException.ThrowIfNull(variable); + (_variables ??= new Dictionary()).Add(variable.Variable.Name.Value, variable); + } + public void AddOperation(OperationPlanNode operation) { ArgumentNullException.ThrowIfNull(operation); @@ -53,7 +65,7 @@ public OperationDefinitionNode ToSyntaxNode() null, null, OperationType.Query, - Array.Empty(), + _variables?.Values.OrderBy(t => t.Variable.Name.Value).ToArray() ?? [], Directives.ToSyntaxNode(), Selections.ToSyntaxNode()); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OutputFieldInfo.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OutputFieldInfo.cs new file mode 100644 index 00000000000..70c4a012a3f --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OutputFieldInfo.cs @@ -0,0 +1,19 @@ +using System.Collections.Immutable; +using HotChocolate.Fusion.Types; + +namespace HotChocolate.Fusion.Planning.Nodes; + +public class OutputFieldInfo(string name, ICompositeType type, ImmutableArray sources) +{ + public OutputFieldInfo(CompositeOutputField field) + : this(field.Name, field.Type, field.Sources.Schemas) + { + + } + + public string Name => name; + + public ICompositeType Type => type; + + public ImmutableArray Sources => sources; +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs index 62877a759f7..0da6037e917 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs @@ -1,3 +1,5 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using HotChocolate.Fusion.Planning.Nodes; using HotChocolate.Fusion.Types; using HotChocolate.Language; @@ -11,9 +13,9 @@ public RootPlanNode CreatePlan(DocumentNode document, string? operationName) { ArgumentNullException.ThrowIfNull(document); - var operationDefinition = document.GetOperation(operationName); + var operationDefinition = document.GetOperation(operationName); var schemasWeighted = GetSchemasWeighted(schema.QueryType, operationDefinition.SelectionSet); - var rootPlanNode = new RootPlanNode(); + var operationPlan = new RootPlanNode(); // this need to be rewritten to check if everything is planned for. foreach (var schemaName in schemasWeighted.OrderByDescending(t => t.Value).Select(t => t.Key)) @@ -23,19 +25,22 @@ public RootPlanNode CreatePlan(DocumentNode document, string? operationName) schema.QueryType, operationDefinition.SelectionSet); - if (TryResolveSelectionSet(operation, operation, new Stack())) + if (TryPlanSelectionSet(operation, operation, new Stack())) { - rootPlanNode.AddOperation(operation); + operationPlan.AddOperation(operation); } } - return rootPlanNode; + OperationVariableBinder.BindOperationVariables(operationDefinition, operationPlan); + + return operationPlan; } - private bool TryResolveSelectionSet( + private bool TryPlanSelectionSet( OperationPlanNode operation, SelectionPlanNode parent, - Stack path) + Stack path, + bool skipUnresolved = false) { if (parent.SelectionNodes is null) { @@ -44,15 +49,12 @@ private bool TryResolveSelectionSet( } List? unresolved = null; - CompositeComplexType? type = null; - var areAnySelectionsResolvable = false; + var type = (CompositeComplexType)parent.DeclaringType; foreach (var selection in parent.SelectionNodes) { if (selection is FieldNode fieldNode) { - type ??= (CompositeComplexType)parent.DeclaringType; - if (!type.Fields.TryGetField(fieldNode.Name.Value, out var field)) { throw new InvalidOperationException( @@ -79,7 +81,6 @@ private bool TryResolveSelectionSet( } parent.AddSelection(new FieldPlanNode(fieldNode, field)); - areAnySelectionsResolvable = true; continue; } @@ -99,10 +100,9 @@ private bool TryResolveSelectionSet( path.Push(pathSegment); - if (TryResolveSelectionSet(operation, fieldPlanNode, path)) + if (TryPlanSelectionSet(operation, fieldPlanNode, path)) { parent.AddSelection(fieldPlanNode); - areAnySelectionsResolvable = true; } else { @@ -121,95 +121,136 @@ private bool TryResolveSelectionSet( } } - if (unresolved?.Count > 0) + return skipUnresolved + || unresolved is null + || unresolved.Count == 0 + || TryHandleUnresolvedSelections(operation, parent, type, unresolved, path); + } + + private bool TryHandleUnresolvedSelections( + OperationPlanNode operation, + SelectionPlanNode parent, + CompositeComplexType type, + List unresolved, + Stack path) + { + if (!TryResolveEntityType(parent, out var entityPath)) + { + return false; + } + + // if we have found an entity to branch of from we will check + // if any of the unresolved selections can be resolved through one of the entity lookups. + var processedSchemas = new HashSet(); + var processedFields = new HashSet(); + var fields = new List(); + + // we first try to weight the schemas that the fields can be resolved by. + // The schema is weighted by the fields it potentially can resolve. + var schemasWeighted = GetSchemasWeighted(unresolved, processedSchemas); + + foreach (var schemaName in schemasWeighted.OrderByDescending(t => t.Value).Select(t => t.Key)) { - var current = parent; - var unresolvedPath = new Stack(); - unresolvedPath.Push(parent); - - // first we try to find an entity from which we can branch. - // We go up until we find the first entity. - while (!current.IsEntity - && current.Parent is SelectionPlanNode parentSelection) + if (!processedSchemas.Add(schemaName)) { - current = parentSelection; - unresolvedPath.Push(current); + continue; } - // If we could not find an entity we cannot resolve the unresolved selections. - if (!current.IsEntity) + // if the path is not resolvable we will skip it and move to the next. + if (!IsEntityPathResolvable(entityPath, schemaName)) { - // TODO: there is a case where we do root selections on data, we will ignore it for now. - return false; + continue; } - // if we have found an entity to branch of from we will check - // if any of the unresolved selections can be resolved through one of the entity lookups. - var processed = new HashSet(); + // next we try to find a lookup + if (!TryGetLookup((SelectionPlanNode)entityPath.Peek(), processedSchemas, out var lookup)) + { + continue; + } - // we first try to weight the schemas that the fields can be resolved by. - // The schema is weighted by the fields it potentially can resolve. - var schemasWeighted = GetSchemasWeighted(unresolved, processed); + // note : this can lead to a operation explosions as fields could be unresolvable + // and would be spread out in the lower level call. We do that for now to test out the + // overall concept and will backtrack later to the upper call. + fields.Clear(); - foreach (var schemaName in schemasWeighted.OrderByDescending(t => t.Value).Select(t => t.Key)) + foreach (var unresolvedField in unresolved) { - if (processed.Add(schemaName)) + if (unresolvedField.Field.Sources.ContainsSchema(schemaName) + && !processedFields.Contains(unresolvedField.Field.Name)) { - var isPathResolvable = true; + fields.Add(unresolvedField.FieldNode); + } + } - // a possible schema must be able to resolve the path to the lookup. - foreach (var pathSegment in unresolvedPath.Skip(1)) - { - if (pathSegment is FieldPlanNode selection - && selection.Field is not null - && selection.Field.Sources.ContainsSchema(schemaName)) - { - continue; - } + var lookupOperation = CreateLookupOperation(schemaName, lookup, type, parent, fields); + var lookupField = lookupOperation.Selections[0]; + + // what do we do of its not successful + if (!TryPlanSelectionSet(lookupOperation, lookupField, path)) + { + continue; + } - isPathResolvable = true; + operation.AddOperation(lookupOperation); + + foreach (var selection in lookupField.Selections) + { + switch (selection) + { + case FieldPlanNode field: + processedFields.Add(field.Field.Name); break; - } - // if the path is not resolvable we will skip it and move to the next. - if (!isPathResolvable) - { - continue; - } + default: + throw new NotSupportedException(); + } + } + } - // next we try to find a lookup - if (TryGetLookup(current, processed, out var lookup)) - { - // note : this can lead to a operation explosions as fields could be unresolvable - // and would be spread out in the lower level call. We do that for now to test out the - // overall concept and will backtrack later to the upper call. - var fields = new List(); + return unresolved.Count == processedFields.Count; + } - foreach (var unresolvedField in unresolved) - { - if (unresolvedField.Field.Sources.ContainsSchema(schemaName)) - { - fields.Add(unresolvedField.FieldNode); - } - } + /// + /// Tries to find an entity type in the current selection path. + /// + private static bool TryResolveEntityType( + SelectionPlanNode parent, + [NotNullWhen(true)] out Stack? entityPath) + { + var current = parent; + entityPath = new Stack(); + entityPath.Push(parent); + + // if the current SelectionPlanNode is not an entity we will move up the selection path. + while (current is { IsEntity: false, Parent: SelectionPlanNode parentSelection }) + { + current = parentSelection; + entityPath.Push(current); + } - var newOperation = new OperationPlanNode( - schemaName, - schema.QueryType, - CreateLookupSelections(lookup, parent, fields), - parent); + if (!current.IsEntity) + { + entityPath = null; + return false; + } - // what do we do of its not successful - if (TryResolveSelectionSet(newOperation, newOperation, path)) - { - operation.AddOperation(newOperation); - } - } + return true; + } + + private static bool IsEntityPathResolvable(Stack entityPath, string schemaName) + { + foreach (var planNode in entityPath.Skip(1)) + { + if (planNode is FieldPlanNode fieldPlanNode) + { + if (!fieldPlanNode.Field.Sources.Contains(schemaName)) + { + return false; } } } - return areAnySelectionsResolvable; + return true; } // this needs more meat @@ -226,7 +267,7 @@ private bool TryGetLookup(SelectionPlanNode selection, HashSet schemas, // is available for free. foreach (var schemaName in schemas) { - if (((CompositeObjectType)selection.DeclaringType).Sources.TryGetMember(schemaName, out var source) + if (((CompositeComplexType)selection.DeclaringType).Sources.TryGetType(schemaName, out var source) && source.Lookups.Length > 0) { lookup = source.Lookups[0]; @@ -237,23 +278,40 @@ private bool TryGetLookup(SelectionPlanNode selection, HashSet schemas, throw new NotImplementedException(); } - private IReadOnlyList CreateLookupSelections( + private OperationPlanNode CreateLookupOperation( + string schemaName, Lookup lookup, + CompositeComplexType entityType, SelectionPlanNode parent, IReadOnlyList selections) { - return - [ - new FieldNode( - new NameNode(lookup.Name), - null, - Array.Empty(), - Array.Empty(), - new SelectionSetNode(selections)) - ]; + var lookupFieldNode = new FieldNode( + new NameNode(lookup.Name), + null, + Array.Empty(), + Array.Empty(), + new SelectionSetNode(selections)); + + var selectionNodes = new ISelectionNode[] { lookupFieldNode }; + + var lookupFieldPlan = new FieldPlanNode( + lookupFieldNode, + new OutputFieldInfo( + lookup.Name, + entityType, + ImmutableArray.Empty.Add(schemaName))); + + var lookupOperation = new OperationPlanNode( + schemaName, + schema.QueryType, + selectionNodes, + parent); + + lookupOperation.AddSelection(lookupFieldPlan); + + return lookupOperation; } - private static Dictionary GetSchemasWeighted( IEnumerable unresolvedFields, IEnumerable skipSchemaNames) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationVariableBinder.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationVariableBinder.cs new file mode 100644 index 00000000000..503f8cd7924 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationVariableBinder.cs @@ -0,0 +1,78 @@ +using HotChocolate.Fusion.Planning.Nodes; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Planning; + +internal static class OperationVariableBinder +{ + public static void BindOperationVariables( + OperationDefinitionNode operationDefinition, + RootPlanNode operationPlan) + { + var operationBacklog = new Stack(); + var selectionBacklog = new Stack(); + var variableDefinitions = operationDefinition.VariableDefinitions.ToDictionary(t => t.Variable.Name.Value); + var usedVariables = new HashSet(); + + foreach (var operation in operationPlan.Operations) + { + operationBacklog.Push(operation); + } + + while (operationBacklog.TryPop(out var operation)) + { + CollectAndBindUsedVariables(operation, variableDefinitions, usedVariables, selectionBacklog); + + foreach (var child in operation.Operations) + { + operationBacklog.Push(child); + } + } + } + + private static void CollectAndBindUsedVariables( + OperationPlanNode operation, + Dictionary variableDefinitions, + HashSet usedVariables, + Stack backlog) + { + usedVariables.Clear(); + backlog.Clear(); + backlog.Push(operation); + + while (backlog.TryPop(out var node)) + { + if (node is FieldPlanNode field) + { + foreach (var argument in field.Arguments) + { + if (argument.Value is VariableNode variable) + { + usedVariables.Add(variable.Name.Value); + } + } + + foreach (var directive in field.Directives) + { + foreach (var argument in directive.Arguments) + { + if (argument.Value is VariableNode variable) + { + usedVariables.Add(variable.Name.Value); + } + } + } + } + + foreach (var selection in node.Selections) + { + backlog.Push(selection); + } + } + + foreach (var variable in usedVariables) + { + operation.AddVariableDefinition(variableDefinitions[variable]); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceInterfaceMemberCollection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceInterfaceMemberCollection.cs deleted file mode 100644 index 2c5cd1e48a0..00000000000 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceInterfaceMemberCollection.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace HotChocolate.Fusion.Types.Collections; - -public class SourceInterfaceMemberCollection(IEnumerable members) - : SourceMemberCollection(members); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceMemberCollection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceMemberCollection.cs index 89d4e7d6409..84b36dc1ea0 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceMemberCollection.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceMemberCollection.cs @@ -5,7 +5,7 @@ namespace HotChocolate.Fusion.Types.Collections; -public class SourceMemberCollection : IEnumerable where TMember : ISourceMember +public class SourceMemberCollection : ISourceMemberCollection where TMember : ISourceMember { private readonly FrozenDictionary _members; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectFieldCollection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectFieldCollection.cs index 05aa3d97d23..ba2d5f84933 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectFieldCollection.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectFieldCollection.cs @@ -1,4 +1,4 @@ namespace HotChocolate.Fusion.Types.Collections; -public class SourceObjectFieldCollection(IEnumerable members) - : SourceMemberCollection(members); +public class SourceObjectFieldCollection(IEnumerable members) + : SourceMemberCollection(members); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectTypeCollection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectTypeCollection.cs index ad3713a435f..7ceaf0376c8 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectTypeCollection.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectTypeCollection.cs @@ -1,4 +1,37 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + namespace HotChocolate.Fusion.Types.Collections; public class SourceObjectTypeCollection(IEnumerable members) - : SourceMemberCollection(members); + : SourceMemberCollection(members) + , ISourceComplexTypeCollection + , ISourceComplexTypeCollection +{ + ISourceComplexType ISourceMemberCollection.this[string schemaName] + => this[schemaName]; + + public ImmutableArray Types + => Members; + + ImmutableArray ISourceComplexTypeCollection.Types + => [..Members]; + + public bool TryGetType(string schemaName, [NotNullWhen(true)] out SourceObjectType? type) + => TryGetMember(schemaName, out type); + + public bool TryGetType(string schemaName, [NotNullWhen(true)] out ISourceComplexType? type) + { + if(TryGetMember(schemaName, out var member)) + { + type = member; + return true; + } + + type = null; + return false; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Completion/CompositeSchemaBuilder.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Completion/CompositeSchemaBuilder.cs index ae4c8612e45..efd6663c341 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Completion/CompositeSchemaBuilder.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Completion/CompositeSchemaBuilder.cs @@ -219,12 +219,12 @@ private static SourceObjectFieldCollection BuildSourceObjectFieldCollection( { var fieldDirectives = FieldDirectiveParser.Parse(fieldDef.Directives); var requireDirectives = RequiredDirectiveParser.Parse(fieldDef.Directives); - var temp = ImmutableArray.CreateBuilder(); + var temp = ImmutableArray.CreateBuilder(); foreach (var fieldDirective in fieldDirectives) { temp.Add( - new SourceObjectField( + new SourceOutputField( fieldDirective.SourceName ?? field.Name, fieldDirective.SchemaName, ParseRequirements(requireDirectives, fieldDirective.SchemaName), diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeComplexType.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeComplexType.cs index 68c24e4736d..f9e4248dff8 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeComplexType.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeComplexType.cs @@ -10,6 +10,7 @@ public abstract class CompositeComplexType : ICompositeNamedType { private DirectiveCollection _directives = default!; private CompositeInterfaceTypeCollection _implements = default!; + private ISourceComplexTypeCollection _sources = default!; private bool _completed; protected CompositeComplexType( @@ -69,6 +70,27 @@ private protected set /// public CompositeOutputFieldCollection Fields { get; } + /// + /// Gets the source type definition of this type. + /// + /// + /// The source type definition of this type. + /// + public ISourceComplexTypeCollection Sources + { + get => _sources; + private protected set + { + if (_completed) + { + throw new NotSupportedException( + "The type definition is sealed and cannot be modified."); + } + + _sources = value; + } + } + private protected void Complete() { if (_completed) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeObjectType.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeObjectType.cs index 6ac611f2c3a..bb2e7cddb5c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeObjectType.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeObjectType.cs @@ -12,7 +12,7 @@ public sealed class CompositeObjectType( { public override TypeKind Kind => TypeKind.Object; - public SourceObjectTypeCollection Sources { get; private set; } = default!; + public new ISourceComplexTypeCollection Sources { get; private set; } = default!; public bool IsEntity { get; private set; } @@ -21,7 +21,8 @@ internal void Complete(CompositeObjectTypeCompletionContext context) Directives = context.Directives; Implements = context.Interfaces; Sources = context.Sources; - IsEntity = context.Sources.Any(t => t.Lookups.Length > 0); + base.Sources = context.Sources; + IsEntity = Sources.Any(t => t.Lookups.Length > 0); base.Complete(); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Contracts/Is.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Contracts/Is.cs new file mode 100644 index 00000000000..ba180f41d71 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Contracts/Is.cs @@ -0,0 +1,28 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +namespace HotChocolate.Fusion.Types; + +public interface ISourceMemberCollection : IEnumerable where TMember : ISourceMember +{ + int Count { get; } + + TMember this[string schemaName] { get; } + + bool ContainsSchema(string schemaName); + + ImmutableArray Schemas { get; } +} + +public interface ISourceComplexTypeCollection : ISourceMemberCollection where TType : ISourceComplexType +{ + bool TryGetType(string schemaName, [NotNullWhen(true)] out TType? type); + + ImmutableArray Types { get; } +} + + + + + + diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceInterfaceField.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceInterfaceField.cs deleted file mode 100644 index 67d4a3c2217..00000000000 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceInterfaceField.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace HotChocolate.Fusion.Types; - -public class SourceInterfaceField( - string name, - string schemaName, - ICompositeType type) - : ISourceMember -{ - public string Name { get; } = name; - - public string SchemaName { get; } = schemaName; - - public ICompositeType Type { get; } = type; -} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceObjectField.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceOutputField.cs similarity index 91% rename from src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceObjectField.cs rename to src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceOutputField.cs index 3761c06d6b8..bf629016687 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceObjectField.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceOutputField.cs @@ -1,6 +1,6 @@ namespace HotChocolate.Fusion.Types; -public sealed class SourceObjectField( +public sealed class SourceOutputField( string name, string schemaName, FieldRequirements? requirements, diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs index 9e0dced1b9f..329089967ca 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs @@ -39,7 +39,7 @@ await Assert .IsEqualTo( """ { - productById { + productById(id: 1) { id name } @@ -81,7 +81,7 @@ await Assert .IsEqualTo( """ { - productById { + productById(id: 1) { id name } @@ -89,7 +89,155 @@ await Assert { productById { - estimatedDelivery + estimatedDelivery(postCode: "12345") + } + } + """); + } + + [Test] + public async Task Plan_Simple_Operation_3_Source_Schema() + { + var compositeSchemaDoc = Utf8GraphQLParser.Parse(FileResource.Open("fusion1.graphql")); + var compositeSchema = CompositeSchemaBuilder.Create(compositeSchemaDoc); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + ... ProductCard + } + } + + fragment ProductCard on Product { + name + reviews(first: 10) { + nodes { + ... ReviewCard + } + } + } + + fragment ReviewCard on Review { + body + stars + author { + ... AuthorCard + } + } + + fragment AuthorCard on UserProfile { + displayName + } + """); + + var rewriter = new InlineFragmentOperationRewriter(compositeSchema); + var rewritten = rewriter.RewriteDocument(doc, null); + + // act + var planner = new OperationPlanner(compositeSchema); + var plan = planner.CreatePlan(rewritten, null); + + // assert + await Assert + .That(plan.ToSyntaxNode().ToString(indented: true)) + .IsEqualTo( + """ + { + productById(id: 1) { + name + } + } + + { + productById { + reviews(first: 10) { + nodes { + body + stars + author + } + } + } + } + + { + userById { + displayName + } + } + """); + } + + [Test] + public async Task Plan_Simple_Operation_3_Source_Schema_And_Single_Variable() + { + var compositeSchemaDoc = Utf8GraphQLParser.Parse(FileResource.Open("fusion1.graphql")); + var compositeSchema = CompositeSchemaBuilder.Create(compositeSchemaDoc); + + var doc = Utf8GraphQLParser.Parse( + """ + query GetProduct($id: ID!, $first: Int! = 10) { + productById(id: $id) { + ... ProductCard + } + } + + fragment ProductCard on Product { + name + reviews(first: $first) { + nodes { + ... ReviewCard + } + } + } + + fragment ReviewCard on Review { + body + stars + author { + ... AuthorCard + } + } + + fragment AuthorCard on UserProfile { + displayName + } + """); + + var rewriter = new InlineFragmentOperationRewriter(compositeSchema); + var rewritten = rewriter.RewriteDocument(doc, null); + + // act + var planner = new OperationPlanner(compositeSchema); + var plan = planner.CreatePlan(rewritten, null); + + // assert + await Assert + .That(plan.ToSyntaxNode().ToString(indented: true)) + .IsEqualTo( + """ + query($id: ID!) { + productById(id: $id) { + name + } + } + + query($first: Int! = 10) { + productById { + reviews(first: $first) { + nodes { + body + stars + author + } + } + } + } + + { + userById { + displayName } } """); diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/fusion1.graphql b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/fusion1.graphql index 0f530f27e0a..46b412e9e08 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/fusion1.graphql +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/fusion1.graphql @@ -1,54 +1,184 @@ -type Query { - productById(id: ID!): Product - @fusion__field(schema: PRODUCTS) +type Query + @fusion__type(schema: ACCOUNTS) + @fusion__type(schema: PRODUCTS) { + viewer: UserProfile + @fusion__field(schema: ACCOUNTS) + productById(id: ID!): Product + @fusion__field(schema: PRODUCTS) + products(first: Int, after: String, last: Int, before: String): ProductConnection + @fusion__field(schema: PRODUCTS) } type Product - @fusion__type(schema: PRODUCTS) - @fusion__type(schema: SHIPPING) - @fusion__lookup( - schema: PRODUCTS - key: "{ id }" - field: "productById(id: ID!): Product" - map: ["id"] - ) - @fusion__lookup( + @fusion__type(schema: PRODUCTS) + @fusion__type(schema: SHIPPING) + @fusion__type(schema: REVIEWS) + @fusion__lookup( + schema: PRODUCTS + key: "{ id }" + field: "productById(id: ID!): Product" + map: ["id"] + ) + @fusion__lookup( + schema: SHIPPING + key: "{ id }" + field: "productById(id: ID!): Product" + map: ["id"] + ) + @fusion__lookup( + schema: REVIEWS + key: "{ id }" + field: "productById(id: ID!): Product" + map: ["id"] + ) { + id: ID! + @fusion__field(schema: PRODUCTS) + @fusion__field(schema: SHIPPING) + name: String! + @fusion__field(schema: PRODUCTS) + description: String + @fusion__field(schema: PRODUCTS) + price: Float! + @fusion__field(schema: PRODUCTS) + dimension: ProductDimension! + @fusion__field(schema: PRODUCTS) + reviews(first: Int, after: String, last: Int, before: String): ProductReviewConnection + @fusion__field(schema: REVIEWS) + estimatedDelivery(postCode: String): Int! + @fusion__field(schema: SHIPPING) + @fusion__requires( schema: SHIPPING - key: "{ id }" - field: "productById(id: ID!): Product" - map: ["id"] - ) { - id: ID! - @fusion__field(schema: PRODUCTS) - @fusion__field(schema: SHIPPING) - name: String! - @fusion__field(schema: PRODUCTS) - description: String - @fusion__field(schema: PRODUCTS) - price: Float! - @fusion__field(schema: PRODUCTS) - dimension: ProductDimension! - @fusion__field(schema: PRODUCTS) - estimatedDelivery(postCode: String): Int! - @fusion__field(schema: SHIPPING) - @fusion__requires( - schema: SHIPPING - field: "estimatedDelivery(postCode: String, height: Int!, width: Int!): Int!" - map: ["dimension.height", "dimension.width"] - ) + field: "estimatedDelivery(postCode: String, height: Int!, width: Int!): Int!" + map: ["dimension.height", "dimension.width"] + ) } type ProductDimension - @fusion__type(schema: PRODUCTS) { - height: Int! - @fusion__field(schema: PRODUCTS) - width: Int! - @fusion__field(schema: PRODUCTS) + @fusion__type(schema: PRODUCTS) { + height: Int! + @fusion__field(schema: PRODUCTS) + width: Int! + @fusion__field(schema: PRODUCTS) +} + +type Review + @fusion__type(schema: REVIEWS) + @fusion__lookup( + schema: REVIEWS + key: "{ id }" + field: "reviewById(id: ID!): Review" + map: ["id"] + ) { + id: ID! + @fusion__field(schema: REVIEWS) + body: String! + @fusion__field(schema: REVIEWS) + stars: Int! + @fusion__field(schema: REVIEWS) + author: UserProfile + @fusion__field(schema: REVIEWS) +} + +type UserProfile + @fusion__type(schema: REVIEWS) + @fusion__type(schema: ACCOUNTS) + @fusion__lookup( + schema: REVIEWS + key: "{ id }" + field: "authorById(id: ID!): UserProfile" + map: ["id"] + ) + @fusion__lookup( + schema: ACCOUNTS + key: "{ id }" + field: "userById(id: ID!): UserProfile" + map: ["id"] + ) { + id: ID! + @fusion__field(schema: ACCOUNTS) + @fusion__field(schema: REVIEWS) + displayName: String! + @fusion__field(schema: ACCOUNTS) + reviews(first: Int, after: String, last: Int, before: String): UserProfileReviewConnection + @fusion__field(schema: REVIEWS) +} + +type ProductReviewConnection + @fusion__type(schema: REVIEWS) { + pageInfo: PageInfo! + @fusion__field(schema: REVIEWS) + edges: [ProductReviewEdge!] + @fusion__field(schema: REVIEWS) + nodes: [Review!] + @fusion__field(schema: REVIEWS) +} + +type ProductReviewEdge + @fusion__type(schema: REVIEWS) { + cursor: String! + @fusion__field(schema: REVIEWS) + node: Review! + @fusion__field(schema: REVIEWS) +} + +type UserProfileReviewConnection + @fusion__type(schema: REVIEWS) { + pageInfo: PageInfo! + @fusion__field(schema: REVIEWS) + edges: [UserProfileReviewEdge!] + @fusion__field(schema: REVIEWS) + nodes: [Review!] + @fusion__field(schema: REVIEWS) +} + +type UserProfileReviewEdge + @fusion__type(schema: REVIEWS) { + cursor: String! + @fusion__field(schema: REVIEWS) + node: Review! + @fusion__field(schema: REVIEWS) +} + +type ProductConnection + @fusion__type(schema: PRODUCTS) { + pageInfo: PageInfo! + @fusion__field(schema: PRODUCTS) + edges: [ProductEdge!] + @fusion__field(schema: PRODUCTS) + nodes: [Product!] + @fusion__field(schema: PRODUCTS) +} + +type ProductEdge + @fusion__type(schema: PRODUCTS) { + cursor: String! + @fusion__field(schema: PRODUCTS) + node: Product! + @fusion__field(schema: PRODUCTS) +} + +type PageInfo + @fusion__type(schema: PRODUCTS) + @fusion__type(schema: REVIEWS) { + hasNextPage: Boolean! + @fusion__field(schema: PRODUCTS) + @fusion__field(schema: REVIEWS) + hasPreviousPage: Boolean! + @fusion__field(schema: PRODUCTS) + @fusion__field(schema: REVIEWS) + startCursor: String + @fusion__field(schema: PRODUCTS) + @fusion__field(schema: REVIEWS) + endCursor: String + @fusion__field(schema: PRODUCTS) + @fusion__field(schema: REVIEWS) } enum fusion__Schema { PRODUCTS SHIPPING + REVIEWS + ACCOUNTS } scalar fusion__FieldDefinition