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
Original file line number Diff line number Diff line change
Expand Up @@ -775,14 +775,14 @@ private void AddGraphQLFields(IReadOnlyList<ISelectionNode> selections, RuntimeC
.First();

AddGraphQLFields(fragmentDocumentNode.SelectionSet.Selections, runtimeConfigProvider);
return;
continue;
}

if (node.Kind == SyntaxKind.InlineFragment)
{
InlineFragmentNode inlineFragment = (InlineFragmentNode)node;
AddGraphQLFields(inlineFragment.SelectionSet.Selections, runtimeConfigProvider);
return;
continue;
}

if (node.Kind != SyntaxKind.Field)
Expand Down
101 changes: 99 additions & 2 deletions src/Service.Tests/UnitTests/SqlQueryStructureHelperTests.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Core.Models;
using Azure.DataApiBuilder.Core.Resolvers;
using Azure.DataApiBuilder.Core.Services;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries;
using HotChocolate.Language;
using HotChocolate.Resolvers;
using Microsoft.AspNetCore.Http;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
Expand Down Expand Up @@ -127,6 +132,77 @@ public void AddGraphQLFields_InlineFragmentSkipsIntrospectionField()
new object?[] { new ISelectionNode[] { fragment }, null });
}

/// <summary>
/// Selections after an inline fragment must still be processed. Using return after
/// expanding the fragment exits the foreach and drops trailing fields.
/// </summary>
[TestMethod]
public void AddGraphQLFields_ContinuesAfterInlineFragment()
{
SqlQueryStructure structure = CreateStructureForColumnSelection();
InlineFragmentNode fragment = new(
location: null,
typeCondition: new NamedTypeNode("book"),
directives: System.Array.Empty<DirectiveNode>(),
selectionSet: new SelectionSetNode(new ISelectionNode[] { new FieldNode("id") }));

GetPrivateMethod("AddGraphQLFields").Invoke(
structure,
new object?[]
{
new ISelectionNode[] { fragment, new FieldNode("title") },
null
});

CollectionAssert.AreEquivalent(new[] { "id", "title" }, structure.Columns.Select(c => c.Label).ToList());
}

/// <summary>
/// Selections after a named fragment spread must still be processed.
/// This matches the #3861 query: <c>...bookFields</c> (selecting id) followed by title.
/// Using return after expanding the spread exits the foreach and drops the trailing field.
/// </summary>
[TestMethod]
public void AddGraphQLFields_ContinuesAfterNamedFragmentSpread()
{
const string query = """
query {
book_by_pk(id: 1) {
...bookFields
title
}
}

fragment bookFields on book { id }
""";

DocumentNode document = Utf8GraphQLParser.Parse(query);
OperationDefinitionNode operationDefinition = document.Definitions.OfType<OperationDefinitionNode>().Single();
FieldNode bookByPk = operationDefinition.SelectionSet.Selections.OfType<FieldNode>().Single();

// Operation.Document is a non-virtual property on a sealed type, so the document
// is assigned the same way other uninitialized query-structure state is in this fixture.
// The name is fully qualified because Azure.Core also defines Azure.Operation.
HotChocolate.Execution.Processing.Operation graphqlOperation =
(HotChocolate.Execution.Processing.Operation)RuntimeHelpers.GetUninitializedObject(
typeof(HotChocolate.Execution.Processing.Operation));
typeof(HotChocolate.Execution.Processing.Operation)
.GetField("<Document>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(graphqlOperation, document);
Mock<IMiddlewareContext> context = new();
context.SetupGet(ctx => ctx.Operation).Returns(graphqlOperation);

SqlQueryStructure structure = CreateStructureForColumnSelection();
typeof(SqlQueryStructure).GetField("_ctx", BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(structure, context.Object);

GetPrivateMethod("AddGraphQLFields").Invoke(
structure,
new object?[] { bookByPk.SelectionSet!.Selections, null });

CollectionAssert.AreEquivalent(new[] { "id", "title" }, structure.Columns.Select(c => c.Label).ToList());
}

[TestMethod]
public void AddGraphQLFields_UnsupportedSelectionThrows()
{
Expand Down Expand Up @@ -175,9 +251,30 @@ private static SqlQueryStructure CreateStructure()
return structure;
}

private static void SetAutoProperty<T>(SqlQueryStructure structure, string propertyName, T value)
private static SqlQueryStructure CreateStructureForColumnSelection()
{
SqlQueryStructure structure = CreateStructure();
Mock<ISqlMetadataProvider> metadata = new();
metadata
.Setup(m => m.TryGetBackingColumn(It.IsAny<string>(), It.IsAny<string>(), out It.Ref<string?>.IsAny))
.Returns((string _, string field, out string? backing) =>
{
backing = field;
return true;
});

SetAutoProperty(structure, "MetadataProvider", metadata.Object, typeof(BaseQueryStructure));
SetAutoProperty(structure, "DatabaseObject", new DatabaseTable("public", "books"), typeof(BaseQueryStructure));
SetAutoProperty(structure, "Columns", new List<LabelledColumn>(), typeof(BaseQueryStructure));
structure.EntityName = "book";
structure.SourceAlias = "table0";
return structure;
}

private static void SetAutoProperty<T>(SqlQueryStructure structure, string propertyName, T value, Type? declaringType = null)
{
typeof(SqlQueryStructure).GetField($"<{propertyName}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic)!
Type type = declaringType ?? typeof(SqlQueryStructure);
type.GetField($"<{propertyName}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(structure, value);
}

Expand Down
Loading