diff --git a/.changeset/@graphql-mesh_cli-4571-dependencies.md b/.changeset/@graphql-mesh_cli-4571-dependencies.md new file mode 100644 index 0000000000000..dd6a8dfe2a1e7 --- /dev/null +++ b/.changeset/@graphql-mesh_cli-4571-dependencies.md @@ -0,0 +1,7 @@ +--- +"@graphql-mesh/cli": patch +--- + +dependencies updates: + +- Updated dependency [`ws@8.9.0` ↗︎](https://www.npmjs.com/package/ws/v/8.9.0) (from `8.8.1`, in `dependencies`) diff --git a/.changeset/@graphql-mesh_grpc-4571-dependencies.md b/.changeset/@graphql-mesh_grpc-4571-dependencies.md new file mode 100644 index 0000000000000..b120a72c5b0bd --- /dev/null +++ b/.changeset/@graphql-mesh_grpc-4571-dependencies.md @@ -0,0 +1,7 @@ +--- +"@graphql-mesh/grpc": patch +--- + +dependencies updates: + +- Updated dependency [`protobufjs@7.1.2` ↗︎](https://www.npmjs.com/package/protobufjs/v/7.1.2) (from `7.1.1`, in `dependencies`) diff --git a/.changeset/@graphql-mesh_http-4571-dependencies.md b/.changeset/@graphql-mesh_http-4571-dependencies.md new file mode 100644 index 0000000000000..a70893b6040a4 --- /dev/null +++ b/.changeset/@graphql-mesh_http-4571-dependencies.md @@ -0,0 +1,7 @@ +--- +"@graphql-mesh/http": patch +--- + +dependencies updates: + +- Updated dependency [`graphql-yoga@3.0.0-next.1` ↗︎](https://www.npmjs.com/package/graphql-yoga/v/3.0.0-next.1) (from `3.0.0-next.0`, in `dependencies`) diff --git a/.changeset/ninety-pots-guess.md b/.changeset/ninety-pots-guess.md new file mode 100644 index 0000000000000..aae57886cce65 --- /dev/null +++ b/.changeset/ninety-pots-guess.md @@ -0,0 +1,5 @@ +--- +'@graphql-mesh/runtime': patch +--- + +Choose the root type name for a specific operation type from the source schema not from the gateway schema, because source schema might have a different like `QueryType` instead of `Query`. diff --git a/packages/runtime/src/get-mesh.ts b/packages/runtime/src/get-mesh.ts index 981607af356ab..8c77284a3c650 100644 --- a/packages/runtime/src/get-mesh.ts +++ b/packages/runtime/src/get-mesh.ts @@ -96,10 +96,14 @@ export function wrapFetchWithPlugins(plugins: MeshPlugin[]): MeshFetch { } // Use in-context-sdk for tracing -function createProxyingResolverFactory(apiName: string): CreateProxyingResolverFn { - return function createProxyingResolver() { +function createProxyingResolverFactory(apiName: string, apiSchema: GraphQLSchema): CreateProxyingResolverFn { + return function createProxyingResolver({ operation, fieldName, subschemaConfig }) { + const rootType = apiSchema.getRootType(operation); return function proxyingResolver(root, args, context, info) { - return context[apiName][info.parentType.name][info.fieldName]({ root, args, context, info }); + if (!context[apiName][rootType.name][info.fieldName]) { + throw new Error(`${info.fieldName} couldn't find in ${rootType.name} of ${apiName} as a ${operation}`); + } + return context[apiName][rootType.name][info.fieldName]({ root, args, context, info }); }; }; } @@ -197,7 +201,7 @@ export async function getMesh(options: GetMeshOptions): Promise { handler: apiSource.handler, batch: 'batch' in source ? source.batch : true, merge: source.merge, - createProxyingResolver: createProxyingResolverFactory(apiName), + createProxyingResolver: createProxyingResolverFactory(apiName, apiSchema), }); } catch (e: any) { sourceLogger.error(`Failed to generate the schema`, e); diff --git a/packages/runtime/test/getMesh.test.ts b/packages/runtime/test/getMesh.test.ts new file mode 100644 index 0000000000000..6f6c2b232d218 --- /dev/null +++ b/packages/runtime/test/getMesh.test.ts @@ -0,0 +1,211 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import LocalforageCache from '@graphql-mesh/cache-localforage'; +import GraphQLHandler from '@graphql-mesh/graphql'; +import StitchingMerger from '@graphql-mesh/merger-stitching'; +import { InMemoryStoreStorageAdapter, MeshStore } from '@graphql-mesh/store'; +import { defaultImportFn, DefaultLogger, PubSub } from '@graphql-mesh/utils'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { printSchemaWithDirectives } from '@graphql-tools/utils'; +import { parse } from 'graphql'; +import { getMesh } from '../src/get-mesh'; +import { MeshResolvedSource } from '../src/types'; + +describe('getMesh', () => { + const baseDir = __dirname; + let cache: LocalforageCache; + let pubsub: PubSub; + let store: MeshStore; + let logger: DefaultLogger; + let merger: StitchingMerger; + beforeEach(() => { + cache = new LocalforageCache(); + pubsub = new PubSub(); + store = new MeshStore('test', new InMemoryStoreStorageAdapter(), { + readonly: false, + validate: false, + }); + logger = new DefaultLogger('Mesh Test'); + merger = new StitchingMerger({ + store, + cache, + pubsub, + logger, + }); + }); + + interface CreateSchemaConfiguration { + suffix: string; + suffixRootTypeNames: boolean; + suffixFieldNames: boolean; + suffixResponses: boolean; + } + + function createGraphQLSchema(config: CreateSchemaConfiguration) { + const queryTypeName = config.suffixRootTypeNames ? `Query${config.suffix}` : 'Query'; + const mutationTypeName = config.suffixRootTypeNames ? `Mutation${config.suffix}` : 'Mutation'; + const subscriptionTypeName = config.suffixRootTypeNames ? `Subscription${config.suffix}` : 'Subscription'; + + return makeExecutableSchema({ + typeDefs: ` + type ${queryTypeName} { + hello${config.suffixFieldNames ? config.suffix : ''}: String + } + + type ${mutationTypeName} { + bye${config.suffixFieldNames ? config.suffix : ''}: String + } + + type ${subscriptionTypeName} { + wave${config.suffixFieldNames ? config.suffix : ''}: String + } + + schema { + query: ${queryTypeName} + mutation: ${mutationTypeName} + subscription: ${subscriptionTypeName} + } + `, + resolvers: { + [queryTypeName]: { + [`hello${config.suffixFieldNames ? config.suffix : ''}`]: () => + `Hello from service${config.suffixResponses ? config.suffix : ''}`, + }, + [mutationTypeName]: { + [`bye${config.suffixFieldNames ? config.suffix : ''}`]: () => + `Bye from service${config.suffixResponses ? config.suffix : ''}schema`, + }, + }, + }); + } + + function createGraphQLSource(config: CreateSchemaConfiguration): MeshResolvedSource { + const name = `service${config.suffix}`; + return { + name, + handler: new GraphQLHandler({ + baseDir, + cache, + pubsub, + name, + config: { + schema: `./schema${config.suffix}.ts`, + }, + store, + logger, + async importFn(moduleId) { + if (moduleId.endsWith(`schema${config.suffix}.ts`)) { + return createGraphQLSchema(config); + } + return defaultImportFn(moduleId); + }, + }), + transforms: [], + }; + } + + it('handle sources with different query type names', async () => { + const mesh = await getMesh({ + cache, + pubsub, + logger, + sources: new Array(3).fill(0).map((_, i) => + createGraphQLSource({ + suffix: i.toString(), + suffixRootTypeNames: true, + suffixFieldNames: false, + suffixResponses: false, + }) + ), + merger, + }); + + expect(printSchemaWithDirectives(mesh.schema)).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + subscription: Subscription + } + + type Query { + hello: String + } + + type Mutation { + bye: String + } + + type Subscription { + wave: String + }" + `); + + const result = await mesh.execute( + ` + { + hello0 + hello1 + hello2 + } + `, + {} + ); + + expect(result).toMatchInlineSnapshot(` + { + "data": {}, + } + `); + }); + + it('can stitch a mutation field to a query field', async () => { + const mesh = await getMesh({ + cache, + pubsub, + logger, + merger, + sources: [ + createGraphQLSource({ + suffix: 'Foo', + suffixRootTypeNames: false, + suffixFieldNames: true, + suffixResponses: true, + }), + createGraphQLSource({ + suffix: 'Bar', + suffixRootTypeNames: false, + suffixFieldNames: true, + suffixResponses: true, + }), + ], + additionalTypeDefs: [ + parse(/* GraphQL */ ` + extend type Mutation { + strikeBack: String + } + `), + ], + additionalResolvers: { + Mutation: { + strikeBack: (root, args, context, info) => context.serviceFoo.Query.helloFoo({ root, args, context, info }), + }, + }, + }); + + const result = await mesh.execute( + /* GraphQL */ ` + mutation { + strikeBack + } + `, + {} + ); + + expect(result).toMatchInlineSnapshot(` + { + "data": { + "strikeBack": "Hello from serviceFoo", + }, + } + `); + }); +});