diff --git a/packages/batch-delegate/src/getLoader.ts b/packages/batch-delegate/src/getLoader.ts index 041df7b951c..498eacc87f2 100644 --- a/packages/batch-delegate/src/getLoader.ts +++ b/packages/batch-delegate/src/getLoader.ts @@ -76,14 +76,17 @@ export function getLoader(options: BatchDelegateOptions fieldName = info.fieldName, dataLoaderOptions, fieldNodes = info.fieldNodes, - selectionSet = fieldNodes[0].selectionSet, } = options; const loaders = getLoadersMap(context ?? GLOBAL_CONTEXT, schema); let cacheKey = fieldName; - if (selectionSet != null) { - cacheKey += memoizedPrint(selectionSet); + if (fieldNodes[0]) { + const fieldNode = { + ...fieldNodes[0], + alias: undefined, + }; + cacheKey += memoizedPrint(fieldNode); } let loader = loaders.get(cacheKey); diff --git a/packages/batch-delegate/tests/basic.example.test.ts b/packages/batch-delegate/tests/basic.example.test.ts index be16b60e47c..0d4790eb42e 100644 --- a/packages/batch-delegate/tests/basic.example.test.ts +++ b/packages/batch-delegate/tests/basic.example.test.ts @@ -97,6 +97,94 @@ describe('batch delegation within basic stitching example', () => { expect(chirps[0].chirpedAtUser.email).not.toBe(null); }); + test('uses a single call even when delegating the same field multiple times', async () => { + let numCalls = 0; + + const chirpSchema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Chirp { + chirpedAtUserId: ID! + } + + type Query { + trendingChirps: [Chirp] + } + `, + resolvers: { + Query: { + trendingChirps: () => [{ chirpedAtUserId: 1 }, { chirpedAtUserId: 2 }], + }, + }, + }); + + // Mocked author schema + const authorSchema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type User { + email: String + } + + type Query { + usersByIds(ids: [ID!]): [User] + } + `, + resolvers: { + Query: { + usersByIds: (_root, args) => { + numCalls++; + return args.ids.map((id: string) => ({ email: `${id}@test.com` })); + }, + }, + }, + }); + + const linkTypeDefs = /* GraphQL */ ` + extend type Chirp { + chirpedAtUser: User + } + `; + + const stitchedSchema = stitchSchemas({ + subschemas: [chirpSchema, authorSchema], + typeDefs: linkTypeDefs, + resolvers: { + Chirp: { + chirpedAtUser: { + selectionSet: `{ chirpedAtUserId }`, + resolve(chirp, _args, context, info) { + return batchDelegateToSchema({ + schema: authorSchema, + operation: 'query' as OperationTypeNode, + fieldName: 'usersByIds', + key: chirp.chirpedAtUserId, + argsFromKeys: ids => ({ ids }), + context, + info, + }); + }, + }, + }, + }, + }); + + const query = /* GraphQL */ ` + query { + trendingChirps { + chirp1: chirpedAtUser { + email + } + chirp2: chirpedAtUser { + email + } + } + } + `; + + await execute({ schema: stitchedSchema, document: parse(query) }); + + expect(numCalls).toEqual(1); + }); + test('works with key arrays', async () => { let numCalls = 0; diff --git a/packages/batch-delegate/tests/cacheByArguments.test.ts b/packages/batch-delegate/tests/cacheByArguments.test.ts new file mode 100644 index 00000000000..203fe324e00 --- /dev/null +++ b/packages/batch-delegate/tests/cacheByArguments.test.ts @@ -0,0 +1,107 @@ +import { execute, isIncrementalResult } from '@graphql-tools/executor'; +import { OperationTypeNode, parse } from 'graphql'; + +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { batchDelegateToSchema } from '@graphql-tools/batch-delegate'; +import { stitchSchemas } from '@graphql-tools/stitch'; + +describe('non-key arguments are taken into account when memoizing result', () => { + test('memoizes non-key arguments as part of batch delegation', async () => { + let numCalls = 0; + + const chirpSchema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Chirp { + chirpedAtUserId: ID! + } + + type Query { + trendingChirps: [Chirp] + } + `, + resolvers: { + Query: { + trendingChirps: () => [{ chirpedAtUserId: 1 }, { chirpedAtUserId: 2 }], + }, + }, + }); + + // Mocked author schema + const authorSchema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type User { + email: String! + } + + type Query { + usersByIds(ids: [ID!], obfuscateEmail: Boolean!): [User] + } + `, + resolvers: { + Query: { + usersByIds: (_root, args) => { + numCalls++; + return args.ids.map((id: string) => ({ email: args.obfuscateEmail ? '***' : `${id}@test.com` })); + }, + }, + }, + }); + + const linkTypeDefs = /* GraphQL */ ` + extend type Chirp { + chirpedAtUser(obfuscateEmail: Boolean!): User + } + `; + + const stitchedSchema = stitchSchemas({ + subschemas: [chirpSchema, authorSchema], + typeDefs: linkTypeDefs, + resolvers: { + Chirp: { + chirpedAtUser: { + selectionSet: `{ chirpedAtUserId }`, + resolve(chirp, args, context, info) { + return batchDelegateToSchema({ + schema: authorSchema, + operation: 'query' as OperationTypeNode, + fieldName: 'usersByIds', + key: chirp.chirpedAtUserId, + argsFromKeys: ids => ({ ids, ...args }), + context, + info, + }); + }, + }, + }, + }, + }); + + const query = /* GraphQL */ ` + query { + trendingChirps { + withObfuscatedEmail: chirpedAtUser(obfuscateEmail: true) { + email + } + withoutObfuscatedEmail: chirpedAtUser(obfuscateEmail: false) { + email + } + } + } + `; + + const result = await execute({ schema: stitchedSchema, document: parse(query) }); + + expect(numCalls).toEqual(2); + + if (isIncrementalResult(result)) throw Error('result is incremental'); + + expect(result.errors).toBeUndefined(); + + const chirps: any = result.data!['trendingChirps']; + expect(chirps[0].withObfuscatedEmail.email).toBe(`***`); + expect(chirps[1].withObfuscatedEmail.email).toBe(`***`); + + expect(chirps[0].withoutObfuscatedEmail.email).toBe(`1@test.com`); + expect(chirps[1].withoutObfuscatedEmail.email).toBe(`2@test.com`); + }); +});