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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions src/Core/Parsers/IntrospectionInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using HotChocolate.AspNetCore;
using HotChocolate.Execution;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace Azure.DataApiBuilder.Core.Parsers
{
Expand All @@ -14,17 +15,18 @@ namespace Azure.DataApiBuilder.Core.Parsers
/// </summary>
public class IntrospectionInterceptor : DefaultHttpRequestInterceptor
{
private RuntimeConfigProvider _runtimeConfigProvider;

/// <summary>
/// Constructor injects RuntimeConfigProvider to allow
/// HotChocolate to attempt to retrieve the runtime config
/// when evaluating GraphQL requests.
/// Parameterless constructor.
/// </summary>
/// <param name="runtimeConfigProvider"></param>
public IntrospectionInterceptor(RuntimeConfigProvider runtimeConfigProvider)
/// <remarks>
/// Hot Chocolate v16 isolates schema services from the application's request services.
/// Resolving constructor-injected app singletons (e.g. <see cref="RuntimeConfigProvider"/>)
/// against the schema service provider therefore fails at executor session creation.
/// We instead resolve dependencies from <see cref="HttpContext.RequestServices"/> in
/// <see cref="OnCreateAsync"/>, where the application's request scope is in effect.
/// </remarks>
public IntrospectionInterceptor()
{
_runtimeConfigProvider = runtimeConfigProvider;
}

/// <summary>
Expand All @@ -51,7 +53,10 @@ public override ValueTask OnCreateAsync(
OperationRequestBuilder requestBuilder,
CancellationToken cancellationToken)
{
if (_runtimeConfigProvider.GetConfig().AllowIntrospection)
RuntimeConfigProvider runtimeConfigProvider =
context.RequestServices.GetRequiredService<RuntimeConfigProvider>();

if (runtimeConfigProvider.GetConfig().AllowIntrospection)
{
requestBuilder.AllowIntrospection();
}
Expand Down
7 changes: 4 additions & 3 deletions src/Core/Resolvers/CosmosQueryStructure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,15 @@ private static IEnumerable<LabelledColumn> GenerateQueryColumns(SelectionSetNode
[MemberNotNull(nameof(OrderByColumns))]
private void Init(IDictionary<string, object?> queryParams)
{
ISelection selection = _context.Selection;
Selection selection = _context.Selection;
ObjectType underlyingType = selection.Field.Type.NamedType<ObjectType>();

IsPaginated = QueryBuilder.IsPaginationType(underlyingType);
OrderByColumns = new();
FieldNode selectionFieldNode = selection.RequireFieldNode();
if (IsPaginated)
{
FieldNode? fieldNode = ExtractQueryField(selection.SyntaxNode);
FieldNode? fieldNode = ExtractQueryField(selectionFieldNode);

if (fieldNode is not null)
{
Expand All @@ -139,7 +140,7 @@ private void Init(IDictionary<string, object?> queryParams)
}
else
{
Columns.AddRange(GenerateQueryColumns(selection.SyntaxNode.SelectionSet!, _context.Operation.Document, SourceAlias));
Columns.AddRange(GenerateQueryColumns(selectionFieldNode.SelectionSet!, _context.Operation.Document, SourceAlias));
string typeName = GraphQLUtils.TryExtractGraphQLFieldModelName(underlyingType.Directives, out string? modelName) ?
Comment thread
aaronburtle marked this conversation as resolved.
modelName :
underlyingType.Name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public SqlQueryStructure(
sqlMetadataProvider,
authorizationResolver,
ctx.Selection.Field,
ctx.Selection.SyntaxNode,
ctx.Selection.RequireFieldNode(),
// The outermost query is where we start, so this can define
Comment thread
aaronburtle marked this conversation as resolved.
// create the IncrementingInteger that will be shared between
// all subqueries in this query.
Expand Down Expand Up @@ -173,7 +173,7 @@ public SqlQueryStructure(
IsMultipleCreateOperation = isMultipleCreateOperation;

ObjectField schemaField = _ctx.Selection.Field;
FieldNode? queryField = _ctx.Selection.SyntaxNode;
FieldNode? queryField = _ctx.Selection.RequireFieldNode();

IOutputType outputType = schemaField.Type;
_underlyingFieldType = outputType.NamedType<ObjectType>();
Expand Down
2 changes: 1 addition & 1 deletion src/Core/Services/DetermineStatusCodeMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public async ValueTask InvokeAsync(RequestContext context)
}

contextData[ExecutionContextData.HttpStatusCode] = HttpStatusCode.BadRequest;
context.Result = singleResult.WithContextData(contextData.ToImmutable());
singleResult.ContextData = contextData.ToImmutable();
}
}
}
Expand Down
20 changes: 15 additions & 5 deletions src/Core/Services/ExecutionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Net;
using System.Text;
using System.Text.Json;
using System.Xml;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
Expand All @@ -18,7 +19,6 @@
using Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries;
using HotChocolate.Execution;
using HotChocolate.Execution.Processing;
using HotChocolate.Language;
using HotChocolate.Resolvers;
using NodaTime.Text;
Expand Down Expand Up @@ -201,7 +201,7 @@ fieldValue.ValueKind is not (JsonValueKind.Undefined or JsonValueKind.Null))
return namedType switch
{
StringType => fieldValue.GetString(), // spec
ByteType => fieldValue.GetByte(),
UnsignedByteType => fieldValue.GetByte(),
ShortType => fieldValue.GetInt16(),
IntType => fieldValue.GetInt32(), // spec
LongType => fieldValue.GetInt64(),
Expand All @@ -211,11 +211,21 @@ fieldValue.ValueKind is not (JsonValueKind.Undefined or JsonValueKind.Null))
DateTimeType => DateTimeOffset.TryParse(fieldValue.GetString()!, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out DateTimeOffset date) ? date : null, // for DW when datetime is null it will be in "" (double quotes) due to stringagg parsing and hence we need to ensure parsing is correct.
DateType => DateTimeOffset.TryParse(fieldValue.GetString()!, out DateTimeOffset date) ? date : null,
HotChocolate.Types.NodaTime.LocalTimeType => fieldValue.GetString()!.Equals("null", StringComparison.OrdinalIgnoreCase) ? null : LocalTimePattern.ExtendedIso.Parse(fieldValue.GetString()!).Value,
ByteArrayType => fieldValue.GetBytesFromBase64(),
// HC v16 ships both ByteArrayType (legacy, GraphQL name "ByteArray", runtime byte[])
// and Base64StringType (new, GraphQL name "Base64String", runtime byte[]). DAB's
// generated schemas still use the GraphQL name "ByteArray", so HC binds entity
// fields to ByteArrayType. We accept either type here so DAB also tolerates
// schemas that bind to Base64StringType (e.g. via DefaultValueType).
// CS0618: ByteArrayType is [Obsolete] in HC v16 in favor of Base64StringType,
// but we still need to pattern-match it because it remains the type bound to
// the GraphQL name "ByteArray" that DAB-generated schemas continue to use.
#pragma warning disable CS0618
Base64StringType or ByteArrayType => fieldValue.GetBytesFromBase64(),
#pragma warning restore CS0618
BooleanType => fieldValue.GetBoolean(), // spec
UrlType => new Uri(fieldValue.GetString()!),
UuidType => fieldValue.GetGuid(),
TimeSpanType => TimeSpan.Parse(fieldValue.GetString()!),
DurationType => XmlConvert.ToTimeSpan(fieldValue.GetString()!),
AnyType => fieldValue.ToString(),
_ => fieldValue.GetString()
};
Expand Down Expand Up @@ -508,7 +518,7 @@ public static InputObjectType InputObjectTypeFromIInputField(IInputValueDefiniti
{
return GetParametersFromSchemaAndQueryFields(
context.Selection.Field,
context.Selection.SyntaxNode,
context.Selection.RequireFieldNode(),
context.Variables);
Comment thread
aaronburtle marked this conversation as resolved.
}

Expand Down
71 changes: 69 additions & 2 deletions src/Core/Services/GraphQLSchemaCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@ private ISchemaBuilder Parse(
// Generate the Query and the Mutation Node.
(DocumentNode queryNode, DocumentNode mutationNode) = GenerateQueryAndMutationNodes(root, inputTypes);

// Hot Chocolate v16 validates schemas eagerly during host startup
// (RequestExecutorWarmupService) and rejects an empty Query type with
// "The object type `Query` has to at least define one field in order to be valid."
// This can occur in valid runtime configurations: GraphQL globally disabled,
// every entity opting out of GraphQL via `graphql.enabled = false`, or no entities
// configured. Inject a hidden placeholder field with a no-op resolver so the schema
// is structurally valid; HC v16 also rejects fields without resolvers.
queryNode = EnsureQueryHasAtLeastOneField(queryNode, sb);

return sb
.AddDocument(root)
.AddAuthorizeDirectiveType()
Expand All @@ -122,12 +131,70 @@ private ISchemaBuilder Parse(
.AddDocument(queryNode)
// Generate the GraphQL mutations from the provided objects
.AddDocument(mutationNode)
// Enable the OneOf directive (https://github.com/graphql/graphql-spec/pull/825) to support the DefaultValue type
.ModifyOptions(o => o.EnableOneOf = true)
// Adds our type interceptor that will create the resolvers.
.TryAddTypeInterceptor(new ResolverTypeInterceptor(new ExecutionHelper(_queryEngineFactory, _mutationEngineFactory, _runtimeConfigProvider)));
}

/// <summary>
/// Name of the hidden placeholder field added to <c>Query</c> when no entity contributes
/// a query field, used to keep the schema valid for HC v16's eager validation.
/// </summary>
private const string EMPTY_SCHEMA_PLACEHOLDER_FIELD_NAME = "_dab";

/// <summary>
/// If the generated <c>Query</c> object type has no fields, append a hidden placeholder
/// field and register a null-returning resolver for it. The placeholder is shadowed in
/// any configuration that produces real query fields, so it is only visible in
/// otherwise-empty schemas (GraphQL globally disabled, all entities opting out,
/// no entities configured).
/// </summary>
private static DocumentNode EnsureQueryHasAtLeastOneField(DocumentNode queryNode, ISchemaBuilder sb)
{
ImmutableArray<IDefinitionNode>.Builder rewritten = ImmutableArray.CreateBuilder<IDefinitionNode>(queryNode.Definitions.Count);
bool placeholderInjected = false;

foreach (IDefinitionNode definition in queryNode.Definitions)
{
if (definition is ObjectTypeDefinitionNode objectType
&& objectType.Name.Value == "Query"
&& objectType.Fields.Count == 0)
{
FieldDefinitionNode placeholderField = new(
location: null,
new NameNode(EMPTY_SCHEMA_PLACEHOLDER_FIELD_NAME),
new StringValueNode(
"Internal placeholder; only present when no entity contributes a query field. "
+ "Always returns null and is never reachable in normal operation."),
arguments: new List<InputValueDefinitionNode>(),
type: new NamedTypeNode(new NameNode("String")),
directives: new List<DirectiveNode>());

rewritten.Add(new ObjectTypeDefinitionNode(
objectType.Location,
objectType.Name,
objectType.Description,
objectType.Directives,
objectType.Interfaces,
new List<FieldDefinitionNode> { placeholderField }));
placeholderInjected = true;
}
else
{
rewritten.Add(definition);
}
}

if (placeholderInjected)
{
// HC v16 requires every field to have a resolver; bind a no-op that always
// returns null. The field is unreachable in normal operation because callers
// for empty-Query configurations never issue GraphQL requests.
sb.AddResolver("Query", EMPTY_SCHEMA_PLACEHOLDER_FIELD_NAME, _ => null);
}

return new DocumentNode(rewritten.ToImmutable());
}

/// <summary>
/// Generate the GraphQL schema query and mutation nodes from the provided database.
/// </summary>
Expand Down
15 changes: 6 additions & 9 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,12 @@
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.2" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="HotChocolate" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.AspNetCore" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.AspNetCore.Authorization" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.ModelContextProtocol" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.Types.NodaTime" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.Utilities.Introspection" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.Transport.Http" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate.Diagnostics" Version="16.0.0-p.7.68" />
<PackageVersion Include="CookieCrumble" Version="16.0.0-p.7.68" />
<PackageVersion Include="HotChocolate" Version="16.0.0-rc.1.43" />
<PackageVersion Include="HotChocolate.AspNetCore" Version="16.0.0-rc.1.43" />
<PackageVersion Include="HotChocolate.AspNetCore.Authorization" Version="16.0.0-rc.1.43" />
<PackageVersion Include="HotChocolate.Types.NodaTime" Version="16.0.0-rc.1.43" />
<PackageVersion Include="HotChocolate.Utilities.Introspection" Version="16.0.0-rc.1.43" />
<PackageVersion Include="HotChocolate.Diagnostics" Version="16.0.0-rc.1.43" />
<PackageVersion Include="Humanizer" Version="2.14.1" />
<PackageVersion Include="Humanizer.Core" Version="2.14.1" />
<PackageVersion Include="DotNetEnv" Version="3.0.0" />
Expand Down
16 changes: 12 additions & 4 deletions src/Service.GraphQLBuilder/CustomScalars/SingleType.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json;
using HotChocolate.Language;
using HotChocolate.Text.Json;
using HotChocolate.Types;

namespace Azure.DataApiBuilder.Service.GraphQLBuilder.CustomScalars
Expand Down Expand Up @@ -48,10 +50,16 @@ public SingleType(
Description = description;
}

protected override float ParseLiteral(IFloatValueLiteral valueSyntax) =>
valueSyntax.ToSingle();
protected override float OnCoerceInputLiteral(IFloatValueLiteral valueLiteral)
=> valueLiteral.ToSingle();

protected override FloatValueNode ParseValue(float runtimeValue) =>
new(runtimeValue);
protected override float OnCoerceInputValue(JsonElement inputValue)
=> inputValue.GetSingle();

protected override void OnCoerceOutputValue(float runtimeValue, ResultElement resultValue)
=> resultValue.SetNumberValue(runtimeValue);

protected override IValueNode OnValueToLiteral(float runtimeValue)
=> new FloatValueNode(runtimeValue);
}
}
Loading
Loading