Skip to content
Merged
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 @@ -415,13 +415,12 @@ public static HashSet<string> GetPreservedTypeNames(
{
foreach (var argument in field.Arguments)
{
var argumentInnerType = argument.Type.InnerType();
var argumentNamedType = argument.Type.NamedType();

if ((argument.HasRequireDirective || field.IsLookup)
&& argumentInnerType is INameProvider { Name: var name }
&& !SpecScalarNames.IsSpecScalar(name))
&& !SpecScalarNames.IsSpecScalar(argumentNamedType.Name))
{
preservedTypeNames.Add(name);
preservedTypeNames.Add(argumentNamedType.Name);
}
}
}
Expand Down

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

Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,9 @@
<data name="RequirementsValidator_FieldDoesNotExistOnType" xml:space="preserve">
<value>The field '{0}' does not exist on the type '{1}'.</value>
</data>
<data name="RequirementsValidator_NoLookupForRequiringField" xml:space="preserve">
<value>The field '{0}.{1}' in schema '{2}' requires data from another schema, but schema '{2}' has no lookup for type '{0}' that could resolve the field once the required data has been fetched.</value>
</data>
<data name="RequirementsValidator_NoLookupsFoundForType" xml:space="preserve">
<value>No lookups found for type '{0}' in schema '{1}'.</value>
</data>
Expand Down Expand Up @@ -630,6 +633,9 @@
<data name="SatisfiabilityValidator_NodeTypeHasNoNodeLookup" xml:space="preserve">
<value>Type '{0}' implements the 'Node' interface, but no source schema provides a non-internal 'Query.node&lt;Node&gt;' lookup field for this type.</value>
</data>
<data name="SatisfiabilityValidator_NoLookupForRequiringField" xml:space="preserve">
<value>The field '{0}.{1}' in schema '{2}' requires data from another schema, but schema '{2}' has no lookup for type '{0}' that could resolve the field once the required data has been fetched.</value>
</data>
<data name="SatisfiabilityValidator_NoLookupsFoundForType" xml:space="preserve">
<value>No lookups found for type '{0}' in schema '{1}'.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using HotChocolate.Types;
using HotChocolate.Types.Mutable;
using static HotChocolate.Fusion.Properties.CompositionResources;
using static HotChocolate.Language.Utf8GraphQLParser.Syntax;

namespace HotChocolate.Fusion.Satisfiability;

Expand All @@ -17,20 +18,41 @@ internal sealed class RequirementsValidator(
private readonly FusionLookupDirectiveCache _lookupCache = lookupCache;
private readonly SatisfiabilityFacts _facts = facts;

/// <summary>
/// Validates that <paramref name="requirements"/> can be resolved for
/// <paramref name="contextType"/> at the position described by <paramref name="parentPathItem"/>.
/// </summary>
/// <param name="requirements">The requirement selection set.</param>
/// <param name="contextType">The object type the requirements are selected on.</param>
/// <param name="parentPathItem">The path item holding the type, or null at a root type.</param>
/// <param name="excludeSchemaName">
/// The schema the requirement leaves must not be resolved from, or null to allow every schema.
/// </param>
/// <param name="allowIntermediatesFromExcludedSchema">
/// Whether intermediate fields of the requirement may be resolved from the excluded schema.
/// </param>
/// <param name="cycleDetectionPath">The shared cycle detection path, or null to start a new one.</param>
/// <param name="unavailableSchemaNames">
/// Schemas whose data is unavailable at the position for any purpose, including lookup keys
/// and nested requirements.
/// </param>
/// <returns>The satisfiability errors, empty when the requirements are satisfiable.</returns>
public ImmutableArray<SatisfiabilityError> Validate(
SelectionSetNode requirements,
MutableObjectTypeDefinition contextType,
SatisfiabilityPathItem? parentPathItem,
string excludeSchemaName,
string? excludeSchemaName,
bool allowIntermediatesFromExcludedSchema = false,
SatisfiabilityPath? cycleDetectionPath = null)
SatisfiabilityPath? cycleDetectionPath = null,
ImmutableHashSet<string>? unavailableSchemaNames = null)
{
var context = new RequirementsValidatorContext(
contextType,
parentPathItem,
excludeSchemaName,
allowIntermediatesFromExcludedSchema,
cycleDetectionPath);
cycleDetectionPath,
unavailableSchemaNames ?? []);

var errors = new List<SatisfiabilityError>();

Expand Down Expand Up @@ -154,9 +176,14 @@ private ImmutableArray<SatisfiabilityError> Visit(
// not been entered yet, so intermediates must also come from outside
// it (default behavior).
var schemaNames = field.GetSchemaNames();
if (fieldNode.SelectionSet is null || !context.AllowIntermediatesFromExcludedSchema)
if (context.ExcludeSchemaName is { } excludeSchemaName
&& (fieldNode.SelectionSet is null || !context.AllowIntermediatesFromExcludedSchema))
{
schemaNames = schemaNames.Remove(context.ExcludeSchemaName);
schemaNames = schemaNames.Remove(excludeSchemaName);
}
if (!context.UnavailableSchemaNames.IsEmpty)
{
schemaNames = schemaNames.RemoveAll(context.UnavailableSchemaNames.Contains);
}
var fieldType = field.Type.AsTypeDefinition();
var optionCount = 0;
Expand Down Expand Up @@ -242,17 +269,29 @@ private ImmutableArray<SatisfiabilityError> Visit(
|| !_facts.IsFieldResolvableOn(type, field, schemaName)))
{
var requirementErrors =
new RequirementsValidator(
schema,
_lookupCache,
_facts,
includeSatisfiabilityPaths).Validate(
Validate(
requirements,
type,
context.Path.Peek(),
previousPathItem,
excludeSchemaName: schemaName,
allowIntermediatesFromExcludedSchema: true,
cycleDetectionPath: context.CycleDetectionPath);
cycleDetectionPath: context.CycleDetectionPath,
unavailableSchemaNames: context.UnavailableSchemaNames);

if (requirementErrors.IsEmpty)
{
requirementErrors =
ValidateReentry(
requirements,
type,
field,
schemaName,
previousPathItem,
RequirementsValidator_NoLookupForRequiringField,
RequirementsValidator_UnableToSatisfyRequirementForLookup,
cycleDetectionPath: context.CycleDetectionPath,
unavailableSchemaNames: context.UnavailableSchemaNames);
}

if (requirementErrors.Length != 0)
{
Expand Down Expand Up @@ -351,20 +390,124 @@ private ImmutableArray<SatisfiabilityError> ValidateSourceSchemaTransition(
contextType,
parentPathItem,
excludeSchemaName: transitionToSchemaName,
cycleDetectionPath: context.CycleDetectionPath),
cycleDetectionPath: context.CycleDetectionPath,
unavailableSchemaNames: context.UnavailableSchemaNames),
RequirementsValidator_NoLookupsFoundForType,
RequirementsValidator_UnableToSatisfyRequirementForLookup);
}

/// <summary>
/// Validates that <paramref name="schemaName"/> can resolve <paramref name="field"/> once its
/// requirements have been fetched from other schemas. This holds when the requirements can be
/// resolved without data from the schema, or when the schema has a lookup for
/// <paramref name="type"/> whose key is resolvable while the type is held on the schema.
/// </summary>
/// <param name="requirements">The requirement selection set of the field.</param>
/// <param name="type">The object type declaring the field.</param>
/// <param name="field">The field with requirements.</param>
/// <param name="schemaName">The schema resolving the field.</param>
/// <param name="parentPathItem">The path item holding the type, or null at a root type.</param>
/// <param name="noLookupMessageFormat">The message format used when the schema cannot be re-entered.</param>
/// <param name="unableToSatisfyRequirementForLookupMessageFormat">
/// The message format used when a lookup key cannot be satisfied.
/// </param>
/// <param name="cycleDetectionPath">The shared cycle detection path, or null to start a new one.</param>
/// <param name="unavailableSchemaNames">
/// Schemas whose data is unavailable at the position for any purpose.
/// </param>
/// <returns>The satisfiability errors, empty when the field can be resolved.</returns>
public ImmutableArray<SatisfiabilityError> ValidateReentry(
SelectionSetNode requirements,
MutableObjectTypeDefinition type,
MutableOutputFieldDefinition field,
string schemaName,
SatisfiabilityPathItem? parentPathItem,
string noLookupMessageFormat,
string unableToSatisfyRequirementForLookupMessageFormat,
SatisfiabilityPath? cycleDetectionPath = null,
ImmutableHashSet<string>? unavailableSchemaNames = null)
{
if (schema.IsRootOperationType(type))
{
return [];
}

unavailableSchemaNames ??= [];

var lookupErrors = new List<SatisfiabilityError>();

foreach (var lookup in _lookupCache.GetPossibleFusionLookupDirectives(type, schemaName))
{
var lookupKeyArg = (string)lookup.Arguments[WellKnownArgumentNames.Key].Value!;
var lookupFieldArg = (string)lookup.Arguments[WellKnownArgumentNames.Field].Value!;
var lookupPathArg = (string?)lookup.Arguments[WellKnownArgumentNames.Path].Value;
var lookupRequirements = ParseSelectionSet($"{{ {lookupKeyArg} }}");

// The key is fetched by the first call to the schema, so the schema itself is a valid
// source for the key fields.
var keyErrors =
Validate(
lookupRequirements,
type,
parentPathItem,
excludeSchemaName: null,
cycleDetectionPath: cycleDetectionPath,
unavailableSchemaNames: unavailableSchemaNames);

if (keyErrors.IsEmpty)
{
return [];
}

var lookupFieldName = ParseFieldDefinition(lookupFieldArg).Name.Value;
var lookupName = lookupPathArg is null
? lookupFieldName
: $"{lookupPathArg}.{lookupFieldName}";

lookupErrors.Add(
new SatisfiabilityError(
string.Format(
unableToSatisfyRequirementForLookupMessageFormat,
lookupRequirements.ToString(indented: false),
lookupName,
schemaName),
keyErrors));
}

// Without a lookup the field can only be resolved by the call that already holds the type,
// which works when the requirements do not depend on data produced by that call.
var independentErrors =
Validate(
requirements,
type,
parentPathItem,
excludeSchemaName: schemaName,
cycleDetectionPath: cycleDetectionPath,
unavailableSchemaNames: unavailableSchemaNames.Add(schemaName));

if (independentErrors.IsEmpty)
{
return [];
}

return
[
new SatisfiabilityError(
string.Format(noLookupMessageFormat, type.Name, field.Name, schemaName),
[.. lookupErrors])
];
}
}

internal sealed class RequirementsValidatorContext
{
public RequirementsValidatorContext(
MutableObjectTypeDefinition contextType,
SatisfiabilityPathItem? parentPathItem,
string excludeSchemaName,
bool allowIntermediatesFromExcludedSchema = false,
SatisfiabilityPath? cycleDetectionPath = null)
string? excludeSchemaName,
bool allowIntermediatesFromExcludedSchema,
SatisfiabilityPath? cycleDetectionPath,
ImmutableHashSet<string> unavailableSchemaNames)
{
TypeContext.Push(contextType);

Expand All @@ -376,17 +519,28 @@ public RequirementsValidatorContext(
ExcludeSchemaName = excludeSchemaName;
AllowIntermediatesFromExcludedSchema = allowIntermediatesFromExcludedSchema;
CycleDetectionPath = cycleDetectionPath ?? [];
UnavailableSchemaNames = unavailableSchemaNames;
}

public Stack<MutableObjectTypeDefinition> TypeContext { get; } = [];

public SatisfiabilityPath Path { get; } = [];

public string ExcludeSchemaName { get; }
/// <summary>
/// Gets the schema the requirement leaves must not be resolved from, or null when every
/// schema is allowed.
/// </summary>
public string? ExcludeSchemaName { get; }

public bool AllowIntermediatesFromExcludedSchema { get; }

public SatisfiabilityPath CycleDetectionPath { get; }

/// <summary>
/// Gets the schemas whose data is unavailable at the position for any purpose, including
/// lookup keys and nested requirements.
/// </summary>
public ImmutableHashSet<string> UnavailableSchemaNames { get; }

public HashSet<FieldAccessCacheKey> FieldAccessCache { get; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ public bool IsFieldAccessible(

/// <summary>
/// Determines whether <paramref name="field"/> is resolvable as the entity's own field on
/// <paramref name="sourceSchema"/>, that is the field exists there, is not partial, and its
/// <c>@require</c> is satisfied. This does not account for reaching
/// <paramref name="sourceSchema"/>, that is the field exists there, is not partial, its
/// <c>@require</c> is satisfied, and the schema can be re-entered at the type through a
/// lookup once the required data has been fetched. This does not account for reaching
/// <paramref name="sourceSchema"/>; see <see cref="CanTransition"/> for that.
/// </summary>
/// <param name="type">The object type declaring the field.</param>
Expand Down
Loading
Loading