Skip to content

Commit 21e1f05

Browse files
authored
fix: batch delegation collapses two requests with different arguments into a single memoized loader (#382)
1 parent 1a50df2 commit 21e1f05

File tree

4 files changed

+213
-0
lines changed

4 files changed

+213
-0
lines changed

.changeset/nervous-cups-scream.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@graphql-tools/batch-delegate': patch
3+
---
4+
5+
Memoize the batched delegation loader by respecting the arguments correctly
6+
7+
[See more details in the PR](https://github.com/ardatan/graphql-tools/pull/5189)

packages/batch-delegate/src/getLoader.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ export function getLoader<K = any, V = any, C = K>(
117117
cacheKey += memoizedPrint(selectionSet);
118118
}
119119

120+
const fieldNode = fieldNodes?.[0];
121+
if (fieldNode?.arguments) {
122+
cacheKey += fieldNode.arguments.map((arg) => memoizedPrint(arg)).join(',');
123+
}
124+
120125
let loader = loaders.get(cacheKey);
121126

122127
if (loader === undefined) {

packages/batch-delegate/tests/basic.example.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,4 +366,92 @@ describe('batch delegation within basic stitching example', () => {
366366
],
367367
});
368368
});
369+
test('uses a single call even when delegating the same field multiple times', async () => {
370+
let numCalls = 0;
371+
372+
const chirpSchema = makeExecutableSchema({
373+
typeDefs: /* GraphQL */ `
374+
type Chirp {
375+
chirpedAtUserId: ID!
376+
}
377+
type Query {
378+
trendingChirps: [Chirp]
379+
}
380+
`,
381+
resolvers: {
382+
Query: {
383+
trendingChirps: () => [
384+
{ chirpedAtUserId: 1 },
385+
{ chirpedAtUserId: 2 },
386+
],
387+
},
388+
},
389+
});
390+
391+
// Mocked author schema
392+
const authorSchema = makeExecutableSchema({
393+
typeDefs: /* GraphQL */ `
394+
type User {
395+
email: String
396+
}
397+
type Query {
398+
usersByIds(ids: [ID!]): [User]
399+
}
400+
`,
401+
resolvers: {
402+
Query: {
403+
usersByIds: (_root, args) => {
404+
numCalls++;
405+
return args.ids.map((id: string) => ({ email: `${id}@test.com` }));
406+
},
407+
},
408+
},
409+
});
410+
411+
const linkTypeDefs = /* GraphQL */ `
412+
extend type Chirp {
413+
chirpedAtUser: User
414+
}
415+
`;
416+
417+
const stitchedSchema = stitchSchemas({
418+
subschemas: [chirpSchema, authorSchema],
419+
typeDefs: linkTypeDefs,
420+
resolvers: {
421+
Chirp: {
422+
chirpedAtUser: {
423+
selectionSet: `{ chirpedAtUserId }`,
424+
resolve(chirp, _args, context, info) {
425+
return batchDelegateToSchema({
426+
schema: authorSchema,
427+
operation: 'query' as OperationTypeNode,
428+
fieldName: 'usersByIds',
429+
key: chirp.chirpedAtUserId,
430+
argsFromKeys: (ids) => ({ ids }),
431+
context,
432+
info,
433+
});
434+
},
435+
},
436+
},
437+
},
438+
});
439+
440+
const query = /* GraphQL */ `
441+
query {
442+
trendingChirps {
443+
chirp1: chirpedAtUser {
444+
email
445+
}
446+
chirp2: chirpedAtUser {
447+
email
448+
}
449+
}
450+
}
451+
`;
452+
453+
await execute({ schema: stitchedSchema, document: parse(query) });
454+
455+
expect(numCalls).toEqual(1);
456+
});
369457
});
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { batchDelegateToSchema } from '@graphql-tools/batch-delegate';
2+
import { execute, isIncrementalResult } from '@graphql-tools/executor';
3+
import { makeExecutableSchema } from '@graphql-tools/schema';
4+
import { stitchSchemas } from '@graphql-tools/stitch';
5+
import { OperationTypeNode, parse } from 'graphql';
6+
import { describe, expect, test } from 'vitest';
7+
8+
describe('non-key arguments are taken into account when memoizing result', () => {
9+
test('memoizes non-key arguments as part of batch delegation', async () => {
10+
let numCalls = 0;
11+
12+
const chirpSchema = makeExecutableSchema({
13+
typeDefs: /* GraphQL */ `
14+
type Chirp {
15+
chirpedAtUserId: ID!
16+
}
17+
type Query {
18+
trendingChirps: [Chirp]
19+
}
20+
`,
21+
resolvers: {
22+
Query: {
23+
trendingChirps: () => [
24+
{ chirpedAtUserId: 1 },
25+
{ chirpedAtUserId: 2 },
26+
],
27+
},
28+
},
29+
});
30+
31+
// Mocked author schema
32+
const authorSchema = makeExecutableSchema({
33+
typeDefs: /* GraphQL */ `
34+
type User {
35+
email: String!
36+
}
37+
type Query {
38+
usersByIds(ids: [ID!], obfuscateEmail: Boolean!): [User]
39+
}
40+
`,
41+
resolvers: {
42+
Query: {
43+
usersByIds: (_root, args) => {
44+
numCalls++;
45+
return args.ids.map((id: string) => ({
46+
email: args.obfuscateEmail ? '***' : `${id}@test.com`,
47+
}));
48+
},
49+
},
50+
},
51+
});
52+
53+
const linkTypeDefs = /* GraphQL */ `
54+
extend type Chirp {
55+
chirpedAtUser(obfuscateEmail: Boolean!): User
56+
}
57+
`;
58+
59+
const stitchedSchema = stitchSchemas({
60+
subschemas: [chirpSchema, authorSchema],
61+
typeDefs: linkTypeDefs,
62+
resolvers: {
63+
Chirp: {
64+
chirpedAtUser: {
65+
selectionSet: `{ chirpedAtUserId }`,
66+
resolve(chirp, args, context, info) {
67+
return batchDelegateToSchema({
68+
schema: authorSchema,
69+
operation: 'query' as OperationTypeNode,
70+
fieldName: 'usersByIds',
71+
key: chirp.chirpedAtUserId,
72+
argsFromKeys: (ids) => ({ ids, ...args }),
73+
context,
74+
info,
75+
});
76+
},
77+
},
78+
},
79+
},
80+
});
81+
82+
const query = /* GraphQL */ `
83+
query {
84+
trendingChirps {
85+
withObfuscatedEmail: chirpedAtUser(obfuscateEmail: true) {
86+
email
87+
}
88+
withoutObfuscatedEmail: chirpedAtUser(obfuscateEmail: false) {
89+
email
90+
}
91+
}
92+
}
93+
`;
94+
95+
const result = await execute({
96+
schema: stitchedSchema,
97+
document: parse(query),
98+
});
99+
100+
expect(numCalls).toEqual(2);
101+
102+
if (isIncrementalResult(result)) throw Error('result is incremental');
103+
104+
expect(result.errors).toBeUndefined();
105+
106+
const chirps: any = result.data!['trendingChirps'];
107+
expect(chirps[0].withObfuscatedEmail.email).toBe(`***`);
108+
expect(chirps[1].withObfuscatedEmail.email).toBe(`***`);
109+
110+
expect(chirps[0].withoutObfuscatedEmail.email).toBe(`[email protected]`);
111+
expect(chirps[1].withoutObfuscatedEmail.email).toBe(`[email protected]`);
112+
});
113+
});

0 commit comments

Comments
 (0)