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 @@ -3977,7 +3977,8 @@ private static bool IsTypeNameSelection(ISelectionNode selection)
if (steps[s] is OperationPlanStep step
&& step.SelectionSets.Contains(candidateSelectionSetId)
&& step.Target.IsParentOfOrSame(targetPath)
&& !string.IsNullOrEmpty(step.SchemaName))
&& !string.IsNullOrEmpty(step.SchemaName)
&& IsConnectorChainResolvable(path, i, currentType, step.SchemaName))
{
var connectors = new List<AncestorConnector>(path.Count - i);
for (var j = i; j < path.Count; j++)
Expand Down Expand Up @@ -4068,6 +4069,61 @@ private static bool IsTypeNameSelection(ISelectionNode selection)
}
}

/// <summary>
/// Determines whether the source schema <paramref name="schemaName"/> resolves every
/// connector of <paramref name="path"/> from <paramref name="startIndex"/> down to the
/// requirement target, whose type is <paramref name="targetType"/>.
/// </summary>
/// <remarks>
/// 1. When the planner wants to fold a requirement (like an entity key) into an already-planned fetch,
/// it has to wrap that requirement in all the intermediate fields and fragments that lead down to it.
/// This method checks that the chosen source schema actually declares every one of those intermediate steps.
/// 2. It walks the recorded path entry by entry: for each field connector,
/// the field must exist on its declaring type in that specific schema (and not be @external);
/// for each inline-fragment connector, the type condition must exist in that schema.
/// 3. If any link in the chain fails, the whole ancestor step is rejected,
/// so the planner falls back to sourcing the requirement from a step that genuinely owns the path
/// (instead of emitting a document the subgraph would reject).
/// </remarks>
private static bool IsConnectorChainResolvable(
List<(SelectionSetNode SelectionSet, ISelectionNode ConnectingNode, ITypeDefinition Type)> path,
int startIndex,
ITypeDefinition targetType,
string schemaName)
{
for (var i = startIndex; i < path.Count; i++)
{
switch (path[i].ConnectingNode)
{
case FieldNode fieldNode:
if (path[i].Type is not FusionComplexTypeDefinition complexType
|| !complexType.Fields.TryGetField(
fieldNode.Name.Value,
allowInaccessibleFields: true,
out var field)
|| !field.Sources.TryGetMember(schemaName, out var source)
|| source.IsExternal)
{
return false;
}
break;

case InlineFragmentNode { TypeCondition: not null }:
// a connector's own type is the declaring type of the next connector,
// and for the last connector it is the type of the requirement target.
var typeCondition = i + 1 < path.Count ? path[i + 1].Type : targetType;

if (!typeCondition.ExistsInSchema(schemaName))
{
return false;
}
break;
}
}

return true;
}

private bool TryInlineIntoAncestorStep(
AncestorStepMatch match,
SelectionSetNode requirements,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,147 @@ ... on Product {
MatchSnapshot(plan);
}

[Fact]
public void Plan_Should_Keep_Require_Subtree_In_Owning_Schema_When_Entry_Schema_Declares_Nested_Lookup()
{
// arrange
var schema = CreateSupplierRatingSchema();

// act
// supplierBySlug (suppliers) -> rating (ratings, @require "_isEligibleForHighlyRecommended")
// -> review.booking.customer (must hop into the bookings service). The suppliers
// service also declares a Booking lookup, but owns neither Supplier.rating nor
// Booking.customer.
var plan = PlanOperation(
schema,
"""
query GetSupplier {
supplierBySlug(slug: "some-slug") {
... on Supplier {
rating {
reviews(first: 20) {
edges {
node {
booking {
customer {
firstName
}
}
}
}
}
}
}
}
}
""");

// assert
// the suppliers service must only resolve the entry field, the entity key and the
// required field, the whole rating subtree belongs to the ratings service.
MatchInline(
plan,
"""
operation:
- document: |
query GetSupplier {
supplierBySlug(slug: "some-slug") {
__typename @fusion__requirement
... on Supplier {
rating {
reviews(first: 20) {
edges {
node {
booking {
customer {
firstName
}
dbId @fusion__requirement
}
}
}
}
}
dbId @fusion__requirement
_isEligibleForHighlyRecommended @fusion__requirement
}
}
}
name: GetSupplier
hash: 123456789101112
searchSpace: 3
expandedNodes: 9
nodes:
- id: 1
type: Operation
schema: SUPPLIERS
operation: |
query GetSupplier_123456789101112_1 {
supplierBySlug(slug: "some-slug") {
__typename
... on Supplier {
dbId
_isEligibleForHighlyRecommended
}
}
}
- id: 3
type: Operation
schema: RATINGS
operation: |
query GetSupplier_123456789101112_3(
$__fusion_2_isEligibleForHighlyRecommended: Boolean!
$__fusion_3_dbId: Int!
) {
supplierByDbId(dbId: $__fusion_3_dbId) {
rating(
isEligibleForHighlyRecommended: $__fusion_2_isEligibleForHighlyRecommended
) {
reviews(first: 20) {
edges {
node {
booking {
dbId
}
}
}
}
}
}
}
source: $.supplierByDbId
target: $.supplierBySlug<Supplier>
requirements:
- name: __fusion_2_isEligibleForHighlyRecommended
selectionMap: >-
_isEligibleForHighlyRecommended
- name: __fusion_3_dbId
selectionMap: >-
dbId
dependencies:
- id: 1
- id: 4
type: Operation
schema: BOOKINGS
operation: |
query GetSupplier_123456789101112_4($__fusion_4_dbId: Int!) {
bookingByDbId(dbId: $__fusion_4_dbId) {
customer {
firstName
}
}
}
source: $.bookingByDbId
target: $.supplierBySlug<Supplier>.rating.reviews.edges.node.booking
requirements:
- name: __fusion_4_dbId
selectionMap: >-
dbId
dependencies:
- id: 3
""");
}

private static FusionSchemaDefinition CreateRequiredFieldSchema()
=> ComposeSchema(
"""
Expand Down Expand Up @@ -538,4 +679,92 @@ enum ProductCategory {
}
""");
}

private static FusionSchemaDefinition CreateSupplierRatingSchema()
=> ComposeSchema(
"""
# name: SUPPLIERS
schema {
query: Query
}

type Query {
supplierBySlug(slug: String!): SupplierResult!
supplierByDbId(dbId: Int! @is(field: "dbId")): Supplier @lookup @internal
bookingByDbId(dbId: Int! @is(field: "dbId")): Booking @lookup @internal
}

union SupplierResult = Supplier | SupplierNotFound

type SupplierNotFound {
message: String!
}

type Supplier @key(fields: "dbId") {
dbId: Int!
name: String!
_isEligibleForHighlyRecommended: Boolean!
}

type Booking @key(fields: "dbId") {
dbId: Int!
}
""",
"""
# name: RATINGS
schema {
query: Query
}

type Query {
supplierByDbId(dbId: Int! @is(field: "dbId")): Supplier @lookup @internal
}

type Supplier @key(fields: "dbId") {
dbId: Int!
rating(
isEligibleForHighlyRecommended: Boolean!
@require(field: "_isEligibleForHighlyRecommended")): Rating!
}

type Rating {
average: Float!
reviews(first: Int): ReviewConnection
}

type ReviewConnection {
edges: [ReviewEdge!]
}

type ReviewEdge {
node: Review!
}

type Review {
booking: Booking
}

type Booking @key(fields: "dbId") {
dbId: Int!
}
""",
"""
# name: BOOKINGS
schema {
query: Query
}

type Query {
bookingByDbId(dbId: Int! @is(field: "dbId")): Booking @lookup @internal
}

type Booking @key(fields: "dbId") {
dbId: Int!
customer: Customer
}

type Customer {
firstName: String
}
""");
}
Loading