Skip to content

Commit

Permalink
[Experimental] Adds True Nullability GraphQL Spec Proposal (#6570)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib authored Oct 2, 2023
1 parent b1dc205 commit c195ced
Show file tree
Hide file tree
Showing 30 changed files with 571 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,9 @@ public static class WellKnownContextData
/// The key to access the authorization allowed flag on the member context.
/// </summary>
public const string AllowAnonymous = "HotChocolate.Authorization.AllowAnonymous";

/// <summary>
/// The key to access the true nullability flag on the execution context.
/// </summary>
public const string EnableTrueNullability = "HotChocolate.Types.EnableTrueNullability";
}
4 changes: 4 additions & 0 deletions src/HotChocolate/Core/src/Abstractions/WellKnownDirectives.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,8 @@ public static class WellKnownDirectives
/// The name of the @tag argument name.
/// </summary>
public const string Name = "name";

public const string NullBubbling = "nullBubbling";

public const string Enable = "enable";
}
18 changes: 18 additions & 0 deletions src/HotChocolate/Core/src/Execution/ErrorHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -249,4 +249,22 @@ public static IError ReadPersistedQueryMiddleware_PersistedQueryNotFound()
.SetMessage("PersistedQueryNotFound")
.SetCode(ErrorCodes.Execution.PersistedQueryNotFound)
.Build();

public static IError NoNullBubbling_ArgumentValue_NotAllowed(
ArgumentNode argument)
{
var errorBuilder = ErrorBuilder.New();

if (argument.Value.Location is not null)
{
errorBuilder.AddLocation(
argument.Value.Location.Line,
argument.Value.Location.Column);
}

errorBuilder.SetSyntaxNode(argument.Value);
errorBuilder.SetMessage(ErrorHelper_NoNullBubbling_ArgumentValue_NotAllowed);

return errorBuilder.Build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,39 @@
using HotChocolate.Execution.Processing;
using HotChocolate.Language;
using HotChocolate.Types;
using HotChocolate.Utilities;
using Microsoft.Extensions.ObjectPool;
using static HotChocolate.Execution.ErrorHelper;
using static HotChocolate.WellKnownDirectives;
using static HotChocolate.Execution.Pipeline.PipelineTools;
using static HotChocolate.WellKnownContextData;

namespace HotChocolate.Execution.Pipeline;

internal sealed class OperationResolverMiddleware
{
private readonly RequestDelegate _next;
private readonly ObjectPool<OperationCompiler> _operationCompilerPool;
private readonly VariableCoercionHelper _coercionHelper;
private readonly IReadOnlyList<IOperationCompilerOptimizer>? _optimizers;

public OperationResolverMiddleware(
RequestDelegate next,
ObjectPool<OperationCompiler> operationCompilerPool,
IEnumerable<IOperationCompilerOptimizer> optimizers)
IEnumerable<IOperationCompilerOptimizer> optimizers,
VariableCoercionHelper coercionHelper)
{
if (optimizers is null)
{
throw new ArgumentNullException(nameof(optimizers));
}

_next = next ?? throw new ArgumentNullException(nameof(next));
_operationCompilerPool = operationCompilerPool;
_next = next ??
throw new ArgumentNullException(nameof(next));
_operationCompilerPool = operationCompilerPool ??
throw new ArgumentNullException(nameof(operationCompilerPool));
_coercionHelper = coercionHelper ??
throw new ArgumentNullException(nameof(coercionHelper));
_optimizers = optimizers.ToArray();
}

Expand All @@ -45,7 +56,7 @@ public async ValueTask InvokeAsync(IRequestContext context)

if (operationType is null)
{
context.Result = ErrorHelper.RootTypeNotFound(operationDef.Operation);
context.Result = RootTypeNotFound(operationDef.Operation);
return;
}

Expand All @@ -61,7 +72,7 @@ public async ValueTask InvokeAsync(IRequestContext context)
}
else
{
context.Result = ErrorHelper.StateInvalidForOperationResolver();
context.Result = StateInvalidForOperationResolver();
}
}

Expand All @@ -78,7 +89,8 @@ private IOperation CompileOperation(
operationType,
context.Document!,
context.Schema,
_optimizers);
_optimizers,
IsNullBubblingEnabled(context, operationDefinition));
_operationCompilerPool.Return(compiler);
return operation;
}
Expand All @@ -93,4 +105,60 @@ private IOperation CompileOperation(
OperationType.Subscription => schema.SubscriptionType,
_ => throw ThrowHelper.RootTypeNotSupported(operationType)
};
}

private bool IsNullBubblingEnabled(IRequestContext context, OperationDefinitionNode operationDefinition)
{
if (!context.Schema.ContextData.ContainsKey(EnableTrueNullability) ||
operationDefinition.Directives.Count == 0)
{
return true;
}

var enabled = true;

for (var i = 0; i < operationDefinition.Directives.Count; i++)
{
var directive = operationDefinition.Directives[i];

if (!directive.Name.Value.EqualsOrdinal(NullBubbling))
{
continue;
}

for (var j = 0; j < directive.Arguments.Count; j++)
{
var argument = directive.Arguments[j];

if (argument.Name.Value.EqualsOrdinal(Enable))
{
if (argument.Value is BooleanValueNode b)
{
enabled = b.Value;
break;
}

if (argument.Value is VariableNode v)
{
enabled = CoerceVariable(context, operationDefinition, v);
break;
}

throw new GraphQLException(NoNullBubbling_ArgumentValue_NotAllowed(argument));
}
}

break;
}

return enabled;
}

private bool CoerceVariable(
IRequestContext context,
OperationDefinitionNode operationDefinition,
VariableNode variable)
{
var variables = CoerceVariables(context, _coercionHelper, operationDefinition.VariableDefinitions);
return variables.GetVariable<bool>(variable.Name.Value);
}
}
6 changes: 4 additions & 2 deletions src/HotChocolate/Core/src/Execution/Pipeline/PipelineTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,20 @@ public static string CreateCacheId(
string? operationName)
=> CreateCacheId(context, CreateOperationId(documentId, operationName));

public static void CoerceVariables(
public static IVariableValueCollection CoerceVariables(
IRequestContext context,
VariableCoercionHelper coercionHelper,
IReadOnlyList<VariableDefinitionNode> variableDefinitions)
{
if (context.Variables is not null)
{
return;
return context.Variables;
}

if (variableDefinitions.Count == 0)
{
context.Variables = _noVariables;
return _noVariables;
}
else
{
Expand All @@ -52,6 +53,7 @@ public static void CoerceVariables(
coercedValues);

context.Variables = new VariableValueCollection(coercedValues);
return context.Variables;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,11 @@ namespace HotChocolate.Execution.Processing;

public sealed partial class OperationCompiler
{
internal sealed class CompilerContext
internal sealed class CompilerContext(ISchema schema, DocumentNode document, bool disableNullBubbling)
{
public CompilerContext(ISchema schema, DocumentNode document)
{
Schema = schema;
Document = document;
}

public ISchema Schema { get; }
public ISchema Schema { get; } = schema;

public DocumentNode Document { get; }
public DocumentNode Document { get; } = document;

public ObjectType Type { get; private set; } = default!;

Expand All @@ -35,6 +29,8 @@ public CompilerContext(ISchema schema, DocumentNode document)

public IImmutableList<ISelectionSetOptimizer> Optimizers { get; private set; } =
ImmutableList<ISelectionSetOptimizer>.Empty;

public bool EnableNullBubbling { get; } = disableNullBubbling;

public void Initialize(
ObjectType type,
Expand Down
55 changes: 31 additions & 24 deletions src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public sealed partial class OperationCompiler
{
private static readonly ImmutableList<ISelectionSetOptimizer> _emptyOptimizers =
ImmutableList<ISelectionSetOptimizer>.Empty;

private readonly InputParser _parser;
private readonly CreateFieldPipeline _createFieldPipeline;
private readonly Stack<BacklogItem> _backlog = new();
Expand Down Expand Up @@ -65,7 +66,8 @@ public IOperation Compile(
ObjectType operationType,
DocumentNode document,
ISchema schema,
IReadOnlyList<IOperationCompilerOptimizer>? optimizers = null)
IReadOnlyList<IOperationCompilerOptimizer>? optimizers = null,
bool enableNullBubbling = true)
{
if (string.IsNullOrEmpty(operationId))
{
Expand Down Expand Up @@ -112,7 +114,7 @@ public IOperation Compile(
var variants = GetOrCreateSelectionVariants(id);
SelectionSetInfo[] infos = { new(operationDefinition.SelectionSet, 0) };

var context = new CompilerContext(schema, document);
var context = new CompilerContext(schema, document, enableNullBubbling);
context.Initialize(operationType, variants, infos, rootPath, rootOptimizers);
CompileSelectionSet(context);

Expand Down Expand Up @@ -378,19 +380,19 @@ private void CompleteSelectionSet(CompilerContext context)
// For now we only allow streams on lists of composite types.
if (selection.SyntaxNode.IsStreamable())
{
var streamDirective = selection.SyntaxNode.GetStreamDirectiveNode();
var nullValue = NullValueNode.Default;
var ifValue = streamDirective?.GetIfArgumentValueOrDefault() ?? nullValue;
long ifConditionFlags = 0;

if (ifValue.Kind is not SyntaxKind.NullValue)
{
var ifCondition = new IncludeCondition(ifValue, nullValue);
ifConditionFlags = GetSelectionIncludeCondition(ifCondition, 0);
}

selection.MarkAsStream(ifConditionFlags);
_hasIncrementalParts = true;
var streamDirective = selection.SyntaxNode.GetStreamDirectiveNode();
var nullValue = NullValueNode.Default;
var ifValue = streamDirective?.GetIfArgumentValueOrDefault() ?? nullValue;
long ifConditionFlags = 0;

if (ifValue.Kind is not SyntaxKind.NullValue)
{
var ifCondition = new IncludeCondition(ifValue, nullValue);
ifConditionFlags = GetSelectionIncludeCondition(ifCondition, 0);
}

selection.MarkAsStream(ifConditionFlags);
_hasIncrementalParts = true;
}
}

Expand Down Expand Up @@ -449,21 +451,21 @@ private void ResolveFields(
case SyntaxKind.Field:
ResolveField(
context,
(FieldNode)selection,
(FieldNode) selection,
includeCondition);
break;

case SyntaxKind.InlineFragment:
ResolveInlineFragment(
context,
(InlineFragmentNode)selection,
(InlineFragmentNode) selection,
includeCondition);
break;

case SyntaxKind.FragmentSpread:
ResolveFragmentSpread(
context,
(FragmentSpreadNode)selection,
(FragmentSpreadNode) selection,
includeCondition);
break;
}
Expand All @@ -481,7 +483,9 @@ private void ResolveField(

if (context.Type.Fields.TryGetField(fieldName, out var field))
{
var fieldType = field.Type.RewriteNullability(selection.Required);
var fieldType = context.EnableNullBubbling
? field.Type.RewriteNullability(selection.Required)
: field.Type.RewriteToNullableType();

if (context.Fields.TryGetValue(responseName, out var preparedSelection))
{
Expand Down Expand Up @@ -516,7 +520,9 @@ selection.SelectionSet is not null
responseName: responseName,
isParallelExecutable: field.IsParallelExecutable,
arguments: CoerceArgumentValues(field, selection, responseName),
includeConditions: includeCondition == 0 ? null : new[] { includeCondition });
includeConditions: includeCondition == 0
? null
: new[] { includeCondition });

context.Fields.Add(responseName, preparedSelection);

Expand Down Expand Up @@ -586,6 +592,7 @@ private void ResolveFragment(
var ifValue = deferDirective?.GetIfArgumentValueOrDefault() ?? nullValue;

long ifConditionFlags = 0;

if (ifValue.Kind is not SyntaxKind.NullValue)
{
var ifCondition = new IncludeCondition(ifValue, nullValue);
Expand Down Expand Up @@ -637,8 +644,8 @@ private static bool DoesTypeApply(IType typeCondition, IObjectType current)
=> typeCondition.Kind switch
{
TypeKind.Object => ReferenceEquals(typeCondition, current),
TypeKind.Interface => current.IsImplementing((InterfaceType)typeCondition),
TypeKind.Union => ((UnionType)typeCondition).Types.ContainsKey(current.Name),
TypeKind.Interface => current.IsImplementing((InterfaceType) typeCondition),
TypeKind.Union => ((UnionType) typeCondition).Types.ContainsKey(current.Name),
_ => false
};

Expand Down Expand Up @@ -789,7 +796,7 @@ private CompilerContext RentContext(CompilerContext context)
{
if (_deferContext is null)
{
return new CompilerContext(context.Schema, context.Document);
return new CompilerContext(context.Schema, context.Document, context.EnableNullBubbling);
}

var temp = _deferContext;
Expand Down Expand Up @@ -862,4 +869,4 @@ public override bool Equals(object? obj)
public override int GetHashCode()
=> HashCode.Combine(SelectionSet, Path);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/HotChocolate/Core/src/Execution/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -333,4 +333,7 @@
<data name="ComplexityAnalyzerCompiler_Enter_OnlyOperations" xml:space="preserve">
<value>We only compile operations.</value>
</data>
<data name="ErrorHelper_NoNullBubbling_ArgumentValue_NotAllowed" xml:space="preserve">
<value>Only boolean values are allowed to switch null bubbling on or off.</value>
</data>
</root>
Loading

0 comments on commit c195ced

Please sign in to comment.