From 0a762a9e465884ad63dc3cdc3faab96b63c39503 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Wed, 26 Nov 2025 14:44:00 +0100 Subject: [PATCH 1/6] organize test --- .../tests/createDelegationPlanBuilder.test.ts | 142 +++++++++++++++++- 1 file changed, 140 insertions(+), 2 deletions(-) diff --git a/packages/stitch/tests/createDelegationPlanBuilder.test.ts b/packages/stitch/tests/createDelegationPlanBuilder.test.ts index 5483115b5..ab3f39cd8 100644 --- a/packages/stitch/tests/createDelegationPlanBuilder.test.ts +++ b/packages/stitch/tests/createDelegationPlanBuilder.test.ts @@ -1,6 +1,8 @@ -import { Subschema } from '@graphql-tools/delegate'; +import { createDefaultExecutor, Subschema } from '@graphql-tools/delegate'; +import { getStitchedSchemaFromSupergraphSdl } from '@graphql-tools/federation'; +import { addMocksToSchema, IMocks } from '@graphql-tools/mock'; import { makeExecutableSchema } from '@graphql-tools/schema'; -import { FragmentDefinitionNode, Kind } from 'graphql'; +import { execute, FragmentDefinitionNode, Kind, parse } from 'graphql'; import { describe, expect, it } from 'vitest'; import { optimizeDelegationMap } from '../src/createDelegationPlanBuilder'; @@ -141,4 +143,140 @@ describe('fragment handling in delegation optimization', () => { }, ]); }); + + it('should make sure to properly handle nested fragments', async () => { + const schema = getStitchedSchemaFromSupergraphSdl({ + supergraphSdl: /* GraphQL */ ` + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { + query: Query + } + + directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + + directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + + directive @join__implements( + graph: join__Graph! + interface: String! + ) repeatable on OBJECT | INTERFACE + + directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false + ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + + directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] + ) repeatable on SCHEMA + + interface IContact @join__type(graph: UPSTREAM) { + properties: Json + } + + interface INode @join__type(graph: UPSTREAM) { + id: ID! + } + + scalar join__FieldSet + + enum join__Graph { + UPSTREAM @join__graph(name: "upstream", url: "") + } + + scalar Json @join__type(graph: UPSTREAM) + + enum link__Purpose { + """ + \`EXECUTION\` features provide metadata necessary for operation execution. + """ + EXECUTION + + """ + \`SECURITY\` features provide metadata necessary to securely resolve fields. + """ + SECURITY + } + + scalar link__Import + + type MaybePerson implements IContact & INode + @join__implements(graph: UPSTREAM, interface: "IContact") + @join__implements(graph: UPSTREAM, interface: "INode") + @join__type(graph: UPSTREAM, key: "id") { + id: ID! + properties: Json + } + + type Query @join__type(graph: UPSTREAM) { + node: INode + } + `, + onSubschemaConfig(subschemaConfig) { + const mocks: IMocks = { + INode: () => ({ __typename: 'MaybePerson' }), + Json: () => ({}), + }; + const mockedSchema = addMocksToSchema({ + schema: subschemaConfig.schema, + mocks, + resolvers: { + _Entity: { + __resolveType: ({ __typename }: { __typename: string }) => + __typename, + }, + Query: { + _entities(_, { representations }) { + return representations; + }, + }, + }, + }); + subschemaConfig.executor = createDefaultExecutor(mockedSchema); + }, + }); + + const result = execute({ + document: parse(/* GraphQL */ ` + { + node { + ... on IContact { + ...MaybePerson + } + } + } + fragment MaybePerson on IContact { + __typename + properties + } + `), + schema, + }); + + expect(result).toEqual({ + data: { + node: { + __typename: 'MaybePerson', + properties: {}, + }, + }, + }); + }); }); From 9be6418b1c52502df146df6af7ba6fb0ed24ac05 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Wed, 26 Nov 2025 13:46:38 +0100 Subject: [PATCH 2/6] ok fix --- .../isolateComputedFieldsTransformer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts b/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts index d16eb8bf0..a360c2728 100644 --- a/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts +++ b/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts @@ -323,7 +323,7 @@ function filterBaseSubschema( } } const allTypes = [typeName, ...iFacesForType]; - const isIsolatedFieldName = allTypes.every((implementingTypeName) => + const isIsolatedFieldName = allTypes.some((implementingTypeName) => isIsolatedField(implementingTypeName, fieldName, isolatedSchemaTypes), ); const isKeyFieldName = allTypes.some((implementingTypeName) => @@ -357,7 +357,7 @@ function filterBaseSubschema( ...iFacesForType, ...typesForInterface[typeName], ]; - const isIsolatedFieldName = allTypes.every((implementingTypeName) => + const isIsolatedFieldName = allTypes.some((implementingTypeName) => isIsolatedField(implementingTypeName, fieldName, isolatedSchemaTypes), ); const isKeyFieldName = allTypes.some((implementingTypeName) => From 1d8029191773388689be67168d4ff96d6f48310e Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Wed, 26 Nov 2025 15:40:31 +0100 Subject: [PATCH 3/6] not actually testing what's necessary --- .../tests/createDelegationPlanBuilder.test.ts | 142 +----------------- 1 file changed, 2 insertions(+), 140 deletions(-) diff --git a/packages/stitch/tests/createDelegationPlanBuilder.test.ts b/packages/stitch/tests/createDelegationPlanBuilder.test.ts index ab3f39cd8..5483115b5 100644 --- a/packages/stitch/tests/createDelegationPlanBuilder.test.ts +++ b/packages/stitch/tests/createDelegationPlanBuilder.test.ts @@ -1,8 +1,6 @@ -import { createDefaultExecutor, Subschema } from '@graphql-tools/delegate'; -import { getStitchedSchemaFromSupergraphSdl } from '@graphql-tools/federation'; -import { addMocksToSchema, IMocks } from '@graphql-tools/mock'; +import { Subschema } from '@graphql-tools/delegate'; import { makeExecutableSchema } from '@graphql-tools/schema'; -import { execute, FragmentDefinitionNode, Kind, parse } from 'graphql'; +import { FragmentDefinitionNode, Kind } from 'graphql'; import { describe, expect, it } from 'vitest'; import { optimizeDelegationMap } from '../src/createDelegationPlanBuilder'; @@ -143,140 +141,4 @@ describe('fragment handling in delegation optimization', () => { }, ]); }); - - it('should make sure to properly handle nested fragments', async () => { - const schema = getStitchedSchemaFromSupergraphSdl({ - supergraphSdl: /* GraphQL */ ` - schema - @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) { - query: Query - } - - directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE - - directive @join__field( - graph: join__Graph - requires: join__FieldSet - provides: join__FieldSet - type: String - external: Boolean - override: String - usedOverridden: Boolean - ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION - - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - - directive @join__implements( - graph: join__Graph! - interface: String! - ) repeatable on OBJECT | INTERFACE - - directive @join__type( - graph: join__Graph! - key: join__FieldSet - extension: Boolean! = false - resolvable: Boolean! = true - isInterfaceObject: Boolean! = false - ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR - - directive @link( - url: String - as: String - for: link__Purpose - import: [link__Import] - ) repeatable on SCHEMA - - interface IContact @join__type(graph: UPSTREAM) { - properties: Json - } - - interface INode @join__type(graph: UPSTREAM) { - id: ID! - } - - scalar join__FieldSet - - enum join__Graph { - UPSTREAM @join__graph(name: "upstream", url: "") - } - - scalar Json @join__type(graph: UPSTREAM) - - enum link__Purpose { - """ - \`EXECUTION\` features provide metadata necessary for operation execution. - """ - EXECUTION - - """ - \`SECURITY\` features provide metadata necessary to securely resolve fields. - """ - SECURITY - } - - scalar link__Import - - type MaybePerson implements IContact & INode - @join__implements(graph: UPSTREAM, interface: "IContact") - @join__implements(graph: UPSTREAM, interface: "INode") - @join__type(graph: UPSTREAM, key: "id") { - id: ID! - properties: Json - } - - type Query @join__type(graph: UPSTREAM) { - node: INode - } - `, - onSubschemaConfig(subschemaConfig) { - const mocks: IMocks = { - INode: () => ({ __typename: 'MaybePerson' }), - Json: () => ({}), - }; - const mockedSchema = addMocksToSchema({ - schema: subschemaConfig.schema, - mocks, - resolvers: { - _Entity: { - __resolveType: ({ __typename }: { __typename: string }) => - __typename, - }, - Query: { - _entities(_, { representations }) { - return representations; - }, - }, - }, - }); - subschemaConfig.executor = createDefaultExecutor(mockedSchema); - }, - }); - - const result = execute({ - document: parse(/* GraphQL */ ` - { - node { - ... on IContact { - ...MaybePerson - } - } - } - fragment MaybePerson on IContact { - __typename - properties - } - `), - schema, - }); - - expect(result).toEqual({ - data: { - node: { - __typename: 'MaybePerson', - properties: {}, - }, - }, - }); - }); }); From 6fa49dc47ada9c2333cde0430e04c6740dad69b9 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Wed, 26 Nov 2025 15:46:27 +0100 Subject: [PATCH 4/6] Revert "ok fix" This reverts commit 9be6418b1c52502df146df6af7ba6fb0ed24ac05. --- .../isolateComputedFieldsTransformer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts b/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts index a360c2728..d16eb8bf0 100644 --- a/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts +++ b/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts @@ -323,7 +323,7 @@ function filterBaseSubschema( } } const allTypes = [typeName, ...iFacesForType]; - const isIsolatedFieldName = allTypes.some((implementingTypeName) => + const isIsolatedFieldName = allTypes.every((implementingTypeName) => isIsolatedField(implementingTypeName, fieldName, isolatedSchemaTypes), ); const isKeyFieldName = allTypes.some((implementingTypeName) => @@ -357,7 +357,7 @@ function filterBaseSubschema( ...iFacesForType, ...typesForInterface[typeName], ]; - const isIsolatedFieldName = allTypes.some((implementingTypeName) => + const isIsolatedFieldName = allTypes.every((implementingTypeName) => isIsolatedField(implementingTypeName, fieldName, isolatedSchemaTypes), ); const isKeyFieldName = allTypes.some((implementingTypeName) => From a5e9ca8108a6c2bc82240bca7f1c84a85505b01f Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Wed, 26 Nov 2025 16:12:50 +0100 Subject: [PATCH 5/6] changeset --- .changeset/lovely-mirrors-hunt.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lovely-mirrors-hunt.md diff --git a/.changeset/lovely-mirrors-hunt.md b/.changeset/lovely-mirrors-hunt.md new file mode 100644 index 000000000..83e315698 --- /dev/null +++ b/.changeset/lovely-mirrors-hunt.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/stitch': patch +--- + +Ensure key fields propagate correctly for interface/object combinations From 986163d9659784f4b4d27aed78ee10f34d27c81e Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Thu, 27 Nov 2025 14:29:05 +0100 Subject: [PATCH 6/6] Reapply "ok fix" This reverts commit 6fa49dc47ada9c2333cde0430e04c6740dad69b9. --- .../isolateComputedFieldsTransformer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts b/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts index d16eb8bf0..a360c2728 100644 --- a/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts +++ b/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts @@ -323,7 +323,7 @@ function filterBaseSubschema( } } const allTypes = [typeName, ...iFacesForType]; - const isIsolatedFieldName = allTypes.every((implementingTypeName) => + const isIsolatedFieldName = allTypes.some((implementingTypeName) => isIsolatedField(implementingTypeName, fieldName, isolatedSchemaTypes), ); const isKeyFieldName = allTypes.some((implementingTypeName) => @@ -357,7 +357,7 @@ function filterBaseSubschema( ...iFacesForType, ...typesForInterface[typeName], ]; - const isIsolatedFieldName = allTypes.every((implementingTypeName) => + const isIsolatedFieldName = allTypes.some((implementingTypeName) => isIsolatedField(implementingTypeName, fieldName, isolatedSchemaTypes), ); const isKeyFieldName = allTypes.some((implementingTypeName) =>