diff --git a/src/HotChocolate/Core/src/Types.Abstractions/Types/IScalarTypeDefinition.cs b/src/HotChocolate/Core/src/Types.Abstractions/Types/IScalarTypeDefinition.cs index a4b365c74d2..4dbb0a17b96 100644 --- a/src/HotChocolate/Core/src/Types.Abstractions/Types/IScalarTypeDefinition.cs +++ b/src/HotChocolate/Core/src/Types.Abstractions/Types/IScalarTypeDefinition.cs @@ -7,6 +7,8 @@ public interface IScalarTypeDefinition , IInputTypeDefinition , ISyntaxNodeProvider { + Uri? SpecifiedBy { get; } + /// /// Checks if the value is an instance of this type. /// diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs index 3abe67ce297..20026314f82 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__Field.cs @@ -11,8 +11,8 @@ namespace HotChocolate.Types.Introspection; -[Introspection] // ReSharper disable once InconsistentNaming +[Introspection] internal sealed class __Field : ObjectType { protected override ObjectTypeConfiguration CreateConfiguration(ITypeDiscoveryContext context) diff --git a/src/HotChocolate/Core/src/Validation/Rules/VariableVisitor.cs b/src/HotChocolate/Core/src/Validation/Rules/VariableVisitor.cs index 48f2e8bb3e6..ea9fde26203 100644 --- a/src/HotChocolate/Core/src/Validation/Rules/VariableVisitor.cs +++ b/src/HotChocolate/Core/src/Validation/Rules/VariableVisitor.cs @@ -121,6 +121,18 @@ protected override ISyntaxVisitorAction Enter( { if (IntrospectionFieldNames.TypeName.Equals(node.Name.Value, StringComparison.Ordinal)) { + if (node.Directives.Count > 0) + { + foreach (var directive in node.Directives) + { + var result = Visit(directive, context); + if (result.IsBreak()) + { + return result; + } + } + } + return Skip; } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Collections/FusionDirectiveCollection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Collections/FusionDirectiveCollection.cs index 8a52ad426a9..44937d70101 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Collections/FusionDirectiveCollection.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Collections/FusionDirectiveCollection.cs @@ -12,6 +12,7 @@ public sealed class FusionDirectiveCollection public FusionDirectiveCollection(FusionDirective[] directives) { + ArgumentNullException.ThrowIfNull(directives); _directives = directives; } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Collections/FusionEnumValueCollection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Collections/FusionEnumValueCollection.cs new file mode 100644 index 00000000000..f045d3bddc7 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Collections/FusionEnumValueCollection.cs @@ -0,0 +1,91 @@ +using System.Collections; +using System.Collections.Frozen; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Types.Collections; + +public sealed class FusionEnumValueCollection + : IReadOnlyList + , IReadOnlyEnumValueCollection +{ + private readonly FusionEnumValue[] _values; + private readonly FrozenDictionary _map; + + public FusionEnumValueCollection(FusionEnumValue[] values) + { + ArgumentNullException.ThrowIfNull(values); + _map = values.ToFrozenDictionary(t => t.Name); + _values = values; + } + + public int Count => _values.Length; + + /// + /// Gets the enum value with the specified name. + /// + public FusionEnumValue this[string name] => _map[name]; + + IEnumValue IReadOnlyEnumValueCollection.this[string name] => _map[name]; + + public FusionEnumValue this[int index] => _values[index]; + + IEnumValue IReadOnlyList.this[int index] => this[index]; + + /// + /// Tries to get the for + /// the specified . + /// + /// + /// The GraphQL enum value name. + /// + /// + /// The GraphQL enum value. + /// + /// + /// true if the represents a value of this enum type; + /// otherwise, false. + /// + public bool TryGetValue(string name, [NotNullWhen(true)] out FusionEnumValue? value) + => _map.TryGetValue(name, out value); + + bool IReadOnlyEnumValueCollection.TryGetValue(string name, [NotNullWhen(true)] out IEnumValue? value) + { + if(_map.TryGetValue(name, out var enumValue)) + { + value = enumValue; + return true; + } + + value = null; + return false; + } + + /// + /// Determines whether the collection contains an enum value with the specified name. + /// + /// + /// The GraphQL enum value name. + /// + /// + /// true if the collection contains an enum value with the specified name; + /// otherwise, false. + /// + public bool ContainsName(string name) + => _map.ContainsKey(name); + + public IEnumerable AsEnumerable() + => _values; + + public IEnumerator GetEnumerator() + => Unsafe.As>(_values).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public static FusionEnumValueCollection Empty { get; } = new([]); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/AggregateCompositeTypeInterceptor.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/AggregateCompositeTypeInterceptor.cs new file mode 100644 index 00000000000..ac87f5c67c3 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/AggregateCompositeTypeInterceptor.cs @@ -0,0 +1,39 @@ +using HotChocolate.Features; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Types.Completion; + +internal sealed class AggregateCompositeTypeInterceptor : CompositeTypeInterceptor +{ + private readonly CompositeTypeInterceptor[] _interceptors; + + public AggregateCompositeTypeInterceptor(CompositeTypeInterceptor[] interceptors) + { + ArgumentNullException.ThrowIfNull(interceptors); + _interceptors = interceptors; + } + + public override void OnCompleteSchema( + ICompositeSchemaBuilderContext context, + ref IFeatureCollection features) + { + foreach (var interceptor in _interceptors) + { + interceptor.OnCompleteSchema(context, ref features); + } + } + + public override void OnCompleteOutputField( + ICompositeSchemaBuilderContext context, + IComplexTypeDefinition type, + IOutputFieldDefinition field, + OperationType? operationType, + ref IFeatureCollection features) + { + foreach (var interceptor in _interceptors) + { + interceptor.OnCompleteOutputField(context, type, field, operationType, ref features); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompletionTools.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompletionTools.cs index e2b5e8e3bf4..a88ee5d5279 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompletionTools.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompletionTools.cs @@ -12,7 +12,7 @@ internal static class CompletionTools { public static FusionDirectiveCollection CreateDirectiveCollection( IReadOnlyList directives, - CompositeSchemaContext context) + CompositeSchemaBuilderContext context) { directives = DirectiveTools.GetUserDirectives(directives); @@ -60,7 +60,7 @@ private static ArgumentAssignment CreateArgumentAssignment( public static FusionInterfaceTypeDefinitionCollection CreateInterfaceTypeCollection( IReadOnlyList interfaceTypes, - CompositeSchemaContext context) + CompositeSchemaBuilderContext context) { if (interfaceTypes.Count == 0) { @@ -79,7 +79,7 @@ public static FusionInterfaceTypeDefinitionCollection CreateInterfaceTypeCollect public static FusionObjectTypeDefinitionCollection CreateObjectTypeCollection( IReadOnlyList types, - CompositeSchemaContext context) + CompositeSchemaBuilderContext context) { var temp = new FusionObjectTypeDefinition[types.Count]; @@ -93,7 +93,7 @@ public static FusionObjectTypeDefinitionCollection CreateObjectTypeCollection( public static SourceObjectTypeCollection CreateSourceObjectTypeCollection( ObjectTypeDefinitionNode typeDef, - CompositeSchemaContext context) + CompositeSchemaBuilderContext context) { var types = TypeDirectiveParser.Parse(typeDef.Directives); var lookups = LookupDirectiveParser.Parse(typeDef.Directives); @@ -113,7 +113,7 @@ public static SourceObjectTypeCollection CreateSourceObjectTypeCollection( public static SourceInterfaceTypeCollection CreateSourceInterfaceTypeCollection( InterfaceTypeDefinitionNode typeDef, - CompositeSchemaContext context) + CompositeSchemaBuilderContext context) { var types = TypeDirectiveParser.Parse(typeDef.Directives); var lookups = LookupDirectiveParser.Parse(typeDef.Directives); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeEnumTypeCompletionContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeEnumTypeCompletionContext.cs new file mode 100644 index 00000000000..511a96d2c77 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeEnumTypeCompletionContext.cs @@ -0,0 +1,13 @@ +using HotChocolate.Features; +using HotChocolate.Fusion.Types.Collections; + +namespace HotChocolate.Fusion.Types.Completion; + +internal readonly ref struct CompositeEnumTypeCompletionContext( + FusionDirectiveCollection directives, + IFeatureCollection features) +{ + public FusionDirectiveCollection Directives { get; } = directives; + + public IFeatureCollection Features { get; } = features; +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeEnumValueCompletionContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeEnumValueCompletionContext.cs new file mode 100644 index 00000000000..669e5b4f228 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeEnumValueCompletionContext.cs @@ -0,0 +1,17 @@ +using HotChocolate.Features; +using HotChocolate.Fusion.Types.Collections; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Types.Completion; + +internal readonly ref struct CompositeEnumValueCompletionContext( + IEnumTypeDefinition declaringType, + FusionDirectiveCollection directives, + IFeatureCollection features) +{ + public IEnumTypeDefinition DeclaringType { get; } = declaringType; + + public FusionDirectiveCollection Directives { get; } = directives; + + public IFeatureCollection Features { get; } = features; +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeScalarTypeCompletionContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeScalarTypeCompletionContext.cs index b6e8544c03f..913a739f32f 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeScalarTypeCompletionContext.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeScalarTypeCompletionContext.cs @@ -4,9 +4,12 @@ namespace HotChocolate.Fusion.Types.Completion; internal readonly ref struct CompositeScalarTypeCompletionContext( ScalarValueKind valueKind, - FusionDirectiveCollection directives) + FusionDirectiveCollection directives, + Uri? specifiedBy) { public ScalarValueKind ValueKind { get; } = valueKind; public FusionDirectiveCollection Directives { get; } = directives; + + public Uri? SpecifiedBy { get; } = specifiedBy; } 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 fd134271b48..0aa104e7f2e 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 @@ -5,6 +5,7 @@ using HotChocolate.Fusion.Utilities; using HotChocolate.Language; using HotChocolate.Types; +using Microsoft.Extensions.DependencyInjection; using static System.Runtime.InteropServices.ImmutableCollectionsMarshal; namespace HotChocolate.Fusion.Types.Completion; @@ -21,14 +22,14 @@ public static FusionSchemaDefinition Create( return CompleteTypes(context); } - private static CompositeSchemaContext CreateTypes( + private static CompositeSchemaBuilderContext CreateTypes( string name, DocumentNode schema, IServiceProvider? services, IFeatureCollection? features) { string? description = null; - string? queryType = null; + var queryType = "Query"; string? mutationType = null; string? subscriptionType = null; var directives = ImmutableArray.Empty; @@ -37,7 +38,32 @@ private static CompositeSchemaContext CreateTypes( var directiveTypes = ImmutableArray.CreateBuilder(); var directiveDefinitions = ImmutableDictionary.CreateBuilder(); - foreach (var definition in schema.Definitions) + var schemaDefinition = schema.Definitions.OfType().FirstOrDefault(); + if (schemaDefinition is not null) + { + description = schemaDefinition.Description?.Value; + directives = [..schemaDefinition.Directives]; + + foreach (var operationType in schemaDefinition.OperationTypes) + { + switch (operationType.Operation) + { + case OperationType.Query: + queryType = operationType.Type.Name.Value; + break; + + case OperationType.Mutation: + mutationType = operationType.Type.Name.Value; + break; + + case OperationType.Subscription: + subscriptionType = operationType.Type.Name.Value; + break; + } + } + } + + foreach (var definition in IntrospectionSchema.Document.Definitions.Concat(schema.Definitions)) { if (definition is IHasName namedSyntaxNode && (FusionBuiltIns.IsBuiltInType(namedSyntaxNode.Name.Value) @@ -49,7 +75,10 @@ private static CompositeSchemaContext CreateTypes( switch (definition) { case ObjectTypeDefinitionNode objectType: - types.Add(CreateObjectType(objectType)); + var type = CreateObjectType( + objectType, + objectType.Name.Value.Equals(queryType, StringComparison.Ordinal)); + types.Add(type); typeDefinitions.Add(objectType.Name.Value, objectType); break; @@ -68,6 +97,11 @@ private static CompositeSchemaContext CreateTypes( typeDefinitions.Add(inputObjectType.Name.Value, inputObjectType); break; + case EnumTypeDefinitionNode enumType: + types.Add(CreateEnumType(enumType)); + typeDefinitions.Add(enumType.Name.Value, enumType); + break; + case ScalarTypeDefinitionNode scalarType: types.Add(CreateScalarType(scalarType)); typeDefinitions.Add(scalarType.Name.Value, scalarType); @@ -77,39 +111,17 @@ private static CompositeSchemaContext CreateTypes( directiveTypes.Add(CreateDirectiveType(directiveType)); directiveDefinitions.Add(directiveType.Name.Value, directiveType); break; - - case SchemaDefinitionNode schemaDefinition: - description = schemaDefinition.Description?.Value; - directives = [.. schemaDefinition.Directives]; - - foreach (var operationType in schemaDefinition.OperationTypes) - { - switch (operationType.Operation) - { - case OperationType.Query: - queryType = operationType.Type.Name.Value; - break; - - case OperationType.Mutation: - mutationType = operationType.Type.Name.Value; - break; - - case OperationType.Subscription: - subscriptionType = operationType.Type.Name.Value; - break; - } - } - break; } } + services ??= EmptyServiceProvider.Instance; features ??= FeatureCollection.Empty; - return new CompositeSchemaContext( + return new CompositeSchemaBuilderContext( name, description, - services ?? EmptyServiceProvider.Instance, - queryType ?? "Query", + services, + queryType, mutationType, subscriptionType, directives, @@ -117,16 +129,30 @@ private static CompositeSchemaContext CreateTypes( typeDefinitions.ToImmutable(), directiveTypes.ToImmutable(), directiveDefinitions.ToImmutable(), - features.ToReadOnly()); + features.ToReadOnly(), + CreateTypeInterceptor(services)); + } + + private static CompositeTypeInterceptor CreateTypeInterceptor(IServiceProvider services) + { + var interceptors = services.GetService>()?.ToArray() ?? []; + + return interceptors.Length switch + { + 0 => new NoOpCompositeTypeInterceptor(), + 1 => interceptors[0], + _ => new AggregateCompositeTypeInterceptor(interceptors) + }; } private static FusionObjectTypeDefinition CreateObjectType( - ObjectTypeDefinitionNode definition) + ObjectTypeDefinitionNode definition, + bool isQuery) { return new FusionObjectTypeDefinition( definition.Name.Value, definition.Description?.Value, - CreateOutputFields(definition.Fields)); + CreateOutputFields(definition.Fields, isQuery)); } private static FusionInterfaceTypeDefinition CreateInterfaceType( @@ -135,7 +161,7 @@ private static FusionInterfaceTypeDefinition CreateInterfaceType( return new FusionInterfaceTypeDefinition( definition.Name.Value, definition.Description?.Value, - CreateOutputFields(definition.Fields)); + CreateOutputFields(definition.Fields, false)); } private static FusionUnionTypeDefinition CreateUnionType( @@ -155,22 +181,99 @@ private static FusionInputObjectTypeDefinition CreateInputObjectType( CreateInputFields(definition.Fields)); } + private static FusionEnumTypeDefinition CreateEnumType( + EnumTypeDefinitionNode definition) + { + return new FusionEnumTypeDefinition( + definition.Name.Value, + definition.Description?.Value, + CreateEnumValues(definition.Values)); + } + + private static FusionScalarTypeDefinition CreateScalarType( + ScalarTypeDefinitionNode definition) + { + return new FusionScalarTypeDefinition( + definition.Name.Value, + definition.Description?.Value); + } + + private static FusionDirectiveDefinition CreateDirectiveType( + DirectiveDefinitionNode definition) + { + return new FusionDirectiveDefinition( + definition.Name.Value, + definition.Description?.Value, + definition.IsRepeatable, + CreateInputFields(definition.Arguments), + DirectiveLocationUtils.Parse(definition.Locations)); + } + private static FusionOutputFieldDefinitionCollection CreateOutputFields( - IReadOnlyList fields) + IReadOnlyList fields, + bool isQuery) { - var sourceFields = new FusionOutputFieldDefinition[fields.Count]; + var size = isQuery ? fields.Count + 3 : fields.Count; + var sourceFields = new FusionOutputFieldDefinition[size]; - for (var i = 0; i < fields.Count; i++) + if (isQuery) { - var field = fields[i]; - var isDeprecated = DeprecatedDirectiveParser.TryParse(field.Directives, out var deprecated); - - sourceFields[i] = new FusionOutputFieldDefinition( - field.Name.Value, - field.Description?.Value, - isDeprecated, - deprecated?.Reason, - CreateOutputFieldArguments(field.Arguments)); + sourceFields[0] = new FusionOutputFieldDefinition( + "__schema", + null, + isDeprecated: false, + deprecationReason: null, + arguments: FusionInputFieldDefinitionCollection.Empty); + + sourceFields[1] = new FusionOutputFieldDefinition( + "__type", + null, + isDeprecated: false, + deprecationReason: null, + arguments: new FusionInputFieldDefinitionCollection( + [ + new FusionInputFieldDefinition( + "name", + null, + null, + isDeprecated: false, + deprecationReason: null) + ])); + + sourceFields[2] = new FusionOutputFieldDefinition( + "__typename", + null, + isDeprecated: false, + deprecationReason: null, + arguments: FusionInputFieldDefinitionCollection.Empty); + + for (var i = 0; i < fields.Count; i++) + { + var field = fields[i]; + var isDeprecated = DeprecatedDirectiveParser.TryParse(field.Directives, out var deprecated); + + sourceFields[i + 3] = new FusionOutputFieldDefinition( + field.Name.Value, + field.Description?.Value, + isDeprecated, + deprecated?.Reason, + CreateOutputFieldArguments(field.Arguments)); + } + } + else + { + for (var i = 0; i < fields.Count; i++) + { + var field = fields[i]; + var isDeprecated = DeprecatedDirectiveParser.TryParse(field.Directives, out var deprecated); + + sourceFields[i] = new FusionOutputFieldDefinition( + field.Name.Value, + field.Description?.Value, + isDeprecated, + deprecated?.Reason, + CreateOutputFieldArguments(field.Arguments)); + } } return new FusionOutputFieldDefinitionCollection(sourceFields); @@ -202,24 +305,6 @@ private static FusionInputFieldDefinitionCollection CreateOutputFieldArguments( return new FusionInputFieldDefinitionCollection(temp); } - private static FusionScalarTypeDefinition CreateScalarType(ScalarTypeDefinitionNode definition) - { - return new FusionScalarTypeDefinition( - definition.Name.Value, - definition.Description?.Value); - } - - private static FusionDirectiveDefinition CreateDirectiveType( - DirectiveDefinitionNode definition) - { - return new FusionDirectiveDefinition( - definition.Name.Value, - definition.Description?.Value, - definition.IsRepeatable, - CreateInputFields(definition.Arguments), - DirectiveLocationUtils.Parse(definition.Locations)); - } - private static FusionInputFieldDefinitionCollection CreateInputFields( IReadOnlyList fields) { @@ -246,89 +331,155 @@ private static FusionInputFieldDefinitionCollection CreateInputFields( return new FusionInputFieldDefinitionCollection(sourceFields); } - private static FusionSchemaDefinition CompleteTypes(CompositeSchemaContext schemaContext) + private static FusionEnumValueCollection CreateEnumValues( + IReadOnlyList fields) { - foreach (var type in schemaContext.TypeDefinitions) + if (fields.Count == 0) + { + return FusionEnumValueCollection.Empty; + } + + var sourceFields = new FusionEnumValue[fields.Count]; + + for (var i = 0; i < fields.Count; i++) + { + var field = fields[i]; + var isDeprecated = DeprecatedDirectiveParser.TryParse(field.Directives, out var deprecated); + + sourceFields[i] = new FusionEnumValue( + field.Name.Value, + field.Description?.Value, + isDeprecated, + deprecated?.Reason); + } + + return new FusionEnumValueCollection(sourceFields); + } + + private static FusionSchemaDefinition CompleteTypes(CompositeSchemaBuilderContext context) + { + foreach (var type in context.TypeDefinitions) { switch (type) { case FusionObjectTypeDefinition objectType: CompleteObjectType( objectType, - schemaContext.GetTypeDefinition(objectType.Name), - schemaContext); + context.GetTypeDefinition(objectType.Name), + context); break; case FusionInterfaceTypeDefinition interfaceType: CompleteInterfaceType( interfaceType, - schemaContext.GetTypeDefinition(interfaceType.Name), - schemaContext); + context.GetTypeDefinition(interfaceType.Name), + context); break; case FusionUnionTypeDefinition unionType: CompleteUnionType( unionType, - schemaContext.GetTypeDefinition(unionType.Name), - schemaContext); + context.GetTypeDefinition(unionType.Name), + context); break; case FusionInputObjectTypeDefinition inputObjectType: CompleteInputObjectType( inputObjectType, - schemaContext.GetTypeDefinition(inputObjectType.Name), - schemaContext); + context.GetTypeDefinition(inputObjectType.Name), + context); + break; + + case FusionEnumTypeDefinition enumType: + CompleteEnumType( + enumType, + context.GetTypeDefinition(enumType.Name), + context); break; case FusionScalarTypeDefinition scalarType: CompleteScalarType( scalarType, - schemaContext.GetTypeDefinition(scalarType.Name), - schemaContext); + context.GetTypeDefinition(scalarType.Name), + context); break; } } - foreach (var directiveType in schemaContext.DirectiveDefinitions) + foreach (var directiveType in context.DirectiveDefinitions) { CompleteDirectiveType( directiveType, - schemaContext.GetDirectiveDefinition(directiveType.Name), - schemaContext); + context.GetDirectiveDefinition(directiveType.Name), + context); } - var directives = CompletionTools.CreateDirectiveCollection(schemaContext.Directives, schemaContext); + var directives = CompletionTools.CreateDirectiveCollection(context.Directives, context); + var features = context.Features; + + context.Interceptor.OnCompleteSchema(context, ref features); return new FusionSchemaDefinition( - schemaContext.Name, - schemaContext.Description, - schemaContext.Services, - schemaContext.GetType(schemaContext.QueryType), - schemaContext.MutationType is not null - ? schemaContext.GetType(schemaContext.MutationType) + context.Name, + context.Description, + context.Services, + context.GetType(context.QueryType), + context.MutationType is not null + ? context.GetType(context.MutationType) : null, - schemaContext.SubscriptionType is not null - ? schemaContext.GetType(schemaContext.SubscriptionType) + context.SubscriptionType is not null + ? context.GetType(context.SubscriptionType) : null, directives, - new FusionTypeDefinitionCollection(AsArray(schemaContext.TypeDefinitions)!), - new FusionDirectiveDefinitionCollection(AsArray(schemaContext.DirectiveDefinitions)!), - schemaContext.Features); + new FusionTypeDefinitionCollection(AsArray(context.TypeDefinitions)!), + new FusionDirectiveDefinitionCollection(AsArray(context.DirectiveDefinitions)!), + features.ToReadOnly()); } private static void CompleteObjectType( FusionObjectTypeDefinition type, ObjectTypeDefinitionNode typeDef, - CompositeSchemaContext schemaContext) + CompositeSchemaBuilderContext context) { + var operationType = GetOperationType(typeDef.Name.Value, context); + foreach (var fieldDef in typeDef.Fields) { - CompleteOutputField(type, type.Fields[fieldDef.Name.Value], fieldDef, schemaContext); + CompleteOutputField( + type, + operationType, + type.Fields[fieldDef.Name.Value], + fieldDef, + context); + } + + if (operationType is OperationType.Query) + { + CompleteOutputField( + type, + operationType, + type.Fields["__schema"], + Utf8GraphQLParser.Syntax.ParseFieldDefinition("__schema: __Schema!"), + context); + + CompleteOutputField( + type, + operationType, + type.Fields["__type"], + Utf8GraphQLParser.Syntax.ParseFieldDefinition("__type(name: String!): __Type"), + context); + + CompleteOutputField( + type, + operationType, + type.Fields["__typename"], + Utf8GraphQLParser.Syntax.ParseFieldDefinition("__typename: String!"), + context); } - var directives = CompletionTools.CreateDirectiveCollection(typeDef.Directives, schemaContext); - var interfaces = CompletionTools.CreateInterfaceTypeCollection(typeDef.Interfaces, schemaContext); - var sources = CompletionTools.CreateSourceObjectTypeCollection(typeDef, schemaContext); + var directives = CompletionTools.CreateDirectiveCollection(typeDef.Directives, context); + var interfaces = CompletionTools.CreateInterfaceTypeCollection(typeDef.Interfaces, context); + var sources = CompletionTools.CreateSourceObjectTypeCollection(typeDef, context); type.Complete( new CompositeObjectTypeCompletionContext( @@ -341,16 +492,23 @@ private static void CompleteObjectType( private static void CompleteInterfaceType( FusionInterfaceTypeDefinition type, InterfaceTypeDefinitionNode typeDef, - CompositeSchemaContext schemaContext) + CompositeSchemaBuilderContext context) { + var operationType = GetOperationType(typeDef.Name.Value, context); + foreach (var fieldDef in typeDef.Fields) { - CompleteOutputField(type, type.Fields[fieldDef.Name.Value], fieldDef, schemaContext); + CompleteOutputField( + type, + operationType, + type.Fields[fieldDef.Name.Value], + fieldDef, + context); } - var directives = CompletionTools.CreateDirectiveCollection(typeDef.Directives, schemaContext); - var interfaces = CompletionTools.CreateInterfaceTypeCollection(typeDef.Interfaces, schemaContext); - var sources = CompletionTools.CreateSourceInterfaceTypeCollection(typeDef, schemaContext); + var directives = CompletionTools.CreateDirectiveCollection(typeDef.Directives, context); + var interfaces = CompletionTools.CreateInterfaceTypeCollection(typeDef.Interfaces, context); + var sources = CompletionTools.CreateSourceInterfaceTypeCollection(typeDef, context); type.Complete( new CompositeInterfaceTypeCompletionContext( @@ -363,18 +521,19 @@ private static void CompleteInterfaceType( private static void CompleteUnionType( FusionUnionTypeDefinition type, UnionTypeDefinitionNode typeDef, - CompositeSchemaContext schemaContext) + CompositeSchemaBuilderContext context) { - var directives = CompletionTools.CreateDirectiveCollection(typeDef.Directives, schemaContext); - var types = CompletionTools.CreateObjectTypeCollection(typeDef.Types, schemaContext); + var directives = CompletionTools.CreateDirectiveCollection(typeDef.Directives, context); + var types = CompletionTools.CreateObjectTypeCollection(typeDef.Types, context); type.Complete(new CompositeUnionTypeCompletionContext(types, directives, FeatureCollection.Empty)); } private static void CompleteOutputField( FusionComplexTypeDefinition declaringType, + OperationType? operationType, FusionOutputFieldDefinition fieldDefinition, FieldDefinitionNode fieldDef, - CompositeSchemaContext schemaContext) + CompositeSchemaBuilderContext context) { foreach (var argumentDef in fieldDef.Arguments) { @@ -382,12 +541,20 @@ private static void CompleteOutputField( fieldDefinition, fieldDefinition.Arguments[argumentDef.Name.Value], argumentDef, - schemaContext); + context); } - var directives = CompletionTools.CreateDirectiveCollection(fieldDef.Directives, schemaContext); - var type = schemaContext.GetType(fieldDef.Type).ExpectOutputType(); - var sources = BuildSourceObjectFieldCollection(fieldDefinition, fieldDef, schemaContext); + var directives = CompletionTools.CreateDirectiveCollection(fieldDef.Directives, context); + var type = context.GetType(fieldDef.Type).ExpectOutputType(); + var sources = BuildSourceObjectFieldCollection(fieldDefinition, fieldDef, context); + var features = FeatureCollection.Empty; + + context.Interceptor.OnCompleteOutputField( + context, + declaringType, + fieldDefinition, + operationType, + ref features); fieldDefinition.Complete( new CompositeObjectFieldCompletionContext( @@ -395,13 +562,13 @@ private static void CompleteOutputField( directives, type, sources, - FeatureCollection.Empty)); + features)); } private static SourceObjectFieldCollection BuildSourceObjectFieldCollection( FusionOutputFieldDefinition fieldDefinition, FieldDefinitionNode fieldDef, - CompositeSchemaContext schemaContext) + CompositeSchemaBuilderContext context) { var fieldDirectives = FieldDirectiveParser.Parse(fieldDef.Directives); var requireDirectives = RequiredDirectiveParser.Parse(fieldDef.Directives); @@ -414,7 +581,7 @@ private static SourceObjectFieldCollection BuildSourceObjectFieldCollection( fieldDirective.SourceName ?? fieldDefinition.Name, fieldDirective.SchemaName, ParseRequirements(requireDirectives, fieldDirective.SchemaName), - CompleteType(fieldDef.Type, fieldDirective.SourceType, schemaContext))); + CompleteType(fieldDef.Type, fieldDirective.SourceType, context))); } return new SourceObjectFieldCollection(temp.ToImmutable()); @@ -454,25 +621,25 @@ private static SourceObjectFieldCollection BuildSourceObjectFieldCollection( static IType CompleteType( ITypeNode type, ITypeNode? sourceType, - CompositeSchemaContext schemaContext) + CompositeSchemaBuilderContext context) { return sourceType is null - ? schemaContext.GetType(type) - : schemaContext.GetType(sourceType, type.NamedType().Name.Value); + ? context.GetType(type) + : context.GetType(sourceType, type.NamedType().Name.Value); } } private static void CompleteInputObjectType( FusionInputObjectTypeDefinition inputObjectType, InputObjectTypeDefinitionNode inputObjectTypeDef, - CompositeSchemaContext schemaContext) + CompositeSchemaBuilderContext context) { foreach (var fieldDef in inputObjectTypeDef.Fields) { - CompleteInputField(inputObjectType, inputObjectType.Fields[fieldDef.Name.Value], fieldDef, schemaContext); + CompleteInputField(inputObjectType, inputObjectType.Fields[fieldDef.Name.Value], fieldDef, context); } - var directives = CompletionTools.CreateDirectiveCollection(inputObjectTypeDef.Directives, schemaContext); + var directives = CompletionTools.CreateDirectiveCollection(inputObjectTypeDef.Directives, context); inputObjectType.Complete(new CompositeInputObjectTypeCompletionContext(directives, FeatureCollection.Empty)); } @@ -480,10 +647,10 @@ private static void CompleteInputField( ITypeSystemMember declaringMember, FusionInputFieldDefinition inputField, InputValueDefinitionNode argumentDef, - CompositeSchemaContext schemaContext) + CompositeSchemaBuilderContext context) { - var directives = CompletionTools.CreateDirectiveCollection(argumentDef.Directives, schemaContext); - var type = schemaContext.GetType(argumentDef.Type).ExpectInputType(); + var directives = CompletionTools.CreateDirectiveCollection(argumentDef.Directives, context); + var type = context.GetType(argumentDef.Type).ExpectInputType(); inputField.Complete( new CompositeInputFieldCompletionContext( @@ -493,19 +660,73 @@ private static void CompleteInputField( FeatureCollection.Empty)); } + private static void CompleteEnumType( + FusionEnumTypeDefinition typeDefinition, + EnumTypeDefinitionNode typeDefinitionNode, + CompositeSchemaBuilderContext context) + { + var directives = CompletionTools.CreateDirectiveCollection(typeDefinitionNode.Directives, context); + + foreach (var value in typeDefinitionNode.Values) + { + CompleteEnumValue( + typeDefinition, + typeDefinition.Values[value.Name.Value], + value, + context); + } + + typeDefinition.Complete( + new CompositeEnumTypeCompletionContext( + directives, + FeatureCollection.Empty)); + } + + private static void CompleteEnumValue( + IEnumTypeDefinition declaringType, + FusionEnumValue enumValue, + EnumValueDefinitionNode enumValueDef, + CompositeSchemaBuilderContext context) + { + var directives = CompletionTools.CreateDirectiveCollection(enumValueDef.Directives, context); + + enumValue.Complete( + new CompositeEnumValueCompletionContext( + declaringType, + directives, + FeatureCollection.Empty)); + } + private static void CompleteScalarType( FusionScalarTypeDefinition typeDefinition, ScalarTypeDefinitionNode typeDefinitionNode, - CompositeSchemaContext schemaContext) + CompositeSchemaBuilderContext context) { - var directives = CompletionTools.CreateDirectiveCollection(typeDefinitionNode.Directives, schemaContext); - typeDefinition.Complete(new CompositeScalarTypeCompletionContext(default, directives)); + var directives = CompletionTools.CreateDirectiveCollection(typeDefinitionNode.Directives, context); + var specifiedByDirective = directives.FirstOrDefault("specifiedBy"); + Uri? specifiedBy = null; + + if (specifiedByDirective is not null) + { + if (specifiedByDirective.Arguments["url"].Value is not StringValueNode url) + { + throw new InvalidOperationException("The specified type does not have a url."); + } + + specifiedBy = new Uri(url.Value); + } + + typeDefinition.Complete( + new CompositeScalarTypeCompletionContext( + default, + directives, + specifiedBy)); } private static void CompleteDirectiveType( FusionDirectiveDefinition directiveDefinition, DirectiveDefinitionNode directiveDefinitionNode, - CompositeSchemaContext schemaContext) + CompositeSchemaBuilderContext context) { foreach (var argumentDef in directiveDefinitionNode.Arguments) { @@ -513,8 +734,30 @@ private static void CompleteDirectiveType( directiveDefinition, directiveDefinition.Arguments[argumentDef.Name.Value], argumentDef, - schemaContext); + context); + } + } + + private static OperationType? GetOperationType( + string typeName, + CompositeSchemaBuilderContext context) + { + if (context.QueryType.Equals(typeName, StringComparison.OrdinalIgnoreCase)) + { + return OperationType.Query; + } + + if (context.MutationType?.Equals(typeName, StringComparison.OrdinalIgnoreCase) == true) + { + return OperationType.Mutation; + } + + if (context.SubscriptionType?.Equals(typeName, StringComparison.OrdinalIgnoreCase) == true) + { + return OperationType.Subscription; } + + return null; } private sealed class EmptyServiceProvider : IServiceProvider @@ -523,4 +766,11 @@ private sealed class EmptyServiceProvider : IServiceProvider public static EmptyServiceProvider Instance { get; } = new(); } + + private enum ComplexType + { + Query, + Object, + Interface + } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeSchemaContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeSchemaContext.cs index 91f522b7bbf..15c7aad7071 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeSchemaContext.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeSchemaContext.cs @@ -7,7 +7,7 @@ namespace HotChocolate.Fusion.Types.Completion; -internal sealed class CompositeSchemaContext +internal sealed class CompositeSchemaBuilderContext : ICompositeSchemaBuilderContext { private readonly Dictionary _compositeTypes = new(SyntaxComparer.BySyntax); private readonly Dictionary _typeDefinitionLookup; @@ -15,7 +15,7 @@ internal sealed class CompositeSchemaContext private readonly Dictionary _directiveDefinitionLookup; private ImmutableDictionary _directiveDefinitionNodeLookup; - public CompositeSchemaContext( + public CompositeSchemaBuilderContext( string name, string? description, IServiceProvider services, @@ -27,7 +27,8 @@ public CompositeSchemaContext( ImmutableDictionary typeDefinitionNodeLookup, ImmutableArray directiveDefinitions, ImmutableDictionary directiveDefinitionNodeLookup, - IFeatureCollection features) + IFeatureCollection features, + CompositeTypeInterceptor interceptor) { _typeDefinitionLookup = typeDefinitions.ToDictionary(t => t.Name); _directiveDefinitionLookup = directiveDefinitions.ToDictionary(t => t.Name); @@ -44,6 +45,7 @@ public CompositeSchemaContext( TypeDefinitions = typeDefinitions; DirectiveDefinitions = directiveDefinitions; Features = features; + Interceptor = interceptor; AddSpecDirectives(); } @@ -54,6 +56,8 @@ public CompositeSchemaContext( public IServiceProvider Services { get; } + public CompositeTypeInterceptor Interceptor { get; } + public string QueryType { get; } public string? MutationType { get; } @@ -134,7 +138,7 @@ private FusionScalarTypeDefinition CreateSpecScalar(string name) { var type = new FusionScalarTypeDefinition(name, null); var typeDef = new ScalarTypeDefinitionNode(null, new NameNode(name), null, []); - type.Complete(new CompositeScalarTypeCompletionContext(default, FusionDirectiveCollection.Empty)); + type.Complete(new CompositeScalarTypeCompletionContext(default, FusionDirectiveCollection.Empty, null)); _typeDefinitionNodeLookup = _typeDefinitionNodeLookup.SetItem(name, typeDef); TypeDefinitions = TypeDefinitions.Add(type); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeTypeInterceptor.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeTypeInterceptor.cs new file mode 100644 index 00000000000..6a24dc537bd --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompositeTypeInterceptor.cs @@ -0,0 +1,23 @@ +using HotChocolate.Features; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Types.Completion; + +public abstract class CompositeTypeInterceptor +{ + public virtual void OnCompleteSchema( + ICompositeSchemaBuilderContext context, + ref IFeatureCollection features) + { + } + + public virtual void OnCompleteOutputField( + ICompositeSchemaBuilderContext context, + IComplexTypeDefinition type, + IOutputFieldDefinition field, + OperationType? operationType, + ref IFeatureCollection features) + { + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/ICompositeSchemaBuilderContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/ICompositeSchemaBuilderContext.cs new file mode 100644 index 00000000000..991669e60d3 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/ICompositeSchemaBuilderContext.cs @@ -0,0 +1,8 @@ +using HotChocolate.Features; + +namespace HotChocolate.Fusion.Types.Completion; + +/// +/// The schema building context. +/// +public interface ICompositeSchemaBuilderContext : IFeatureProvider; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/IntrospectionSchema.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/IntrospectionSchema.cs new file mode 100644 index 00000000000..1f1baade071 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/IntrospectionSchema.cs @@ -0,0 +1,114 @@ +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Types.Completion; + +internal static class IntrospectionSchema +{ + private static DocumentNode? s_document; + + public static ReadOnlySpan SourceText => + """ + type __Schema { + description: String + types: [__Type!]! + queryType: __Type! + mutationType: __Type + subscriptionType: __Type + directives: [__Directive!]! + } + + type __Type { + kind: __TypeKind! + name: String + description: String + # may be non-null for custom SCALAR, otherwise null. + specifiedByURL: String + # must be non-null for OBJECT and INTERFACE, otherwise null. + fields(includeDeprecated: Boolean! = false): [__Field!] + # must be non-null for OBJECT and INTERFACE, otherwise null. + interfaces: [__Type!] + # must be non-null for INTERFACE and UNION, otherwise null. + possibleTypes: [__Type!] + # must be non-null for ENUM, otherwise null. + enumValues(includeDeprecated: Boolean! = false): [__EnumValue!] + # must be non-null for INPUT_OBJECT, otherwise null. + inputFields(includeDeprecated: Boolean! = false): [__InputValue!] + # must be non-null for NON_NULL and LIST, otherwise null. + ofType: __Type + } + + enum __TypeKind { + SCALAR + OBJECT + INTERFACE + UNION + ENUM + INPUT_OBJECT + LIST + NON_NULL + } + + type __Field { + name: String! + description: String + args(includeDeprecated: Boolean! = false): [__InputValue!]! + type: __Type! + isDeprecated: Boolean! + deprecationReason: String + } + + type __InputValue { + name: String! + description: String + type: __Type! + defaultValue: String + isDeprecated: Boolean! + deprecationReason: String + } + + type __EnumValue { + name: String! + description: String + isDeprecated: Boolean! + deprecationReason: String + } + + type __Directive { + name: String! + description: String + isRepeatable: Boolean! + locations: [__DirectiveLocation!]! + args(includeDeprecated: Boolean! = false): [__InputValue!]! + } + + enum __DirectiveLocation { + QUERY + MUTATION + SUBSCRIPTION + FIELD + FRAGMENT_DEFINITION + FRAGMENT_SPREAD + INLINE_FRAGMENT + VARIABLE_DEFINITION + SCHEMA + SCALAR + OBJECT + FIELD_DEFINITION + ARGUMENT_DEFINITION + INTERFACE + UNION + ENUM + ENUM_VALUE + INPUT_OBJECT + INPUT_FIELD_DEFINITION + } + """u8; + + public static DocumentNode Document + { + get + { + return s_document ??= Utf8GraphQLParser.Parse(SourceText); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/NoOpCompositeTypeInterceptor.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/NoOpCompositeTypeInterceptor.cs new file mode 100644 index 00000000000..075c6b968ba --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/NoOpCompositeTypeInterceptor.cs @@ -0,0 +1,3 @@ +namespace HotChocolate.Fusion.Types.Completion; + +internal sealed class NoOpCompositeTypeInterceptor : CompositeTypeInterceptor; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionEnumTypeDefinition.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionEnumTypeDefinition.cs new file mode 100644 index 00000000000..03cbb0a1732 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionEnumTypeDefinition.cs @@ -0,0 +1,119 @@ +using HotChocolate.Features; +using HotChocolate.Fusion.Types.Collections; +using HotChocolate.Fusion.Types.Completion; +using HotChocolate.Language; +using HotChocolate.Serialization; +using HotChocolate.Types; +using static HotChocolate.Fusion.Types.ThrowHelper; + +namespace HotChocolate.Fusion.Types; + +public sealed class FusionEnumTypeDefinition : IEnumTypeDefinition +{ + private bool _completed; + + public FusionEnumTypeDefinition( + string name, + string? description, + FusionEnumValueCollection values) + { + Name = name; + Description = description; + Values = values; + + // these properties are initialized + // in the type complete step. + Directives = null!; + Features = null!; + } + + public string Name { get; } + + public string? Description { get; } + + public TypeKind Kind => TypeKind.Enum; + + public SchemaCoordinate Coordinate => new(Name, ofDirective: false); + + public FusionEnumValueCollection Values { get; } + + IReadOnlyEnumValueCollection IEnumTypeDefinition.Values => Values; + + public FusionDirectiveCollection Directives + { + get; + private set + { + EnsureNotSealed(_completed); + field = value; + } + } + + IReadOnlyDirectiveCollection IDirectivesProvider.Directives + => Directives; + + public IFeatureCollection Features + { + get; + private set + { + EnsureNotSealed(_completed); + field = value; + } + } + + internal void Complete(CompositeEnumTypeCompletionContext context) + { + EnsureNotSealed(_completed); + + Directives = context.Directives; + Features = context.Features; + + _completed = true; + } + + /// + /// Get the string representation of the union type definition. + /// + /// + /// Returns the string representation of the union type definition. + /// + public override string ToString() + => SchemaDebugFormatter.Format(this).ToString(true); + + /// + /// Creates a + /// from a . + /// + public EnumTypeDefinitionNode ToSyntaxNode() + => SchemaDebugFormatter.Format(this); + + ISyntaxNode ISyntaxNodeProvider.ToSyntaxNode() + => SchemaDebugFormatter.Format(this); + + /// + public bool Equals(IType? other) + => Equals(other, TypeComparison.Reference); + + public bool Equals(IType? other, TypeComparison comparison) + { + if (comparison is TypeComparison.Reference) + { + return ReferenceEquals(this, other); + } + + return other is FusionEnumTypeDefinition otherEnum + && otherEnum.Name.Equals(Name, StringComparison.Ordinal); + } + + /// + public bool IsAssignableFrom(ITypeDefinition type) + { + if (type.Kind == TypeKind.Enum) + { + return Equals(type, TypeComparison.Reference); + } + + return false; + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionEnumValue.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionEnumValue.cs new file mode 100644 index 00000000000..e22bd8c4043 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionEnumValue.cs @@ -0,0 +1,91 @@ +using HotChocolate.Features; +using HotChocolate.Fusion.Types.Collections; +using HotChocolate.Fusion.Types.Completion; +using HotChocolate.Language; +using HotChocolate.Serialization; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Types; + +public sealed class FusionEnumValue : IEnumValue +{ + private bool _completed; + + public FusionEnumValue( + string name, + string? description, + bool isDeprecated, + string? deprecationReason) + { + Name = name; + Description = description; + IsDeprecated = isDeprecated; + DeprecationReason = deprecationReason; + + // these properties are initialized + // in the type complete step. + DeclaringType = null!; + Directives = null!; + Features = null!; + } + + public string Name { get; } + + public string? Description { get; } + + public IEnumTypeDefinition DeclaringType + { + get; + set + { + ThrowHelper.EnsureNotSealed(_completed); + field = value; + } + } + + public SchemaCoordinate Coordinate => new(DeclaringType.Name, Name, ofDirective: false); + + public bool IsDeprecated { get; } + + public string? DeprecationReason { get; } + + public FusionDirectiveCollection Directives + { + get; + private set + { + ThrowHelper.EnsureNotSealed(_completed); + field = value; + } + } + + IReadOnlyDirectiveCollection IDirectivesProvider.Directives => Directives; + + public IFeatureCollection Features + { + get; + private set + { + ThrowHelper.EnsureNotSealed(_completed); + field = value; + } + } + + internal void Complete(CompositeEnumValueCompletionContext context) + { + ThrowHelper.EnsureNotSealed(_completed); + DeclaringType = context.DeclaringType; + Directives = context.Directives; + Features = context.Features; + _completed = true; + } + + public override string ToString() + => SchemaDebugFormatter.Format(this).ToString(indented: true); + + public EnumValueDefinitionNode ToSyntaxNode() + => SchemaDebugFormatter.Format(this); + + ISyntaxNode ISyntaxNodeProvider.ToSyntaxNode() + => SchemaDebugFormatter.Format(this); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionScalarTypeDefinition.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionScalarTypeDefinition.cs index 2ff709406c3..3181804eb97 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionScalarTypeDefinition.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionScalarTypeDefinition.cs @@ -45,6 +45,8 @@ private set IReadOnlyDirectiveCollection IDirectivesProvider.Directives => _directives; + public Uri? SpecifiedBy { get; private set; } + public ScalarValueKind ValueKind { get; private set; } public IFeatureCollection Features @@ -62,6 +64,7 @@ internal void Complete(CompositeScalarTypeCompletionContext context) ThrowHelper.EnsureNotSealed(_completed); Directives = context.Directives; ValueKind = context.ValueKind; + SpecifiedBy = context.SpecifiedBy; // if the value kind is any, we need to determine the value kind based on the name // for the spec scalars. @@ -122,7 +125,6 @@ public bool IsInstanceOfType(IValueNode value) public bool Equals(IType? other) => Equals(other, TypeComparison.Reference); - /// public bool Equals(IType? other, TypeComparison comparison) { if (comparison is TypeComparison.Reference) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs index 71cd0a5ec2b..6b4a3e6439a 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs @@ -11,9 +11,11 @@ using HotChocolate.Fusion.Configuration; using HotChocolate.Fusion.Diagnostics; using HotChocolate.Fusion.Execution.Clients; +using HotChocolate.Fusion.Execution.Introspection; using HotChocolate.Fusion.Execution.Nodes; using HotChocolate.Fusion.Planning; using HotChocolate.Fusion.Types; +using HotChocolate.Fusion.Types.Completion; using HotChocolate.Language; using HotChocolate.Utilities; using HotChocolate.Validation; @@ -35,12 +37,13 @@ internal sealed class FusionRequestExecutorManager private readonly IOptionsMonitor _optionsMonitor; private readonly IServiceProvider _applicationServices; private readonly Channel _executorEvents = - Channel.CreateBounded(new BoundedChannelOptions(1) - { - FullMode = BoundedChannelFullMode.Wait, - SingleReader = true, - SingleWriter = false - }); + Channel.CreateBounded( + new BoundedChannelOptions(1) + { + FullMode = BoundedChannelFullMode.Wait, + SingleReader = true, + SingleWriter = false + }); private ImmutableArray _observers = []; private bool _disposed; @@ -215,6 +218,7 @@ private FeatureCollection CreateSchemaFeatures( features.Set(requestOptions); features.Set(parserOptions); features.Set(clientConfigurations); + features.Set(CreateTypeResolverInterceptors()); foreach (var configure in setup.SchemaFeaturesModifiers) { @@ -224,6 +228,18 @@ private FeatureCollection CreateSchemaFeatures( return features; } + private static Dictionary CreateTypeResolverInterceptors() + => new() + { + { nameof(Query), new Query() }, + { nameof(__Directive), new __Directive() }, + { nameof(__EnumValue), new __EnumValue() }, + { nameof(__Field), new __Field() }, + { nameof(__InputValue), new __InputValue() }, + { nameof(__Schema), new __Schema() }, + { nameof(__Type), new __Type() } + }; + private ServiceProvider CreateSchemaServices( FusionGatewaySetup setup) { @@ -262,6 +278,8 @@ private void AddCoreServices(IServiceCollection services) services.AddSingleton>( static _ => new DefaultObjectPool( new RequestContextPooledObjectPolicy())); + + services.AddSingleton(static _ => new IntrospectionFieldInterceptor()); } private static void AddOperationPlanner(IServiceCollection services) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/FieldContextExtensions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/FieldContextExtensions.cs new file mode 100644 index 00000000000..a1a91aac75b --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/FieldContextExtensions.cs @@ -0,0 +1,19 @@ +using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Execution.Introspection; + +internal static class FieldContextExtensions +{ + public static ObjectResult RentInitializedObjectResult(this FieldContext context) + { + var selection = context.Selection; + var operation = selection.DeclaringSelectionSet.DeclaringOperation; + var selectionSetType = selection.Field.Type.NamedType().ExpectObjectType(); + var selectionSet = operation.GetSelectionSet(context.Selection, selectionSetType); + var selectionSetResult = context.ResultPool.RentObjectResult(); + selectionSetResult.Initialize(context.ResultPool, selectionSet, context.IncludeFlags, rawLeafFields: true); + return selectionSetResult; + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/ITypeResolverInterceptor.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/ITypeResolverInterceptor.cs new file mode 100644 index 00000000000..1ae8e67e449 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/ITypeResolverInterceptor.cs @@ -0,0 +1,8 @@ +using HotChocolate.Features; + +namespace HotChocolate.Fusion.Execution.Introspection; + +internal interface ITypeResolverInterceptor +{ + void OnApplyResolver(string fieldName, IFeatureCollection features); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/IntrospectionFieldInterceptor.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/IntrospectionFieldInterceptor.cs new file mode 100644 index 00000000000..8fa843987d9 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/IntrospectionFieldInterceptor.cs @@ -0,0 +1,51 @@ +using HotChocolate.Features; +using HotChocolate.Fusion.Types.Completion; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Execution.Introspection; + +internal sealed class IntrospectionFieldInterceptor : CompositeTypeInterceptor +{ + public override void OnCompleteOutputField( + ICompositeSchemaBuilderContext context, + IComplexTypeDefinition type, + IOutputFieldDefinition field, + OperationType? operationType, + ref IFeatureCollection features) + { + var typeLookup = context.Features.GetRequired>(); + var typeName = type.Name; + + if (operationType.HasValue) + { + typeName = operationType.Value.ToString(); + } + + if (typeLookup.TryGetValue(typeName, out var resolverInterceptor)) + { + if (features.IsReadOnly) + { + features = features.IsEmpty + ? new FeatureCollection() + : new FeatureCollection(features); + } + + resolverInterceptor.OnApplyResolver(field.Name, features); + } + } + + public override void OnCompleteSchema( + ICompositeSchemaBuilderContext context, + ref IFeatureCollection features) + { + if (!features.IsEmpty) + { + features = features.IsReadOnly + ? new FeatureCollection(features) + : features; + + features.Set>(null); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/MemHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/MemHelper.cs new file mode 100644 index 00000000000..cced7c0384e --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/MemHelper.cs @@ -0,0 +1,58 @@ +using System.Text; +using HotChocolate.Fusion.Execution.Nodes; + +namespace HotChocolate.Fusion.Execution.Introspection; + +internal static class MemHelper +{ + private static readonly Encoding s_utf8 = Encoding.UTF8; + + public static void WriteValue(this FieldContext context, Uri? uri) + => WriteValue(context, uri?.ToString()); + + public static void WriteValue(this FieldContext context, string? s) + { + if (s is null) + { + return; + } + + var start = context.Memory.Length; + var expectedSize = s_utf8.GetByteCount(s); + var span = context.Memory.GetSpan(expectedSize + 1); + span[0] = RawFieldValueType.String; + var written = s_utf8.GetBytes(s, span[1..]); + context.Memory.Advance(written + 1); + var segment = context.Memory.GetWrittenMemorySegment(start, written + 1); + context.FieldResult.SetNextValue(segment); + } + + public static void WriteValue(this FieldContext context, bool b) + { + var start = context.Memory.Length; + const int length = 2; + var span = context.Memory.GetSpan(length); + span[0] = RawFieldValueType.Boolean; + span[1] = b ? (byte)1 : (byte)0; + context.Memory.Advance(length); + var segment = context.Memory.GetWrittenMemorySegment(start, length); + context.FieldResult.SetNextValue(segment); + } + + public static void WriteValue(this FieldContext context, ReadOnlySpan value) + { + if (value.Length == 0) + { + return; + } + + var start = context.Memory.Length; + var length = value.Length + 1; + var span = context.Memory.GetSpan(length); + span[0] = RawFieldValueType.String; + value.CopyTo(span[1..]); + context.Memory.Advance(length); + var segment = context.Memory.GetWrittenMemorySegment(start, length); + context.FieldResult.SetNextValue(segment); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/Query.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/Query.cs new file mode 100644 index 00000000000..620a9b6695c --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/Query.cs @@ -0,0 +1,40 @@ +using HotChocolate.Features; +using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Execution.Introspection; + +internal class Query : ITypeResolverInterceptor +{ + public void OnApplyResolver(string fieldName, IFeatureCollection features) + { + switch (fieldName) + { + case "__schema": + features.Set(new ResolveFieldValue(Schema)); + break; + + case "__type": + features.Set(new ResolveFieldValue(Type)); + break; + } + } + + public static void Schema(FieldContext context) + { + var result = context.RentInitializedObjectResult(); + context.FieldResult.SetNextValue(result); + context.AddRuntimeResult(context.Schema); + } + + public static void Type(FieldContext context) + { + var name = context.ArgumentValue("name"); + if (context.Schema.Types.TryGetType(name.Value, out var type)) + { + var result = context.RentInitializedObjectResult(); + context.FieldResult.SetNextValue(result); + context.AddRuntimeResult(type); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__Directive.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__Directive.cs new file mode 100644 index 00000000000..76b731a3d89 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__Directive.cs @@ -0,0 +1,137 @@ +using HotChocolate.Features; +using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Language; +using HotChocolate.Types; +using DirectiveLocation = HotChocolate.Types.DirectiveLocation; + +namespace HotChocolate.Fusion.Execution.Introspection; + +// ReSharper disable once InconsistentNaming +internal sealed class __Directive : ITypeResolverInterceptor +{ + public void OnApplyResolver(string fieldName, IFeatureCollection features) + { + switch (fieldName) + { + case "name": + features.Set(new ResolveFieldValue(Name)); + break; + case "description": + features.Set(new ResolveFieldValue(Description)); + break; + case "isRepeatable": + features.Set(new ResolveFieldValue(IsRepeatable)); + break; + case "locations": + features.Set(new ResolveFieldValue(Locations)); + break; + case "arguments": + features.Set(new ResolveFieldValue(Arguments)); + break; + } + } + + public static void Name(FieldContext context) + { + var directiveDef = context.Parent(); + context.WriteValue(directiveDef.Name); + } + + public static void Description(FieldContext context) + { + var directiveDef = context.Parent(); + context.WriteValue(directiveDef.Description); + } + + public static void IsRepeatable(FieldContext context) + { + var directiveDef = context.Parent(); + context.WriteValue(directiveDef.IsRepeatable); + } + + public static void Locations(FieldContext context) + { + var directiveDef = context.Parent(); + switch (directiveDef.Locations) + { + case DirectiveLocation.Query: + context.WriteValue(__DirectiveLocation.Query); + break; + case DirectiveLocation.Mutation: + context.WriteValue(__DirectiveLocation.Mutation); + break; + case DirectiveLocation.Subscription: + context.WriteValue(__DirectiveLocation.Subscription); + break; + case DirectiveLocation.Field: + context.WriteValue(__DirectiveLocation.Field); + break; + case DirectiveLocation.FragmentDefinition: + context.WriteValue(__DirectiveLocation.FragmentDefinition); + break; + case DirectiveLocation.FragmentSpread: + context.WriteValue(__DirectiveLocation.FragmentSpread); + break; + case DirectiveLocation.InlineFragment: + context.WriteValue(__DirectiveLocation.InlineFragment); + break; + case DirectiveLocation.VariableDefinition: + context.WriteValue(__DirectiveLocation.VariableDefinition); + break; + case DirectiveLocation.Schema: + context.WriteValue(__DirectiveLocation.Schema); + break; + case DirectiveLocation.Scalar: + context.WriteValue(__DirectiveLocation.Scalar); + break; + case DirectiveLocation.Object: + context.WriteValue(__DirectiveLocation.Object); + break; + case DirectiveLocation.FieldDefinition: + context.WriteValue(__DirectiveLocation.FieldDefinition); + break; + case DirectiveLocation.ArgumentDefinition: + context.WriteValue(__DirectiveLocation.ArgumentDefinition); + break; + case DirectiveLocation.Interface: + context.WriteValue(__DirectiveLocation.Interface); + break; + case DirectiveLocation.Union: + context.WriteValue(__DirectiveLocation.Union); + break; + case DirectiveLocation.Enum: + context.WriteValue(__DirectiveLocation.Enum); + break; + case DirectiveLocation.EnumValue: + context.WriteValue(__DirectiveLocation.EnumValue); + break; + case DirectiveLocation.InputObject: + context.WriteValue(__DirectiveLocation.InputObject); + break; + case DirectiveLocation.InputFieldDefinition: + context.WriteValue(__DirectiveLocation.InputFieldDefinition); + break; + default: + throw new NotSupportedException($"Directive location {directiveDef.Locations} is not supported."); + } + } + + public static void Arguments(FieldContext context) + { + var directiveDef = context.Parent(); + var includeDeprecated = context.ArgumentValue("includeDeprecated").Value; + var list = context.ResultPool.RentObjectListResult(); + context.FieldResult.SetNextValue(list); + + foreach (var argument in directiveDef.Arguments) + { + if (!includeDeprecated && argument.IsDeprecated) + { + continue; + } + + context.AddRuntimeResult(argument); + list.SetNextValue(context.RentInitializedObjectResult()); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__DirectiveLocation.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__DirectiveLocation.cs new file mode 100644 index 00000000000..714e65b8d9d --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__DirectiveLocation.cs @@ -0,0 +1,25 @@ +namespace HotChocolate.Fusion.Execution.Introspection; + +// ReSharper disable once InconsistentNaming +internal static class __DirectiveLocation +{ + public static ReadOnlySpan Query => "QUERY"u8; + public static ReadOnlySpan Mutation => "MUTATION"u8; + public static ReadOnlySpan Subscription => "SUBSCRIPTION"u8; + public static ReadOnlySpan Field => "FIELD"u8; + public static ReadOnlySpan FragmentDefinition => "FRAGMENT_DEFINITION"u8; + public static ReadOnlySpan FragmentSpread => "FRAGMENT_SPREAD"u8; + public static ReadOnlySpan InlineFragment => "INLINE_FRAGMENT"u8; + public static ReadOnlySpan VariableDefinition => "VARIABLE_DEFINITION"u8; + public static ReadOnlySpan Schema => "SCHEMA"u8; + public static ReadOnlySpan Scalar => "SCALAR"u8; + public static ReadOnlySpan Object => "OBJECT"u8; + public static ReadOnlySpan FieldDefinition => "FIELD_DEFINITION"u8; + public static ReadOnlySpan ArgumentDefinition => "ARGUMENT_DEFINITION"u8; + public static ReadOnlySpan Interface => "INTERFACE"u8; + public static ReadOnlySpan Union => "UNION"u8; + public static ReadOnlySpan Enum => "ENUM"u8; + public static ReadOnlySpan EnumValue => "ENUM_VALUE"u8; + public static ReadOnlySpan InputObject => "INPUT_OBJECT"u8; + public static ReadOnlySpan InputFieldDefinition => "INPUT_FIELD_DEFINITION"u8; +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__EnumValue.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__EnumValue.cs new file mode 100644 index 00000000000..01881b67c80 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__EnumValue.cs @@ -0,0 +1,43 @@ +using HotChocolate.Features; +using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Execution.Introspection; + +// ReSharper disable once InconsistentNaming +internal sealed class __EnumValue : ITypeResolverInterceptor +{ + public void OnApplyResolver(string fieldName, IFeatureCollection features) + { + switch (fieldName) + { + case "name": + features.Set(new ResolveFieldValue(Name)); + break; + + case "description": + features.Set(new ResolveFieldValue(Description)); + break; + + case "isDeprecated": + features.Set(new ResolveFieldValue(IsDeprecated)); + break; + + case "deprecationReason": + features.Set(new ResolveFieldValue(DeprecationReason)); + break; + } + } + + public static void Name(FieldContext context) + => context.WriteValue(context.Parent().Name); + + public static void Description(FieldContext context) + => context.WriteValue(context.Parent().Description); + + public static void IsDeprecated(FieldContext context) + => context.WriteValue(context.Parent().IsDeprecated); + + public static void DeprecationReason(FieldContext context) + => context.WriteValue(context.Parent().DeprecationReason); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__Field.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__Field.cs new file mode 100644 index 00000000000..aefdbec5470 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__Field.cs @@ -0,0 +1,92 @@ +using HotChocolate.Features; +using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Execution.Introspection; + +// ReSharper disable once InconsistentNaming +internal sealed class __Field : ITypeResolverInterceptor +{ + public void OnApplyResolver(string fieldName, IFeatureCollection features) + { + switch (fieldName) + { + case "name": + features.Set(new ResolveFieldValue(Name)); + break; + + case "description": + features.Set(new ResolveFieldValue(Description)); + break; + + case "args": + features.Set(new ResolveFieldValue(Arguments)); + break; + + case "type": + features.Set(new ResolveFieldValue(Type)); + break; + + case "isDeprecated": + features.Set(new ResolveFieldValue(IsDeprecated)); + break; + + case "deprecationReason": + features.Set(new ResolveFieldValue(DeprecationReason)); + break; + } + } + + public static void Name(FieldContext context) + => context.WriteValue(context.Parent().Name); + + public static void Description(FieldContext context) + { + if (context.Parent() is { Description: not null } fieldDef) + { + context.WriteValue(fieldDef.Description); + } + } + + public static void Arguments(FieldContext context) + { + var field = context.Parent(); + var includeDeprecated = context.ArgumentValue("includeDeprecated").Value; + var list = context.ResultPool.RentObjectListResult(); + context.FieldResult.SetNextValue(list); + + foreach (var value in field.Arguments) + { + if (!includeDeprecated && value.IsDeprecated) + { + continue; + } + + context.AddRuntimeResult(value); + list.SetNextValue(context.RentInitializedObjectResult()); + } + } + + public static void Type(FieldContext context) + { + var field = context.Parent(); + context.AddRuntimeResult(field.Type); + context.FieldResult.SetNextValue(context.RentInitializedObjectResult()); + } + + public static void IsDeprecated(FieldContext context) + { + var field = context.Parent(); + context.WriteValue(field.IsDeprecated); + } + + public static void DeprecationReason(FieldContext context) + { + var field = context.Parent(); + if (field.DeprecationReason is not null) + { + context.WriteValue(field.DeprecationReason); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__InputValue.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__InputValue.cs new file mode 100644 index 00000000000..9541b215d1c --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__InputValue.cs @@ -0,0 +1,82 @@ +using HotChocolate.Features; +using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Language.Utilities; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Execution.Introspection; + +// ReSharper disable once InconsistentNaming +internal sealed class __InputValue : ITypeResolverInterceptor +{ + public void OnApplyResolver(string fieldName, IFeatureCollection features) + { + switch (fieldName) + { + case "name": + features.Set(new ResolveFieldValue(Name)); + break; + + case "description": + features.Set(new ResolveFieldValue(Description)); + break; + + case "type": + features.Set(new ResolveFieldValue(Type)); + break; + + case "isDeprecated": + features.Set(new ResolveFieldValue(IsDeprecated)); + break; + + case "deprecationReason": + features.Set(new ResolveFieldValue(DeprecationReason)); + break; + + case "defaultValue": + features.Set(new ResolveFieldValue(DefaultValue)); + break; + } + } + + public static void Name(FieldContext context) + => context.WriteValue(context.Parent().Name); + + public static void Description(FieldContext context) + { + if (context.Parent() is { Description: not null } fieldDef) + { + context.WriteValue(fieldDef.Description); + } + } + + public static void Type(FieldContext context) + { + var field = context.Parent(); + context.AddRuntimeResult(field.Type); + context.FieldResult.SetNextValue(context.RentInitializedObjectResult()); + } + + public static void IsDeprecated(FieldContext context) + { + var field = context.Parent(); + context.WriteValue(field.IsDeprecated); + } + + public static void DeprecationReason(FieldContext context) + { + var field = context.Parent(); + if (field.DeprecationReason is not null) + { + context.WriteValue(field.DeprecationReason); + } + } + + public static void DefaultValue(FieldContext context) + { + var field = context.Parent(); + if (field.DefaultValue is not null) + { + context.WriteValue(field.DefaultValue.Print()); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__Schema.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__Schema.cs new file mode 100644 index 00000000000..c7f53169c6d --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__Schema.cs @@ -0,0 +1,89 @@ +using HotChocolate.Features; +using HotChocolate.Fusion.Execution.Nodes; + +namespace HotChocolate.Fusion.Execution.Introspection; + +// ReSharper disable once InconsistentNaming +internal sealed class __Schema : ITypeResolverInterceptor +{ + public void OnApplyResolver(string fieldName, IFeatureCollection features) + { + switch (fieldName) + { + case "description": + features.Set(new ResolveFieldValue(Description)); + break; + + case "types": + features.Set(new ResolveFieldValue(Types)); + break; + + case "queryType": + features.Set(new ResolveFieldValue(QueryType)); + break; + + case "mutationType": + features.Set(new ResolveFieldValue(MutationType)); + break; + + case "subscriptionType": + features.Set(new ResolveFieldValue(SubscriptionType)); + break; + + case "directives": + features.Set(new ResolveFieldValue(Directives)); + break; + } + } + + public static void Description(FieldContext context) + => context.WriteValue(context.Schema.Description); + + public static void Types(FieldContext context) + { + var list = context.ResultPool.RentObjectListResult(); + context.FieldResult.SetNextValue(list); + + foreach (var type in context.Schema.Types) + { + context.AddRuntimeResult(type); + list.SetNextValue(context.RentInitializedObjectResult()); + } + } + + public static void QueryType(FieldContext context) + { + context.AddRuntimeResult(context.Schema.QueryType); + context.FieldResult.SetNextValue(context.RentInitializedObjectResult()); + } + + public static void MutationType(FieldContext context) + { + if (context.Schema.MutationType is not null) + { + context.AddRuntimeResult(context.Schema.MutationType); + context.FieldResult.SetNextValue(context.RentInitializedObjectResult()); + } + } + + public static void SubscriptionType(FieldContext context) + { + if (context.Schema.MutationType is not null) + { + context.AddRuntimeResult(context.Schema.MutationType); + context.FieldResult.SetNextValue(context.RentInitializedObjectResult()); + } + } + + public static void Directives(FieldContext context) + { + var list = context.ResultPool.RentObjectListResult(); + context.FieldResult.SetNextValue(list); + + foreach (var directiveDefinition in context.Schema.DirectiveDefinitions) + { + context.AddRuntimeResult(directiveDefinition); + list.SetNextValue(context.RentInitializedObjectResult()); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__Type.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__Type.cs new file mode 100644 index 00000000000..1143ecb1e37 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__Type.cs @@ -0,0 +1,247 @@ +using HotChocolate.Features; +using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Execution.Introspection; + +// ReSharper disable once InconsistentNaming +internal sealed class __Type : ITypeResolverInterceptor +{ + public void OnApplyResolver(string fieldName, IFeatureCollection features) + { + switch (fieldName) + { + case "kind": + features.Set(new ResolveFieldValue(Kind)); + break; + + case "name": + features.Set(new ResolveFieldValue(Name)); + break; + + case "description": + features.Set(new ResolveFieldValue(Description)); + break; + + case "fields": + features.Set(new ResolveFieldValue(Fields)); + break; + + case "interfaces": + features.Set(new ResolveFieldValue(Interfaces)); + break; + + case "possibleTypes": + features.Set(new ResolveFieldValue(PossibleTypes)); + break; + + case "enumValues": + features.Set(new ResolveFieldValue(EnumValues)); + break; + + case "inputFields": + features.Set(new ResolveFieldValue(InputFields)); + break; + + case "ofType": + features.Set(new ResolveFieldValue(OfType)); + break; + + case "isOneOf": + features.Set(new ResolveFieldValue(IsOneOf)); + break; + + case "specifiedBy": + features.Set(new ResolveFieldValue(SpecifiedBy)); + break; + } + } + + public static void Kind(FieldContext context) + { + switch (context.Parent().Kind) + { + case TypeKind.Object: + context.WriteValue(__TypeKind.Object); + break; + + case TypeKind.Interface: + context.WriteValue(__TypeKind.Interface); + break; + + case TypeKind.Union: + context.WriteValue(__TypeKind.Union); + break; + + case TypeKind.InputObject: + context.WriteValue(__TypeKind.InputObject); + break; + + case TypeKind.Enum: + context.WriteValue(__TypeKind.Enum); + break; + + case TypeKind.Scalar: + context.WriteValue(__TypeKind.Scalar); + break; + + case TypeKind.List: + context.WriteValue(__TypeKind.List); + break; + + case TypeKind.NonNull: + context.WriteValue(__TypeKind.NonNull); + break; + } + } + + public static void Name(FieldContext context) + { + if (context.Parent() is ITypeDefinition typeDef) + { + context.WriteValue(typeDef.Name); + } + } + + public static void Description(FieldContext context) + { + if (context.Parent() is ITypeDefinition typeDef) + { + context.WriteValue(typeDef.Description); + } + } + + public static void Fields(FieldContext context) + { + var type = context.Parent(); + + if (type is IComplexTypeDefinition ct) + { + var includeDeprecated = context.ArgumentValue("includeDeprecated").Value; + var list = context.ResultPool.RentObjectListResult(); + context.FieldResult.SetNextValue(list); + + foreach (var field in ct.Fields) + { + if (field.IsIntrospectionField || (!includeDeprecated && field.IsDeprecated)) + { + continue; + } + + context.AddRuntimeResult(field); + list.SetNextValue(context.RentInitializedObjectResult()); + } + } + } + + public static void Interfaces(FieldContext context) + { + if (context.Parent() is IComplexTypeDefinition complexType) + { + var list = context.ResultPool.RentObjectListResult(); + context.FieldResult.SetNextValue(list); + + foreach (var type in complexType.Implements) + { + context.AddRuntimeResult(type); + list.SetNextValue(context.RentInitializedObjectResult()); + } + } + } + + public static void PossibleTypes(FieldContext context) + { + if (context.Parent() is ITypeDefinition nt && nt.IsAbstractType()) + { + var list = context.ResultPool.RentObjectListResult(); + context.FieldResult.SetNextValue(list); + + foreach (var type in context.Schema.GetPossibleTypes(nt)) + { + context.AddRuntimeResult(type); + list.SetNextValue(context.RentInitializedObjectResult()); + } + } + } + + public static void EnumValues(FieldContext context) + { + if (context.Parent() is IEnumTypeDefinition et) + { + var includeDeprecated = context.ArgumentValue("includeDeprecated").Value; + var list = context.ResultPool.RentObjectListResult(); + context.FieldResult.SetNextValue(list); + + foreach (var value in et.Values) + { + if (!includeDeprecated && value.IsDeprecated) + { + continue; + } + + context.AddRuntimeResult(value); + list.SetNextValue(context.RentInitializedObjectResult()); + } + } + } + + public static void InputFields(FieldContext context) + { + if (context.Parent() is IInputObjectTypeDefinition iot) + { + var includeDeprecated = context.ArgumentValue("includeDeprecated").Value; + var list = context.ResultPool.RentObjectListResult(); + context.FieldResult.SetNextValue(list); + + foreach (var value in iot.Fields) + { + if (!includeDeprecated && value.IsDeprecated) + { + continue; + } + + context.AddRuntimeResult(value); + list.SetNextValue(context.RentInitializedObjectResult()); + } + } + } + + public static void OfType(FieldContext context) + { + switch (context.Parent()) + { + case ListType lt: + { + var obj = context.RentInitializedObjectResult(); + context.FieldResult.SetNextValue(obj); + context.AddRuntimeResult(lt.ElementType); + break; + } + + case NonNullType nnt: + { + var obj = context.ResultPool.RentObjectListResult(); + context.FieldResult.SetNextValue(obj); + context.AddRuntimeResult(nnt.NullableType); + break; + } + } + } + + public static void IsOneOf(FieldContext context) + { + if (context.Parent() is IInputObjectTypeDefinition iot) + { + context.WriteValue(iot.Directives.ContainsName(DirectiveNames.OneOf.Name)); + } + } + + public static void SpecifiedBy(FieldContext context) + { + if (context.Parent() is IScalarTypeDefinition { SpecifiedBy: not null } scalar) + { + context.WriteValue(scalar.SpecifiedBy); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__TypeKind.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__TypeKind.cs new file mode 100644 index 00000000000..2fd69d0e5d6 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Introspection/__TypeKind.cs @@ -0,0 +1,14 @@ +namespace HotChocolate.Fusion.Execution.Introspection; + +// ReSharper disable once InconsistentNaming +internal static class __TypeKind +{ + public static ReadOnlySpan Scalar => "SCALAR"u8; + public static ReadOnlySpan Object => "OBJECT"u8; + public static ReadOnlySpan Interface => "INTERFACE"u8; + public static ReadOnlySpan Union => "UNION"u8; + public static ReadOnlySpan Enum => "ENUM"u8; + public static ReadOnlySpan InputObject => "INPUT_OBJECT"u8; + public static ReadOnlySpan List => "LIST"u8; + public static ReadOnlySpan NonNull => "NON_NULL"u8; +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/FieldContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/FieldContext.cs new file mode 100644 index 00000000000..b579e9382ce --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/FieldContext.cs @@ -0,0 +1,17 @@ +using HotChocolate.Buffers; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Execution.Nodes; + +internal abstract class FieldContext +{ + public abstract ResultPoolSession ResultPool { get; } + public abstract PooledArrayWriter Memory { get; } + public abstract ISchemaDefinition Schema { get; } + public abstract Selection Selection { get; } + public abstract FieldResult FieldResult { get; } + public abstract ulong IncludeFlags { get; } + public abstract T Parent(); + public abstract T ArgumentValue(string name) where T : IValueNode; + public abstract void AddRuntimeResult(T result); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/IntrospectionExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/IntrospectionExecutionNode.cs new file mode 100644 index 00000000000..f18184c5799 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/IntrospectionExecutionNode.cs @@ -0,0 +1,124 @@ +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Execution.Nodes; + +public sealed class IntrospectionExecutionNode : ExecutionNode +{ + private readonly Selection[] _selections; + + public IntrospectionExecutionNode(int id, Selection[] selections) + { + Id = id; + _selections = selections; + } + + public override int Id { get; } + + public override ReadOnlySpan Dependencies => default; + + public override Task ExecuteAsync( + OperationPlanContext context, + CancellationToken cancellationToken = default) + { + var resultPool = context.ResultPool; + var backlog = new Stack<(object? Parent, Selection Selection, FieldResult Result)>(); + var root = context.ResultPool.RentObjectResult(); + var selectionSet = context.OperationPlan.Operation.RootSelectionSet; + root.Initialize(resultPool, selectionSet, context.IncludeFlags, rawLeafFields: true); + + foreach (var selection in _selections) + { + if (selection.Resolver is null + || !selection.Field.IsIntrospectionField + || !selection.IsIncluded(context.IncludeFlags)) + { + continue; + } + + backlog.Push((null, selection, root[selection.ResponseName])); + } + + ExecuteSelections(context, backlog); + context.AddPartialResults(root, _selections); + + return Task.FromResult(new ExecutionStatus(Id, IsSkipped: false)); + } + + private static void ExecuteSelections( + OperationPlanContext context, + Stack<(object? Parent, Selection Selection, FieldResult Result)> backlog) + { + var operation = context.OperationPlan.Operation; + var fieldContext = new ReusableFieldContext( + context.Schema, + context.Variables, + context.IncludeFlags, + context.ResultPool, + context.CreateRentedBuffer()); + + while (backlog.TryPop(out var current)) + { + var (parent, selection, result) = current; + fieldContext.Initialize(parent, selection, result); + + selection.Resolver?.Invoke(fieldContext); + + if (!selection.IsLeaf) + { + if (result is ObjectFieldResult { HasNullValue: false } objectFieldResult) + { + var objectType = selection.Type.NamedType(); + var selectionSet = operation.GetSelectionSet(selection, objectType); + var objectResult = objectFieldResult.Value; + var insertIndex = 0; + + for (var i = 0; i < selectionSet.Selections.Length; i++) + { + var childSelection = selectionSet.Selections[i]; + + if (!childSelection.IsIncluded(context.IncludeFlags)) + { + continue; + } + + backlog.Push((fieldContext.RuntimeResults[0], childSelection, objectResult.Fields[insertIndex++])); + } + } + else if (result is ListFieldResult { HasNullValue: false, Value: ObjectListResult list }) + { + var objectType = selection.Type.NamedType(); + var selectionSet = operation.GetSelectionSet(selection, objectType); + + for (var i = 0; i < list.Items.Count; i++) + { + var objectResult = list.Items[i]; + var runtimeResult = fieldContext.RuntimeResults[i]; + + if (objectResult is null) + { + continue; + } + + var insertIndex = 0; + + for (var j = 0; j < selectionSet.Selections.Length; j++) + { + var childSelection = selectionSet.Selections[j]; + + if (!childSelection.IsIncluded(context.IncludeFlags)) + { + continue; + } + + backlog.Push((runtimeResult, childSelection, objectResult.Fields[insertIndex++])); + } + } + } + } + } + } + + protected internal override void Seal() + { + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationCompiler.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationCompiler.cs index de37d7dd1d9..03a178328e4 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationCompiler.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationCompiler.cs @@ -9,6 +9,7 @@ public sealed class OperationCompiler { private readonly ISchemaDefinition _schema; private readonly ObjectPool>> _fieldsPool; + private readonly TypeNameField _typeNameField; public OperationCompiler( ISchemaDefinition schema, @@ -19,6 +20,8 @@ public OperationCompiler( _schema = schema; _fieldsPool = fieldsPool; + var nonNullStringType = new NonNullType(_schema.Types.GetType(SpecScalarNames.String)); + _typeNameField = new TypeNameField(nonNullStringType); } public Operation Compile(string id, OperationDefinitionNode operationDefinition) @@ -215,7 +218,9 @@ private SelectionSet BuildSelectionSet( CollapseIncludeFlags(includeFlags); } - var field = typeContext.Fields[first.Node.Name.Value]; + var field = first.Node.Name.Value.Equals(IntrospectionFieldNames.TypeName) + ? _typeNameField + : typeContext.Fields[first.Node.Name.Value]; selections[i++] = new Selection( ++lastId, diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ResolveFieldValue.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ResolveFieldValue.cs new file mode 100644 index 00000000000..91966c93515 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ResolveFieldValue.cs @@ -0,0 +1,4 @@ +namespace HotChocolate.Fusion.Execution.Nodes; + +internal delegate void ResolveFieldValue( + FieldContext context); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ReusableFieldContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ReusableFieldContext.cs new file mode 100644 index 00000000000..03ef418021e --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ReusableFieldContext.cs @@ -0,0 +1,103 @@ +using HotChocolate.Buffers; +using HotChocolate.Execution; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Execution.Nodes; + +internal sealed class ReusableFieldContext( + ISchemaDefinition schema, + IVariableValueCollection variableValues, + ulong includeFlags, + ResultPoolSession resultPool, + PooledArrayWriter memory) + : FieldContext +{ + private readonly Dictionary _arguments = []; + private readonly List _runtimeResults = []; + private Selection _selection = null!; + private object? _parent; + private FieldResult _result = null!; + + public override ResultPoolSession ResultPool => resultPool; + + public override PooledArrayWriter Memory => memory; + + public override ISchemaDefinition Schema => schema; + + public override Selection Selection => _selection; + + public override FieldResult FieldResult => _result; + + public List RuntimeResults => _runtimeResults; + + public override ulong IncludeFlags => includeFlags; + + public override T Parent() => (T)_parent!; + + public override T ArgumentValue(string name) + { + if (_arguments.TryGetValue(name, out var value)) + { + if (value is T casted) + { + return casted; + } + + // todo: add proper exception. + throw new Exception("Invalid argument value!"); + } + + // todo: add proper exception. + throw new Exception("Invalid argument name!"); + } + + public override void AddRuntimeResult(T result) + { + _runtimeResults.Add(result); + } + + public void Initialize(object? parent, Selection selection, FieldResult result) + { + _parent = parent; + _result = result; + _selection = selection; + _runtimeResults.Clear(); + CoerceArgumentValues(selection); + } + + private void CoerceArgumentValues(Selection selection) + { + _arguments.Clear(); + + if (selection.Field.Arguments.Count == 0) + { + return; + } + + var syntaxNode = selection.SyntaxNodes[0].Node; + + foreach (var argument in selection.Field.Arguments) + { + var argumentValue = syntaxNode.Arguments.FirstOrDefault( + t => t.Name.Value.Equals(argument.Name, StringComparison.Ordinal)) + ?.Value; + + if (argumentValue is VariableNode variable + && variableValues.TryGetValue(variable.Name.Value, out IValueNode? variableValue)) + { + argumentValue = variableValue; + } + + argumentValue ??= argument.DefaultValue; + + if (argument.Type.IsNonNullType() && argumentValue is null or NullValueNode) + { + // TODO: The argument value is invalid. + throw new Exception("The argument value is invalid."); + } + + _arguments.Add(argument.Name, argumentValue ?? NullValueNode.Default); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs index bafe00604d5..3ea38955fe7 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs @@ -1,3 +1,4 @@ +using HotChocolate.Language; using HotChocolate.Types; namespace HotChocolate.Fusion.Execution.Nodes; @@ -31,6 +32,11 @@ public Selection( _syntaxNodes = syntaxNodes; _includeFlags = includeFlags; _flags = isInternal ? Flags.Internal : Flags.None; + + if (field.Type.NamedType().IsLeafType()) + { + _flags |= Flags.Leaf; + } } public uint Id { get; } @@ -39,6 +45,8 @@ public Selection( public bool IsInternal => (_flags & Flags.Internal) == Flags.Internal; + public bool IsLeaf => (_flags & Flags.Leaf) == Flags.Leaf; + public IOutputFieldDefinition Field { get; } public IType Type => Field.Type; @@ -47,6 +55,8 @@ public Selection( public ReadOnlySpan SyntaxNodes => _syntaxNodes; + internal ResolveFieldValue? Resolver => Field.Features.Get(); + public bool IsIncluded(ulong includeFlags) { if (_includeFlags.Length == 0) @@ -64,8 +74,7 @@ public bool IsIncluded(ulong includeFlags) { var flags1 = _includeFlags[0]; var flags2 = _includeFlags[1]; - return (flags1 & includeFlags) == flags1 - || (flags2 & includeFlags) == flags2; + return (flags1 & includeFlags) == flags1 || (flags2 & includeFlags) == flags2; } if (_includeFlags.Length == 3) @@ -107,6 +116,7 @@ private enum Flags { None = 0, Internal = 1, - Sealed = 2 + Leaf = 2, + Sealed = 4 } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/TypeNameField.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/TypeNameField.cs new file mode 100644 index 00000000000..a5ca6f3a23e --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/TypeNameField.cs @@ -0,0 +1,62 @@ +using HotChocolate.Features; +using HotChocolate.Fusion.Execution.Introspection; +using HotChocolate.Fusion.Types.Collections; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Execution.Nodes; + +internal sealed class TypeNameField : IOutputFieldDefinition +{ + public TypeNameField(IOutputType nonNullStringType) + { + ArgumentNullException.ThrowIfNull(nonNullStringType); + Type = nonNullStringType; + var features = new FeatureCollection(); + features.Set(new ResolveFieldValue( + ctx => ctx.WriteValue(ctx.Selection.DeclaringSelectionSet.DeclaringOperation.RootType.Name))); + Features = features.ToReadOnly(); + } + + public string Name => IntrospectionFieldNames.TypeName; + + public string? Description => null; + + public IComplexTypeDefinition DeclaringType + => throw new NotSupportedException("__typename is a function not an actual field belonging to a type."); + + public ITypeSystemMember DeclaringMember + => DeclaringType; + + public SchemaCoordinate Coordinate + => throw new NotSupportedException("__typename is a function not an actual field belonging to a type."); + + public bool IsDeprecated => false; + + public string? DeprecationReason => null; + + public IReadOnlyDirectiveCollection Directives => FusionDirectiveCollection.Empty; + + public IReadOnlyFieldDefinitionCollection Arguments + => FusionInputFieldDefinitionCollection.Empty; + + public IOutputType Type { get; } + + IType IFieldDefinition.Type => Type; + + public FieldFlags Flags => FieldFlags.TypeNameIntrospectionField; + + public IFeatureCollection Features { get; } + + public FieldDefinitionNode ToSyntaxNode() + => new FieldDefinitionNode( + null, + new NameNode(IntrospectionFieldNames.TypeName), + null, + [], + new NonNullTypeNode(new NamedTypeNode("String")), + []); + + ISyntaxNode ISyntaxNodeProvider.ToSyntaxNode() + => ToSyntaxNode(); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs index 0a35bf98337..63e178d96c8 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs @@ -1,4 +1,6 @@ +using System.Collections.Concurrent; using System.Collections.Immutable; +using HotChocolate.Buffers; using HotChocolate.Execution; using HotChocolate.Features; using HotChocolate.Fusion.Execution.Clients; @@ -23,17 +25,19 @@ public OperationPlanContext( OperationPlan = operationPlan; RequestContext = requestContext; Variables = variables; + IncludeFlags = operationPlan.Operation.CreateIncludeFlags(variables); // TODO : fully implement and inject ResultPoolSession _resultStore = new FetchResultStore( RequestContext.Schema, resultPoolSession, operationPlan.Operation, - operationPlan.Operation.CreateIncludeFlags(variables)); + IncludeFlags); // create a client scope for the current request context. var clientScopeFactory = requestContext.RequestServices.GetRequiredService(); ClientScope = clientScopeFactory.CreateScope(requestContext.Schema); + ResultPool = resultPoolSession; } public OperationExecutionPlan OperationPlan { get; } @@ -46,6 +50,10 @@ public OperationPlanContext( public ISourceSchemaClientScope ClientScope { get; } + public ResultPoolSession ResultPool { get; } + + public ulong IncludeFlags { get; } + public IFeatureCollection Features => RequestContext.Features; public ImmutableArray CreateVariableValueSets( @@ -75,6 +83,12 @@ public ImmutableArray CreateVariableValueSets( public void AddPartialResults(SelectionPath sourcePath, ReadOnlySpan results) => _resultStore.AddPartialResults(sourcePath, results); + public void AddPartialResults(ObjectResult result, ReadOnlySpan selections) + => _resultStore.AddPartialResults(result, selections); + + public PooledArrayWriter CreateRentedBuffer() + => _resultStore.CreateRentedBuffer(); + internal IExecutionResult CreateFinalResult() { return OperationResultBuilder.New() @@ -131,9 +145,9 @@ file static class OperationPlanContextExtensions { public static OperationResultBuilder RegisterForCleanup( this OperationResultBuilder builder, - IEnumerable disposables) + ConcurrentStack disposables) { - foreach (var disposable in disposables) + while (disposables.TryPop(out var disposable)) { builder.RegisterForCleanup(() => { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 8b2f1733fda..30f8caf2353 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -20,17 +20,15 @@ namespace HotChocolate.Fusion.Execution; internal sealed class FetchResultStore : IDisposable { private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.NoRecursion); - private readonly ResultPoolSession _resultPoolSession; private readonly ValueCompletion _valueCompletion; private readonly Operation _operation; - private readonly ObjectResult _root; private readonly ulong _includeFlags; + private readonly ObjectResult _root; private readonly ImmutableArray _errors = []; // TODO : attach resources to result object. private readonly ConcurrentStack _memory = []; - private bool _isInitialized; public FetchResultStore( ISchemaDefinition schema, @@ -42,18 +40,18 @@ public FetchResultStore( ArgumentNullException.ThrowIfNull(resultPoolSession); ArgumentNullException.ThrowIfNull(operation); - _resultPoolSession = resultPoolSession; - _valueCompletion = new ValueCompletion(schema, resultPoolSession, ErrorHandling.Propagate, 32, includeFlags); _operation = operation; - _root = resultPoolSession.RentObjectResult(); _includeFlags = includeFlags; + _valueCompletion = new ValueCompletion(schema, resultPoolSession, ErrorHandling.Propagate, 32, includeFlags); + _root = resultPoolSession.RentObjectResult(); + _root.Initialize(resultPoolSession, operation.RootSelectionSet, includeFlags); } public ObjectResult Data => _root; public ImmutableArray Errors => _errors; - public IEnumerable MemoryOwners => _memory; + public ConcurrentStack MemoryOwners => _memory; public bool AddPartialResults( SelectionPath sourcePath, @@ -95,6 +93,28 @@ public bool AddPartialResults( } } + public void AddPartialResults(ObjectResult result, ReadOnlySpan selections) + { + _lock.EnterWriteLock(); + + try + { + foreach (var selection in selections) + { + if (!selection.IsIncluded(_includeFlags)) + { + continue; + } + + result.MoveFieldTo(selection.ResponseName, _root); + } + } + finally + { + _lock.ExitWriteLock(); + } + } + private bool SaveSafe( ReadOnlySpan results, ReadOnlySpan startElements) @@ -112,13 +132,6 @@ private bool SaveSafe( if (result.Path.IsRoot) { var selectionSet = _operation.RootSelectionSet; - - if (!_isInitialized) - { - _root.Initialize(_resultPoolSession, selectionSet, _includeFlags); - _isInitialized = true; - } - if (!_valueCompletion.BuildResult(selectionSet, result, startElement, _root)) { return false; @@ -374,7 +387,7 @@ private static ReadOnlySpan GetRawValue(JsonElement value) #endif } - private PooledArrayWriter CreateRentedBuffer() + public PooledArrayWriter CreateRentedBuffer() { var buffer = new PooledArrayWriter(); _memory.Push(buffer); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ListFieldResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ListFieldResult.cs index d27380322f9..2e8a8e4ee34 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ListFieldResult.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ListFieldResult.cs @@ -52,6 +52,9 @@ public override void SetNextValue(ResultData value) listResult.SetParent(Parent!, ParentIndex); } + protected override void OnSetParent(ResultData parent, int index) + => Value?.SetParent(parent, index); + /// /// Writes the list field to the specified JSON writer. /// diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ObjectFieldResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ObjectFieldResult.cs index 5b0f7991295..4fc22b6a9a4 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ObjectFieldResult.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ObjectFieldResult.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using HotChocolate.Execution; @@ -39,6 +40,9 @@ public override void SetNextValue(ResultData value) objectResult.SetParent(Parent!, ParentIndex); } + protected override void OnSetParent(ResultData parent, int index) + => Value?.SetParent(parent, index); + /// /// Writes the object result to a JSON writer. /// @@ -73,6 +77,7 @@ public override void WriteTo( } /// + [MemberNotNullWhen(false, nameof(Value))] public override bool HasNullValue => Value is null; /// diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ObjectResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ObjectResult.cs index f6b9e3b045b..697f0f14164 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ObjectResult.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ObjectResult.cs @@ -16,7 +16,7 @@ public sealed class ObjectResult : ResultData, IReadOnlyDictionary _fieldMap = []; - private FieldResult[] _buffer = []; + private FieldResult[] _fields = []; /// /// Gets the selection set represents the structure of this object result. @@ -31,7 +31,7 @@ public sealed class ObjectResult : ResultData, IReadOnlyDictionary /// Gets the fields of the object result. /// - public ReadOnlySpan Fields => _buffer.AsSpan(0, _fieldMap.Count); + public ReadOnlySpan Fields => _fields.AsSpan(0, _fieldMap.Count); /// /// Gets the number of fields in the object result. @@ -64,6 +64,14 @@ public sealed class ObjectResult : ResultData, IReadOnlyDictionary _fieldMap.TryGetValue(responseName, out value); + internal void MoveFieldTo(string fieldName, ObjectResult target) + { + var field = _fieldMap[fieldName]; + field.SetParent(target, field.ParentIndex); + target._fields[field.ParentIndex] = field; + target._fieldMap[fieldName] = field; + } + /// /// Writes the object result to the specified JSON writer. /// @@ -84,7 +92,7 @@ public override void WriteTo( { writer.WriteStartObject(); - var fields = _buffer.AsSpan(0, _fieldMap.Count); + var fields = _fields.AsSpan(0, _fieldMap.Count); ref var field = ref MemoryMarshal.GetReference(fields); ref var end = ref Unsafe.Add(ref field, fields.Length); @@ -115,17 +123,24 @@ public override void WriteTo( /// /// The include flags. /// - public void Initialize(ResultPoolSession resultPoolSession, SelectionSet selectionSet, ulong includeFlags) + /// + /// Leaf fields will be stored as raw memory. + /// + public void Initialize( + ResultPoolSession resultPoolSession, + SelectionSet selectionSet, + ulong includeFlags, + bool rawLeafFields = false) { ArgumentNullException.ThrowIfNull(resultPoolSession); ArgumentNullException.ThrowIfNull(selectionSet); SelectionSet = selectionSet; - if (_buffer.Length < selectionSet.Selections.Length) + if (_fields.Length < selectionSet.Selections.Length) { _fieldMap.EnsureCapacity(selectionSet.Selections.Length); - _buffer = new FieldResult[selectionSet.Selections.Length]; + _fields = new FieldResult[selectionSet.Selections.Length]; } var insertIndex = 0; @@ -139,8 +154,8 @@ public void Initialize(ResultPoolSession resultPoolSession, SelectionSet selecti } var ii = insertIndex++; - var field = CreateFieldResult(this, ii, resultPoolSession, selection); - _buffer[ii] = field; + var field = CreateFieldResult(this, ii, resultPoolSession, selection, rawLeafFields); + _fields[ii] = field; _fieldMap.Add(selection.ResponseName, field); } @@ -148,7 +163,8 @@ static FieldResult CreateFieldResult( ResultData parent, int parentIndex, ResultPoolSession resultPoolSession, - Selection selection) + Selection selection, + bool rawLeafFields) { FieldResult field; @@ -156,9 +172,17 @@ static FieldResult CreateFieldResult( { field = resultPoolSession.RentListFieldResult(); } - else if (selection.Field.Type.NamedType().IsLeafType()) + else if (selection.IsLeaf) { - field = resultPoolSession.RentLeafFieldResult(); + if (rawLeafFields) + { + // TODO : shall we pool these as well? + field = new RawFieldResult(); + } + else + { + field = resultPoolSession.RentLeafFieldResult(); + } } else { @@ -179,7 +203,7 @@ internal override void SetCapacity(int capacity, int maxAllowedCapacity) _maxAllowedCapacity = maxAllowedCapacity; _fieldMap.EnsureCapacity(capacity); - _buffer = new FieldResult[capacity]; + _fields = new FieldResult[capacity]; } /// @@ -192,14 +216,14 @@ public override bool Reset() { SelectionSet = null!; - if (_fieldMap.Count > _buffer.Length) + if (_fieldMap.Count > _fields.Length) { return false; } for (var i = 0; i < _fieldMap.Count; i++) { - _buffer[i] = null!; + _fields[i] = null!; } #if NET9_0_OR_GREATER @@ -237,7 +261,7 @@ public override bool Reset() { for (var i = 0; i < _fieldMap.Count; i++) { - yield return _buffer[i].AsKeyValuePair(); + yield return _fields[i].AsKeyValuePair(); } } @@ -245,7 +269,7 @@ IEnumerator IEnumerable.GetEnumerator() { for (var i = 0; i < _fieldMap.Count; i++) { - yield return _buffer[i].AsKeyValuePair(); + yield return _fields[i].AsKeyValuePair(); } } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/RawFieldResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/RawFieldResult.cs new file mode 100644 index 00000000000..be8afeb9bb1 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/RawFieldResult.cs @@ -0,0 +1,53 @@ +using System.Text.Json; +using HotChocolate.Buffers; +using HotChocolate.Execution; + +namespace HotChocolate.Fusion.Execution; + +public sealed class RawFieldResult : FieldResult +{ + public override bool HasNullValue => Value.Length == 0; + + public ReadOnlyMemorySegment Value { get; private set; } + + public override void SetNextValue(ReadOnlyMemorySegment value) => Value = value; + + public override void WriteTo( + Utf8JsonWriter writer, + JsonSerializerOptions? options = null, + JsonNullIgnoreCondition nullIgnoreCondition = JsonNullIgnoreCondition.None) + { + writer.WritePropertyName(Selection.ResponseName); + + var span = Value.Span; + + if (span.Length == 0) + { + writer.WriteNullValue(); + } + + switch (span[0]) + { + case RawFieldValueType.String: + writer.WriteStringValue(span[1..]); + break; + + case RawFieldValueType.Boolean when span[1] == 0: + writer.WriteBooleanValue(false); + break; + + case RawFieldValueType.Boolean when span[1] == 1: + writer.WriteBooleanValue(true); + break; + } + } + + protected internal override KeyValuePair AsKeyValuePair() + => new(Selection.ResponseName, Value); + + public override bool Reset() + { + Value = default; + return base.Reset(); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/RawFieldValueType.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/RawFieldValueType.cs new file mode 100644 index 00000000000..f3806980626 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/RawFieldValueType.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.Fusion.Execution; + +public static class RawFieldValueType +{ + public const byte String = 1; + public const byte Boolean = 2; +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultData.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultData.cs index 34424ec5879..9a4098f3891 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultData.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ResultData.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using HotChocolate.Buffers; using HotChocolate.Execution; namespace HotChocolate.Fusion.Execution; @@ -78,6 +79,11 @@ protected internal void SetParent(ResultData parent, int index) Parent = parent; ParentIndex = index; + OnSetParent(parent, index); + } + + protected virtual void OnSetParent(ResultData parent, int index) + { } /// @@ -106,6 +112,15 @@ public virtual void SetNextValue(JsonElement value) throw new NotSupportedException(); } + /// + /// Sets the next value to the given value. + /// + /// The value to set. + public virtual void SetNextValue(ReadOnlyMemorySegment value) + { + throw new NotSupportedException(); + } + /// /// Tries to set the next value to . /// diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs index 783cdc4dedf..9b068f1a9de 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs @@ -10,16 +10,30 @@ public sealed partial class OperationPlanner /// Builds the actual execution plan from the provided . /// private OperationExecutionPlan BuildExecutionPlan( - string id, + Operation operation, + OperationDefinitionNode operationDefinition, ImmutableList planSteps, - OperationDefinitionNode originalOperation, - OperationDefinitionNode internalOperation) + bool isIntrospectionOnly) { + if (isIntrospectionOnly) + { + var introspectionNode = new IntrospectionExecutionNode(1, [.. operation.RootSelectionSet.Selections]); + introspectionNode.Seal(); + + return new OperationExecutionPlan + { + Operation = operation, + OperationDefinition = operationDefinition, + RootNodes = [introspectionNode], + AllNodes = [introspectionNode] + }; + } + var completedSteps = new HashSet(); var completedNodes = new Dictionary(); var dependencyLookup = new Dictionary>(); - planSteps = PrepareSteps(planSteps, originalOperation, dependencyLookup); + planSteps = PrepareSteps(planSteps, operationDefinition, dependencyLookup); BuildExecutionNodes(planSteps, completedSteps, completedNodes, dependencyLookup); BuildDependencyStructure(completedNodes, dependencyLookup); @@ -33,7 +47,14 @@ private OperationExecutionPlan BuildExecutionPlan( .Select(t => t.Value) .ToImmutableArray(); - var operation = _operationCompiler.Compile(id, internalOperation); + if (operation.HasIntrospectionFields()) + { + var introspectionNode = new IntrospectionExecutionNode( + allNodes.Max(t => t.Id) + 1, + operation.GetIntrospectionSelections()); + rootNodes = rootNodes.Add(introspectionNode); + allNodes = allNodes.Add(introspectionNode); + } foreach (var node in allNodes) { @@ -43,7 +64,7 @@ private OperationExecutionPlan BuildExecutionPlan( return new OperationExecutionPlan { Operation = operation, - OperationDefinition = originalOperation, + OperationDefinition = operationDefinition, RootNodes = rootNodes, AllNodes = allNodes }; @@ -218,3 +239,34 @@ private static void BuildDependencyStructure( } } } + +file static class Extensions +{ + public static bool HasIntrospectionFields(this Operation operation) + { + foreach (var selection in operation.RootSelectionSet.Selections) + { + if (selection.Field.IsIntrospectionField) + { + return true; + } + } + + return false; + } + + public static Selection[] GetIntrospectionSelections(this Operation operation) + { + var selections = new List(operation.RootSelectionSet.Selections.Length); + + foreach (var selection in operation.RootSelectionSet.Selections) + { + if (selection.Field.IsIntrospectionField) + { + selections.Add(selection); + } + } + + return selections.ToArray(); + } +} 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 468971a858a..422d19dd374 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs @@ -59,26 +59,30 @@ public OperationExecutionPlan CreatePlan(string id, OperationDefinitionNode oper foreach (var (schemaName, resolutionCost) in _schema.GetPossibleSchemas(selectionSet)) { possiblePlans.Enqueue( - node with - { - SchemaName = schemaName, - BacklogCost = 1 + resolutionCost - }); + node with { SchemaName = schemaName, BacklogCost = 1 + resolutionCost }); } - var (internalOperationDefinition, planSteps) = Plan(possiblePlans) - ?? throw new InvalidOperationException("The operation cannot be resolved."); + var plan = Plan(possiblePlans); + var internalOperationDefinition = plan.HasValue ? plan.Value.InternalOperationDefinition : operationDefinition; + var operation = _operationCompiler.Compile(id, internalOperationDefinition); + var isIntrospectionOnly = operation.IsIntrospectionOnly(); + + if (!plan.HasValue && !isIntrospectionOnly) + { + throw new InvalidOperationException("No possible plan was found for."); + } return BuildExecutionPlan( - id, + operation, + operationDefinition, // this is not ideal and are we going to rework this once we figured out // introspection and defer and stream. - planSteps.OfType().ToImmutableList(), - operationDefinition, - internalOperationDefinition); + plan.HasValue ? plan.Value.Steps.OfType().ToImmutableList() : [], + isIntrospectionOnly); } - private (OperationDefinitionNode, ImmutableList)? Plan(PriorityQueue possiblePlans) + private (OperationDefinitionNode InternalOperationDefinition, ImmutableList Steps)? Plan( + PriorityQueue possiblePlans) { while (possiblePlans.TryDequeue(out var current, out _)) { @@ -417,11 +421,7 @@ private void PlanInlineFieldWithRequirements( requirements = requirements.Add(argumentRequirementKey, operationRequirement); } - var updatedStep = currentStep with - { - Definition = operation, - Requirements = requirements - }; + var updatedStep = currentStep with { Definition = operation, Requirements = requirements }; steps = steps.SetItem(workItem.StepIndex, updatedStep); @@ -450,9 +450,9 @@ private void PlanFieldWithRequirement( { var selectionSetStub = new SelectionSet( workItem.Selection.SelectionSetId, - new SelectionSetNode([workItem.Selection.Node]), - workItem.Selection.Field.DeclaringType, - workItem.Selection.Path); + new SelectionSetNode([workItem.Selection.Node]), + workItem.Selection.Field.DeclaringType, + workItem.Selection.Path); current = InlineLookupRequirements(selectionSetStub, current, lookup, backlog); if (current.Steps.ById(workItem.StepId) is not OperationPlanStep currentStep) @@ -847,8 +847,7 @@ file static class Extensions { for (var i = 0; i < current.Steps.Count; i++) { - if (current.Steps[i] is OperationPlanStep step - && step.SelectionSets.Contains(selectionSetId)) + if (current.Steps[i] is OperationPlanStep step && step.SelectionSets.Contains(selectionSetId)) { yield return (step, i, step.SchemaName); } @@ -1126,6 +1125,21 @@ private static IEnumerable GetPossibleLookups(this ITypeDefinition type, return []; } + public static bool IsIntrospectionOnly(this Operation operation) + { + foreach (var selection in operation.RootSelectionSet.Selections) + { + if (selection.Field.IsIntrospectionField) + { + continue; + } + + return false; + } + + return true; + } + public static int NextId(this ImmutableList steps) => steps.LastOrDefault()?.Id + 1 ?? 1; } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/SelectionSetPartitioner.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/SelectionSetPartitioner.cs index fc3ebc01996..a1b6aea10da 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/SelectionSetPartitioner.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/SelectionSetPartitioner.cs @@ -173,6 +173,12 @@ void CompleteSelection(T original, T? resolvable, T? unresolvable, int index) FieldNode fieldNode, FieldNode? providedFieldNode) { + // the __typename field is available on all subgraphs + if (fieldNode.Name.Value.Equals(IntrospectionFieldNames.TypeName)) + { + return (fieldNode, null); + } + var field = complexType.Fields[fieldNode.Name.Value]; if (providedFieldNode is null) diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/IntrospectionTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/IntrospectionTests.cs new file mode 100644 index 00000000000..ea20941e16e --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/IntrospectionTests.cs @@ -0,0 +1,368 @@ +using HotChocolate.Transport.Http; +using HotChocolate.Types; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Fusion; + +public class IntrospectionTests : FusionTestBase +{ + [Fact] + public async Task Fetch_Schema_Types_Name() + { + // arrange + using var server1 = CreateSourceSchema( + "A", + b => b.AddQueryType()); + + using var server2 = CreateSourceSchema( + "B", + b => b.AddQueryType()); + + // act + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", server1), + ("B", server2), + ]); + + // assert + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + using var result = await client.PostAsync( + """ + { + __schema { + types { + name + } + } + } + """, + new Uri("http://localhost:5000/graphql")); + + // act + using var response = await result.ReadAsResultAsync(); + response.MatchSnapshot(); + } + + [Fact] + public async Task Fetch_Specific_Type() + { + // arrange + using var server1 = CreateSourceSchema( + "A", + b => b.AddQueryType()); + + using var server2 = CreateSourceSchema( + "B", + b => b.AddQueryType()); + + // act + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", server1), + ("B", server2), + ]); + + // assert + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + using var result = await client.PostAsync( + """ + { + __type(name: "String") { + name + } + } + """, + new Uri("http://localhost:5000/graphql")); + + // act + using var response = await result.ReadAsResultAsync(); + response.MatchSnapshot(); + } + + [Fact] + public async Task Typename_On_Query() + { + // arrange + using var server1 = CreateSourceSchema( + "A", + b => b.AddQueryType()); + + using var server2 = CreateSourceSchema( + "B", + b => b.AddQueryType()); + + // act + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", server1), + ("B", server2), + ]); + + // assert + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + using var result = await client.PostAsync( + """ + { + __typename + } + """, + new Uri("http://localhost:5000/graphql")); + + // act + using var response = await result.ReadAsResultAsync(); + response.MatchSnapshot(); + } + + [Fact] + public async Task Typename_On_Query_Skip_True() + { + // arrange + using var server1 = CreateSourceSchema( + "A", + b => b.AddQueryType()); + + using var server2 = CreateSourceSchema( + "B", + b => b.AddQueryType()); + + // act + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", server1), + ("B", server2), + ]); + + // assert + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + using var result = await client.PostAsync( + """ + query ($s: Boolean! = true) { + __typename @skip(if: $s) + } + """, + new Uri("http://localhost:5000/graphql")); + + // act + using var response = await result.ReadAsResultAsync(); + response.MatchSnapshot(); + } + + [Fact] + public async Task Typename_On_Query_Skip_False() + { + // arrange + using var server1 = CreateSourceSchema( + "A", + b => b.AddQueryType()); + + using var server2 = CreateSourceSchema( + "B", + b => b.AddQueryType()); + + // act + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", server1), + ("B", server2), + ]); + + // assert + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + using var result = await client.PostAsync( + """ + query ($s: Boolean! = false) { + __typename @skip(if: $s) + } + """, + new Uri("http://localhost:5000/graphql")); + + // act + using var response = await result.ReadAsResultAsync(); + response.MatchSnapshot(); + } + + [Fact] + public async Task Typename_On_Query_With_Alias() + { + // arrange + using var server1 = CreateSourceSchema( + "A", + b => b.AddQueryType()); + + using var server2 = CreateSourceSchema( + "B", + b => b.AddQueryType()); + + // act + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", server1), + ("B", server2), + ]); + + // assert + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + using var result = await client.PostAsync( + """ + { + a: __typename + } + """, + new Uri("http://localhost:5000/graphql")); + + // act + using var response = await result.ReadAsResultAsync(); + response.MatchSnapshot(); + } + + [Fact] + public async Task Typename_On_Object() + { + // arrange + using var server1 = CreateSourceSchema( + "A", + b => b.AddQueryType()); + + using var server2 = CreateSourceSchema( + "B", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", server1), + ("B", server2), + ]); + + // act + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + using var result = await client.PostAsync( + """ + { + books { + nodes { + __typename + } + } + } + """, + new Uri("http://localhost:5000/graphql")); + + // assert + using var response = await result.ReadAsResultAsync(); + response.MatchSnapshot(); + } + + [Fact] + public async Task Typename_On_Object_With_Alias() + { + // arrange + using var server1 = CreateSourceSchema( + "A", + b => b.AddQueryType()); + + using var server2 = CreateSourceSchema( + "B", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", server1), + ("B", server2), + ]); + + // act + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + using var result = await client.PostAsync( + """ + { + books { + nodes { + a: __typename + } + } + } + """, + new Uri("http://localhost:5000/graphql")); + + // assert + using var response = await result.ReadAsResultAsync(); + response.MatchSnapshot(); + } + + public static class SourceSchema1 + { + public record Book(int Id, string Title, Author Author); + + public record Author(int Id); + + public class Query + { + private readonly OrderedDictionary _books = + new OrderedDictionary() + { + [1] = new Book(1, "C# in Depth", new Author(1)), + [2] = new Book(2, "The Lord of the Rings", new Author(2)), + [3] = new Book(3, "The Hobbit", new Author(2)), + [4] = new Book(4, "The Silmarillion", new Author(2)) + }; + + [Lookup] + public Book GetBookById(int id) + => _books[id]; + + [UsePaging] + public IEnumerable GetBooks() + => _books.Values; + } + } + + public static class SourceSchema2 + { + public record Author(int Id, string Name) + { + public IEnumerable GetBooks() + { + if (Id == 1) + { + yield return new Book(1, this); + } + else + { + yield return new Book(2, this); + yield return new Book(3, this); + yield return new Book(4, this); + } + } + } + + public class Query + { + private readonly OrderedDictionary _authors = + new OrderedDictionary() + { + [1] = new Author(1, "Jon Skeet"), + [2] = new Author(2, "JRR Tolkien") + }; + + [Internal] + [Lookup] + public Author GetAuthorById(int id) + => _authors[id]; + + [UsePaging] + public IEnumerable GetAuthors() + => _authors.Values; + } + + public record Book(int Id, Author Author); + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Fetch_Schema_Types_Name.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Fetch_Schema_Types_Name.snap new file mode 100644 index 00000000000..52afd3622fe --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Fetch_Schema_Types_Name.snap @@ -0,0 +1,68 @@ +{ + "data": { + "__schema": { + "types": [ + { + "name": "__Schema" + }, + { + "name": "__Type" + }, + { + "name": "__TypeKind" + }, + { + "name": "__Field" + }, + { + "name": "__InputValue" + }, + { + "name": "__EnumValue" + }, + { + "name": "__Directive" + }, + { + "name": "__DirectiveLocation" + }, + { + "name": "Query" + }, + { + "name": "Author" + }, + { + "name": "AuthorsConnection" + }, + { + "name": "AuthorsEdge" + }, + { + "name": "Book" + }, + { + "name": "BooksConnection" + }, + { + "name": "BooksEdge" + }, + { + "name": "PageInfo" + }, + { + "name": "fusion__Schema" + }, + { + "name": "String" + }, + { + "name": "Boolean" + }, + { + "name": "Int" + } + ] + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Fetch_Specific_Type.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Fetch_Specific_Type.snap new file mode 100644 index 00000000000..33e73734922 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Fetch_Specific_Type.snap @@ -0,0 +1,7 @@ +{ + "data": { + "__type": { + "name": "String" + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Object.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Object.snap new file mode 100644 index 00000000000..942dcc4beeb --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Object.snap @@ -0,0 +1,20 @@ +{ + "data": { + "books": { + "nodes": [ + { + "__typename": "Book" + }, + { + "__typename": "Book" + }, + { + "__typename": "Book" + }, + { + "__typename": "Book" + } + ] + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Object_With_Alias.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Object_With_Alias.snap new file mode 100644 index 00000000000..655b304c037 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Object_With_Alias.snap @@ -0,0 +1,20 @@ +{ + "data": { + "books": { + "nodes": [ + { + "a": "Book" + }, + { + "a": "Book" + }, + { + "a": "Book" + }, + { + "a": "Book" + } + ] + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Query.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Query.snap new file mode 100644 index 00000000000..b1b150a5c46 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Query.snap @@ -0,0 +1,5 @@ +{ + "data": { + "__typename": "Query" + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Query_Skip_False.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Query_Skip_False.snap new file mode 100644 index 00000000000..b1b150a5c46 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Query_Skip_False.snap @@ -0,0 +1,5 @@ +{ + "data": { + "__typename": "Query" + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Query_Skip_True.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Query_Skip_True.snap new file mode 100644 index 00000000000..73cd5c32b8a --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Query_Skip_True.snap @@ -0,0 +1,3 @@ +{ + "data": {} +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Query_With_Alias.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Query_With_Alias.snap new file mode 100644 index 00000000000..8fa12848f5f --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Typename_On_Query_With_Alias.snap @@ -0,0 +1,5 @@ +{ + "data": { + "a": "Query" + } +} diff --git a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/StringSyntaxWriter.cs b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/StringSyntaxWriter.cs index eebddb22258..a2bc7a1007d 100644 --- a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/StringSyntaxWriter.cs +++ b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/StringSyntaxWriter.cs @@ -5,7 +5,6 @@ namespace HotChocolate.Language.Utilities; public class StringSyntaxWriter : ISyntaxWriter { private static readonly StringSyntaxWriterPool s_pool = new(); - private readonly StringBuilder _stringBuilder = new(); private int _indent; public static StringSyntaxWriter Rent() @@ -18,7 +17,7 @@ public static void Return(StringSyntaxWriter writer) s_pool.Return(writer); } - internal StringBuilder StringBuilder => _stringBuilder; + internal StringBuilder StringBuilder { get; } = new(); public void Indent() { @@ -35,19 +34,19 @@ public void Unindent() public void Write(char c) { - _stringBuilder.Append(c); + StringBuilder.Append(c); } public void Write(string s) { - _stringBuilder.Append(s); + StringBuilder.Append(s); } public void WriteIndent(bool condition = true) { if (condition && _indent > 0) { - _stringBuilder.Append(' ', 2 * _indent); + StringBuilder.Append(' ', 2 * _indent); } } @@ -55,7 +54,7 @@ public void WriteLine(bool condition = true) { if (condition) { - _stringBuilder.AppendLine(); + StringBuilder.AppendLine(); } } @@ -63,17 +62,17 @@ public void WriteSpace(bool condition = true) { if (condition) { - _stringBuilder.Append(' '); + StringBuilder.Append(' '); } } public void Clear() { - _stringBuilder.Clear(); + StringBuilder.Clear(); } public override string ToString() { - return _stringBuilder.ToString(); + return StringBuilder.ToString(); } } diff --git a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/StringSyntaxWriterPool.cs b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/StringSyntaxWriterPool.cs index 2c66e9b68c0..3f173449505 100644 --- a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/StringSyntaxWriterPool.cs +++ b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/StringSyntaxWriterPool.cs @@ -2,17 +2,11 @@ namespace HotChocolate.Language.Utilities; -internal sealed class StringSyntaxWriterPool - : DefaultObjectPool +internal sealed class StringSyntaxWriterPool() : DefaultObjectPool(new Policy(), 8) { - public StringSyntaxWriterPool() - : base(new Policy(), 8) - { - } - private sealed class Policy : IPooledObjectPolicy { - public StringSyntaxWriter Create() => new StringSyntaxWriter(); + public StringSyntaxWriter Create() => new(); public bool Return(StringSyntaxWriter obj) { diff --git a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.QuerySyntax.cs b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.QuerySyntax.cs index 5c8a989064f..8254a3bac83 100644 --- a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.QuerySyntax.cs +++ b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.QuerySyntax.cs @@ -17,7 +17,7 @@ private void VisitOperationDefinition( writer.Write(node.Operation.ToString().ToLowerInvariant()); } - if (node.Name is { }) + if (node.Name is not null) { writer.WriteSpace(); writer.WriteName(node.Name); @@ -72,7 +72,7 @@ private void VisitVariableDefinition(VariableDefinitionNode node, ISyntaxWriter writer.WriteType(node.Type); - if (node.DefaultValue is { }) + if (node.DefaultValue is not null) { writer.Write(" = "); writer.WriteValue(node.DefaultValue); @@ -229,7 +229,7 @@ private void VisitInlineFragment(InlineFragmentNode node, ISyntaxWriter writer) writer.Write("..."); - if (node.TypeCondition is { }) + if (node.TypeCondition is not null) { writer.WriteSpace(); writer.Write(Keywords.On); diff --git a/src/HotChocolate/Mutable/src/Types.Mutable/MutableScalarTypeDefinition.cs b/src/HotChocolate/Mutable/src/Types.Mutable/MutableScalarTypeDefinition.cs index f79c5433b96..ab2af38fbdd 100644 --- a/src/HotChocolate/Mutable/src/Types.Mutable/MutableScalarTypeDefinition.cs +++ b/src/HotChocolate/Mutable/src/Types.Mutable/MutableScalarTypeDefinition.cs @@ -13,7 +13,6 @@ public class MutableScalarTypeDefinition(string name) : INamedTypeSystemMemberDefinition , IScalarTypeDefinition , IMutableTypeDefinition - , IFeatureProvider { private DirectiveCollection? _directives; @@ -78,6 +77,28 @@ public bool IsAssignableFrom(ITypeDefinition type) return false; } + public Uri? SpecifiedBy + { + get + { + var specifiedBy = Directives.FirstOrDefault("specifiedBy"); + + if (specifiedBy is null) + { + return null; + } + + var url = specifiedBy.Arguments.First(t => t.Name.Equals("url", StringComparison.Ordinal)); + + if (url.Value is not StringValueNode urlValue) + { + throw new InvalidOperationException("The specified URL is not a valid URI."); + } + + return new Uri(urlValue.Value); + } + } + /// public bool IsInstanceOfType(IValueNode value) { diff --git a/src/HotChocolate/Primitives/src/Primitives/Types/TypeKind.cs b/src/HotChocolate/Primitives/src/Primitives/Types/TypeKind.cs index 096ea02ee63..5fb854cf2c8 100644 --- a/src/HotChocolate/Primitives/src/Primitives/Types/TypeKind.cs +++ b/src/HotChocolate/Primitives/src/Primitives/Types/TypeKind.cs @@ -125,7 +125,7 @@ public enum TypeKind /// /// /// GraphQL Enum types, like Scalar types, also represent leaf values in a GraphQL type system. - /// However Enum types describe the set of possible values. + /// However, Enum types describe the set of possible values. /// /// /// Enums are not references for a numeric value, but are unique values in their own right.