From 0ad10da625754ab80606f42205fe1f7d3ba89226 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 24 Sep 2025 19:08:37 +0300 Subject: [PATCH 1/2] fix(delegate): apply type-merging in extension fields --- .changeset/big-dryers-sniff.md | 5 + packages/delegate/src/resolveExternalValue.ts | 27 +++ packages/stitch/tests/stitchSchemas.test.ts | 214 ++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 .changeset/big-dryers-sniff.md diff --git a/.changeset/big-dryers-sniff.md b/.changeset/big-dryers-sniff.md new file mode 100644 index 000000000..ab04a168a --- /dev/null +++ b/.changeset/big-dryers-sniff.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/delegate': patch +--- + +Apply type-merging correctly in extended fields diff --git a/packages/delegate/src/resolveExternalValue.ts b/packages/delegate/src/resolveExternalValue.ts index 8aa6958c5..95e5e6bdc 100644 --- a/packages/delegate/src/resolveExternalValue.ts +++ b/packages/delegate/src/resolveExternalValue.ts @@ -146,6 +146,33 @@ function resolveExternalObject>( } } + if (!mergedTypeInfo) { + for (const possibleTypeName of possibleTypeNames) { + const potentialMergedTypeInfo = + stitchingInfo.mergedTypes[possibleTypeName]; + if (potentialMergedTypeInfo != null) { + for (const [ + sourceSubschema, + targetSubschemas, + ] of potentialMergedTypeInfo.targetSubschemas) { + if ( + targetSubschemas.length && + sourceSubschema.name == (subschema as Subschema).name + ) { + subschema = sourceSubschema as SubschemaConfig< + any, + any, + any, + TContext + >; + mergedTypeInfo = potentialMergedTypeInfo; + } + } + break; + } + } + } + // If there are no merge targets from the subschema, return. if (!mergedTypeInfo) { return object; diff --git a/packages/stitch/tests/stitchSchemas.test.ts b/packages/stitch/tests/stitchSchemas.test.ts index b31d9b6f3..7ceaab607 100644 --- a/packages/stitch/tests/stitchSchemas.test.ts +++ b/packages/stitch/tests/stitchSchemas.test.ts @@ -9,6 +9,7 @@ import { addResolversToSchema, makeExecutableSchema, } from '@graphql-tools/schema'; +import { stitchingDirectives } from '@graphql-tools/stitching-directives'; import { assertSome, createGraphQLError, @@ -16,6 +17,7 @@ import { getResolversFromSchema, IResolvers, } from '@graphql-tools/utils'; +import { assertAsyncIterable } from '@internal/testing'; import { bookingSchema as localBookingSchema, productSchema as localProductSchema, @@ -3635,3 +3637,215 @@ it('should respect selectionSet in the additional resolvers to override a field' }, }); }); + +it('should be able to extend a transformed schema', async () => { + const projects = [ + { + id: '1', + name: 'Project 1', + rootFolderId: '1', + }, + { + id: '2', + name: 'Project 2', + rootFolderId: '2', + }, + ]; + const projectSchema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + directive @key(selectionSet: String!) on OBJECT + type Project @key(selectionSet: "{ id }") { + id: ID! + name: String! + rootFolderId: ID! + } + type Query { + project(id: ID!): Project + } + `, + resolvers: { + Query: { + project: (_, { id }) => projects.find((project) => project.id === id), + }, + }, + }); + + const folders = [ + { + id: '1', + name: 'Folder 1', + }, + { + id: '2', + name: 'Folder 2', + }, + ]; + const folderSchema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + directive @key(selectionSet: String!) on OBJECT + directive @merge( + argsExpr: String + keyArg: String + keyField: String + key: [String!] + additionalArgs: String + ) on FIELD_DEFINITION + directive @computed(selectionSet: String!) on FIELD_DEFINITION + scalar _Any + union _Entity = Folder | Project + type Folder { + id: ID! + name: String! + } + type Project @key(selectionSet: "{ id }") { + id: ID! + rootFolder: Folder! @computed(selectionSet: "{ rootFolderId }") + } + type Query { + folder(id: ID!): Folder! + _entities(representations: [_Any!]!): [_Entity]! @merge + } + `, + resolvers: { + Query: { + folder: (_, { id }) => folders.find((folder) => folder.id === id), + _entities: (_, { representations }: { representations: any[] }) => + representations.map((rep) => ({ + ...rep, + rootFolder: folders.find( + (folder) => folder.id === rep.rootFolderId, + ), + })), + }, + }, + }); + const subscriptionSchema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + _: String + } + type Subscription { + projectUsed: ProjectUsed! + } + type ProjectUsed { + projectId: ID! + } + `, + resolvers: { + Subscription: { + projectUsed: { + async *subscribe() { + for (const project of projects) { + yield { + projectUsed: { + projectId: project.id, + }, + }; + } + }, + }, + }, + }, + }); + + const projectSubschema = { + name: 'project', + schema: projectSchema, + }; + + const folderSubschema = { + name: 'folder', + schema: folderSchema, + }; + + const subscriptionSubschema = { + name: 'subscription', + schema: subscriptionSchema, + }; + + const stitchedSchema = stitchSchemas({ + subschemas: [projectSubschema, folderSubschema, subscriptionSubschema], + subschemaConfigTransforms: [ + stitchingDirectives().stitchingDirectivesTransformer, + ], + typeDefs: /* GraphQL */ ` + extend type ProjectUsed { + project: Project! + } + `, + resolvers: { + ProjectUsed: { + project: { + selectionSet: '{ projectId }', + resolve: (projectUsed, _, context, info) => + delegateToSchema({ + schema: projectSubschema, + operation: 'query' as any, + fieldName: 'project', + args: { + id: projectUsed.projectId, + }, + context, + info, + }), + }, + }, + }, + }); + + const result = await normalizedExecutor({ + schema: stitchedSchema, + document: parse(/* GraphQL */ ` + subscription { + projectUsed { + projectId + project { + id + name + rootFolder { + id + name + } + } + } + } + `), + }); + assertAsyncIterable(result); + const payloads = []; + for await (const payload of result) { + payloads.push(payload); + } + expect(payloads).toEqual([ + { + data: { + projectUsed: { + project: { + id: '1', + name: 'Project 1', + rootFolder: { + id: '1', + name: 'Folder 1', + }, + }, + projectId: '1', + }, + }, + }, + { + data: { + projectUsed: { + project: { + id: '2', + name: 'Project 2', + rootFolder: { + id: '2', + name: 'Folder 2', + }, + }, + projectId: '2', + }, + }, + }, + ]); +}); From 56acbbb5a57e8d136b9efa99c1b7c87a5ccd64b7 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 24 Sep 2025 19:10:23 +0300 Subject: [PATCH 2/2] Update packages/delegate/src/resolveExternalValue.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/delegate/src/resolveExternalValue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/delegate/src/resolveExternalValue.ts b/packages/delegate/src/resolveExternalValue.ts index 95e5e6bdc..a6eafb246 100644 --- a/packages/delegate/src/resolveExternalValue.ts +++ b/packages/delegate/src/resolveExternalValue.ts @@ -157,7 +157,7 @@ function resolveExternalObject>( ] of potentialMergedTypeInfo.targetSubschemas) { if ( targetSubschemas.length && - sourceSubschema.name == (subschema as Subschema).name + sourceSubschema.name === (subschema as Subschema).name ) { subschema = sourceSubschema as SubschemaConfig< any,