Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ internal static CompositionSettings CreateCompositionSettings(
{
CacheControlMergeBehavior = settings.CacheControlMergeBehavior,
EnableGlobalObjectIdentification = settings.EnableGlobalObjectIdentification,
AddNodesField = settings.AddNodesField,
NodeResolution = settings.NodeResolution,
TagMergeBehavior = settings.TagMergeBehavior
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public struct GraphQLCompositionSettings
/// </summary>
public bool? EnableGlobalObjectIdentification { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the plural Global Object Identification field is added.
/// </summary>
public bool? AddNodesField { get; set; }

/// <summary>
/// Gets or sets how the gateway resolves the <c>Query.node</c> field.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public sealed class SourceSchemaMergerOptions
/// </summary>
public bool EnableGlobalObjectIdentification { get; set; }

/// <summary>
/// Adds the plural <c>Query.nodes(ids: [ID!]!): [Node]!</c> gateway field when Global Object
/// Identification is enabled.
/// </summary>
public bool AddNodesField { get; set; }

/// <summary>
/// Defines how the gateway resolves the <c>Query.node</c> field.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ public CompositionResult<MutableSchemaDefinition> Compose()
"Source-schema node resolution requires global object identification to be enabled.");
}

if (_schemaComposerOptions.Merger.NodeResolution is NodeResolution.SourceSchema
&& _schemaComposerOptions.Merger.AddNodesField)
{
return InvalidNodeResolution(
"The nodes field requires gateway node resolution.");
}

if (!Enum.IsDefined(
_schemaComposerOptions.ApolloFederationCompatibility
.ShareableFieldRuntimeTypeRouting))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ internal sealed record MergerSettings

public bool? EnableGlobalObjectIdentification { get; set; }

public bool? AddNodesField { get; set; }

public NodeResolution? NodeResolution { get; set; }

public bool? RemoveUnreferencedDefinitions { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public CompositionSettings MergeInto(CompositionSettings settings)
EnableGlobalObjectIdentification =
compositionSettings.Merger.EnableGlobalObjectIdentification
?? settings.Merger.EnableGlobalObjectIdentification,
AddNodesField =
compositionSettings.Merger.AddNodesField
?? settings.Merger.AddNodesField,
NodeResolution =
compositionSettings.Merger.NodeResolution
?? settings.Merger.NodeResolution,
Expand Down Expand Up @@ -113,6 +116,11 @@ public SourceSchemaMergerOptions ToOptions()
mergerOptions.EnableGlobalObjectIdentification = enableGlobalObjectIdentification;
}

if (mergerSettings.AddNodesField is { } addNodesField)
{
mergerOptions.AddNodesField = addNodesField;
}

if (mergerSettings.NodeResolution is { } nodeResolution)
{
mergerOptions.NodeResolution = nodeResolution;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,21 @@ private void AddNodeField(MutableSchemaDefinition mergedSchema)

queryType.Fields.Add(canonicalNodeField);
}

if (_options.AddNodesField && !queryType.Fields.ContainsName(FieldNames.Nodes))
{
var canonicalNodesField = new MutableOutputFieldDefinition(
FieldNames.Nodes,
new NonNullType(new ListType(nodeType)));
canonicalNodesField.Arguments.Add(
new MutableInputFieldDefinition(
ArgumentNames.Ids,
new NonNullType(new ListType(new NonNullType(idType)))));
canonicalNodesField.Directives.Add(
new Directive(_fusionDirectiveDefinitions[DirectiveNames.FusionGatewayField]));

queryType.Fields.Add(canonicalNodesField);
}
}
}

Expand All @@ -358,8 +373,16 @@ private static bool IsGoiNodesField(
IInterfaceTypeDefinition nodeType,
IScalarTypeDefinition idType)
{
var listType = field.Type switch
{
ListType list => list,
NonNullType { NullableType: ListType list } => list,
_ => null
};

if (field.Name != FieldNames.Nodes
|| field.Type.NamedType() != nodeType
|| listType is null
|| listType.ElementType.NamedType() != nodeType
|| field.Arguments.Count != 1
|| !field.Arguments.TryGetField(ArgumentNames.Ids, out var argument))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ internal sealed class ExecutePlanNodeSpan(
[ExecutionNodeType.Operation] = GraphQL.Operation.Step.KindValues.Operation,
[ExecutionNodeType.OperationBatch] = GraphQL.Operation.Step.KindValues.OperationBatch,
[ExecutionNodeType.Introspection] = GraphQL.Operation.Step.KindValues.Introspection,
[ExecutionNodeType.Node] = GraphQL.Operation.Step.KindValues.Node
[ExecutionNodeType.Node] = GraphQL.Operation.Step.KindValues.Node,
[ExecutionNodeType.Nodes] = GraphQL.Operation.Step.KindValues.Node
}.ToFrozenDictionary();

public static ExecutePlanNodeSpan? Start(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ protected void EnqueueDependentForExecution(OperationPlanContext context, Execut
context.EnqueueForExecution(this, dependent);
}

protected void BeginDependentSelection(OperationPlanContext context)
{
ArgumentNullException.ThrowIfNull(context);
context.BeginDependentSelection(this);
}

internal void AddDependency(IOperationPlanNode node)
{
ExpectMutable();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ public enum ExecutionNodeType
OperationBatch,
EventStream,
Introspection,
Node
Node,
Nodes
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using HotChocolate.Language;

namespace HotChocolate.Fusion.Execution.Nodes;

/// <summary>
/// Dispatches a plural Global Object Identification field to one variable-batched branch per
/// concrete object type.
/// </summary>
public sealed class NodesFieldExecutionNode : ExecutionNode
{
internal const string IdVariableName = "__fusion_nodes_id";

private readonly Dictionary<string, ExecutionNode> _branches = [];
private readonly string _responseName;
private readonly IValueNode _idsValue;
private readonly ExecutionNodeCondition[] _conditions;

internal NodesFieldExecutionNode(
int id,
string responseName,
IValueNode idsValue,
ExecutionNodeCondition[] conditions)
{
Id = id;
_responseName = responseName;
_idsValue = idsValue;
_conditions = conditions;
}

public override int Id { get; }

public override ExecutionNodeType Type => ExecutionNodeType.Nodes;

public override ReadOnlySpan<ExecutionNodeCondition> Conditions => _conditions;

public override string? SchemaName => null;

public Dictionary<string, ExecutionNode> Branches => _branches;

public string ResponseName => _responseName;

public IValueNode IdsValue => _idsValue;

protected override ValueTask<ExecutionStatus> OnExecuteAsync(
OperationPlanContext context,
CancellationToken cancellationToken = default)
{
var ids = GetIds(context);
var groups = new Dictionary<string, List<NodeIdValue>>(StringComparer.Ordinal);

context.InitializeNodesResult(_responseName, ids.Count);
BeginDependentSelection(context);

for (var i = 0; i < ids.Count; i++)
{
var id = ids[i];

if (!context.TryParseTypeNameFromId(id, out var typeName)
|| !_branches.ContainsKey(typeName))
{
context.AddNodesError(
_responseName,
i,
ErrorHelper.InvalidNodeIdFormat(id));
continue;
}

if (!groups.TryGetValue(typeName, out var group))
{
group = [];
groups.Add(typeName, group);
}

group.Add(new NodeIdValue(i, id));
}

foreach (var (typeName, values) in groups)
{
var branch = _branches[typeName];
var variables = context.CreateNodesVariableValueSets(
_responseName,
IdVariableName,
values);
context.SetDynamicVariableValueSets(branch, variables);
EnqueueDependentForExecution(context, branch);
}

return ValueTask.FromResult(ExecutionStatus.Success);
}

internal void AddBranch(string objectTypeName, ExecutionNode node)
{
ArgumentException.ThrowIfNullOrEmpty(objectTypeName);
ArgumentNullException.ThrowIfNull(node);
ExpectMutable();
_branches[objectTypeName] = node;
}

private IReadOnlyList<string> GetIds(OperationPlanContext context)
{
IValueNode? value = _idsValue;

if (value is VariableNode variable)
{
if (!context.Variables.TryGetValue(variable.Name.Value, out value) || value is null)
{
throw new InvalidOperationException(
$"Expected to find a value for variable '{variable.Name.Value}'.");
}
}

if (value is ListValueNode list)
{
var values = new string[list.Items.Count];
for (var i = 0; i < list.Items.Count; i++)
{
values[i] = GetIdValue(list.Items[i]);
}

return values;
}

return [GetIdValue(value)];

static string GetIdValue(IValueNode idValue)
=> idValue switch
{
StringValueNode stringValue => stringValue.Value,
IntValueNode intValue => intValue.Value,
_ => throw new InvalidOperationException("Expected an ID value.")
};
}
}

internal readonly record struct NodeIdValue(int Index, string Id);
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ protected override async ValueTask<ExecutionStatus> OnExecuteAsync(
CancellationToken cancellationToken = default)
{
var diagnosticEvents = context.DiagnosticEvents;
var variables = context.CreateVariableValueSets(_target, _forwardedVariables, _requirements);
var variables = context.TryGetDynamicVariableValueSets(this, out var dynamicVariables)
? dynamicVariables
: context.CreateVariableValueSets(_target, _forwardedVariables, _requirements);

if (variables.Length == 0 && (_requirements.Length > 0 || _forwardedVariables.Length > 0))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ private static void WriteNodes(
case NodeFieldExecutionNode nodeExecutionNode:
WriteNodeFieldNode(jsonWriter, operation, nodeExecutionNode, nodeTrace);
break;

case NodesFieldExecutionNode nodesExecutionNode:
WriteNodesFieldNode(jsonWriter, operation, nodesExecutionNode, nodeTrace);
break;
}
}

Expand Down Expand Up @@ -1120,6 +1124,36 @@ private static void WriteNodeFieldNode(
jsonWriter.WriteEndObject();
}

private static void WriteNodesFieldNode(
JsonWriter jsonWriter,
Operation operation,
NodesFieldExecutionNode node,
ExecutionNodeTrace? trace)
{
jsonWriter.WriteStartObject();
jsonWriter.WritePropertyName("id");
jsonWriter.WriteNumberValue(node.Id);
jsonWriter.WritePropertyName("type");
jsonWriter.WriteStringValue(node.Type.ToString());
jsonWriter.WritePropertyName("idsValue");
jsonWriter.WriteStringValue(node.IdsValue.ToString());
jsonWriter.WritePropertyName("responseName");
jsonWriter.WriteStringValue(node.ResponseName);
jsonWriter.WritePropertyName("branches");
jsonWriter.WriteStartObject();

foreach (var branch in node.Branches.OrderBy(kvp => kvp.Key))
{
jsonWriter.WritePropertyName(branch.Key);
jsonWriter.WriteNumberValue(branch.Value.Id);
}

jsonWriter.WriteEndObject();
TryWriteConditions(jsonWriter, node);
TryWriteNodeTrace(jsonWriter, operation, trace);
jsonWriter.WriteEndObject();
}

private static void TryWriteNodeTrace(JsonWriter jsonWriter, Operation operation, ExecutionNodeTrace? trace)
{
if (trace is not null)
Expand Down
Loading
Loading