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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ internal static class FieldNodeExtensions
public static SelectionState GetSelectionState(
this FieldNode fieldNode,
ISyntaxNode? declaringNode,
ITypeDefinition? parentType)
ITypeDefinition? parentType,
NamedTypeNode? fragmentTypeCondition = null)
{
var skipField = fieldNode.GetSkipIfValue();
var includeField = fieldNode.GetIncludeIfValue();
Expand All @@ -23,6 +24,12 @@ public static SelectionState GetSelectionState(
skipFragment = fragmentSpread.GetSkipIfValue();
includeFragment = fragmentSpread.GetIncludeIfValue();

if (fragmentTypeCondition is not null
&& fragmentTypeCondition.Name.Value != parentType?.Name)
{
typeConditional = true;
}

break;

case InlineFragmentNode inlineFragment:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,12 +437,22 @@ protected override ISyntaxVisitorAction Enter(

var parentType =
context.PendingFields.Count == 0
? null
? context.Schema.GetOperationType(context.OperationType)
: context.PendingFields.Peek().Field.Type.NamedType();

var declaringNode = context.Nodes.ElementAtOrDefault(2);

NamedTypeNode? fragmentTypeCondition = null;
if (declaringNode is FragmentSpreadNode spreadNode
&& context.Fragments.TryGetValue(spreadNode.Name.Value, out var fragmentNode))
Comment thread
glen-84 marked this conversation as resolved.
{
fragmentTypeCondition = fragmentNode.TypeCondition;
}

var selectionState = fieldNode.GetSelectionState(
declaringNode: context.Nodes.ElementAtOrDefault(2),
parentType);
declaringNode,
parentType,
fragmentTypeCondition);

if (selectionState is SelectionState.Excluded)
{
Expand Down Expand Up @@ -635,7 +645,14 @@ protected override ISyntaxVisitorAction Enter(
{
if (context.Fragments.TryGetValue(fragmentSpreadNode.Name.Value, out var fragmentNode))
{
// Narrow the type for the duration of this fragment.
var parent = context.Frames.Peek();
var typeCondition = fragmentNode.TypeCondition.Name.Value;
var narrowed = (IOutputTypeDefinition)context.Schema.Types[typeCondition];

context.Frames.Push(parent with { Type = narrowed });
Visit(fragmentNode.SelectionSet, context);
context.Frames.Pop();
}

return Skip;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,109 @@ fragment DogFields on Dog {
tool.Tool.OutputSchema.MatchSnapshot(extension: ".json");
}

[Fact]
public void CreateTool_FragmentSpreadSelectsImplementationDeclaredField_NarrowsToTypeCondition()
{
// arrange
var schema = CreateSchema();
var document = Utf8GraphQLParser.Parse(
"""
query GetWithInterfaceType {
withInterfaceType {
name
...CatFields
}
}

fragment CatFields on Cat {
isPurring
}
""");
var toolDefinition = new OperationToolDefinition(document);

// act
var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);

// assert
tool.Tool.OutputSchema.MatchSnapshot(extension: ".json");
}

[Fact]
public void CreateTool_FragmentSpreadOnParentType_KeepsFieldsRequired()
{
// arrange
var schema = CreateSchema();
var document = Utf8GraphQLParser.Parse(
"""
query GetWithInterfaceType {
withInterfaceType {
...PetFields
}
}

fragment PetFields on Pet {
name
}
""");
var toolDefinition = new OperationToolDefinition(document);

// act
var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);

// assert
tool.Tool.OutputSchema.MatchSnapshot(extension: ".json");
}

[Fact]
public void CreateTool_FragmentSpreadOnOperationType_KeepsFieldsRequired()
{
// arrange
var schema = CreateSchema();
var document = Utf8GraphQLParser.Parse(
"""
query GetBooks {
...QueryFields
}

fragment QueryFields on Query {
books {
title
}
}
""");
var toolDefinition = new OperationToolDefinition(document);

// act
var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);

// assert
tool.Tool.OutputSchema.MatchSnapshot(extension: ".json");
}

[Fact]
public void CreateTool_InlineFragmentOnOperationType_KeepsFieldsRequired()
{
// arrange
var schema = CreateSchema();
var document = Utf8GraphQLParser.Parse(
"""
query GetBooks {
... on Query {
books {
title
}
}
}
""");
var toolDefinition = new OperationToolDefinition(document);

// act
var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);

// assert
tool.Tool.OutputSchema.MatchSnapshot(extension: ".json");
}

[Fact]
public void CreateTool_DuplicateFieldSelection_MergesIntoSingleProperty()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"type": "object",
"properties": {
"data": {
"type": [
"object",
"null"
],
"properties": {
"books": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string"
}
},
"required": [
"title"
],
"additionalProperties": false
}
}
},
"additionalProperties": false,
"required": [
"books"
]
},
"errors": {
"type": "array",
"items": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"locations": {
"type": [
"array",
"null"
],
"items": {
"type": "object",
"properties": {
"line": {
"type": "integer"
},
"column": {
"type": "integer"
}
},
"additionalProperties": false
}
},
"path": {
"type": [
"array",
"null"
],
"items": {
"type": [
"string",
"integer"
]
}
},
"extensions": {
"type": [
"object",
"null"
],
"additionalProperties": true
}
},
"required": [
"message"
],
"additionalProperties": false
}
}
},
"additionalProperties": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"type": "object",
"properties": {
"data": {
"type": [
"object",
"null"
],
"properties": {
"withInterfaceType": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
],
"additionalProperties": false
}
},
"additionalProperties": false,
"required": [
"withInterfaceType"
]
},
"errors": {
"type": "array",
"items": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"locations": {
"type": [
"array",
"null"
],
"items": {
"type": "object",
"properties": {
"line": {
"type": "integer"
},
"column": {
"type": "integer"
}
},
"additionalProperties": false
}
},
"path": {
"type": [
"array",
"null"
],
"items": {
"type": [
"string",
"integer"
]
}
},
"extensions": {
"type": [
"object",
"null"
],
"additionalProperties": true
}
},
"required": [
"message"
],
"additionalProperties": false
}
}
},
"additionalProperties": false
}
Loading
Loading