@apollo/gateway 0.300.0-alpha.3 → 2.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/LICENSE +95 -0
  2. package/README.md +1 -1
  3. package/dist/__generated__/graphqlTypes.d.ts +130 -0
  4. package/dist/__generated__/graphqlTypes.d.ts.map +1 -0
  5. package/dist/__generated__/graphqlTypes.js +25 -0
  6. package/dist/__generated__/graphqlTypes.js.map +1 -0
  7. package/dist/config.d.ts +106 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +47 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/datasources/LocalGraphQLDataSource.d.ts +3 -3
  12. package/dist/datasources/LocalGraphQLDataSource.d.ts.map +1 -1
  13. package/dist/datasources/LocalGraphQLDataSource.js +5 -5
  14. package/dist/datasources/LocalGraphQLDataSource.js.map +1 -1
  15. package/dist/datasources/RemoteGraphQLDataSource.d.ts +6 -4
  16. package/dist/datasources/RemoteGraphQLDataSource.d.ts.map +1 -1
  17. package/dist/datasources/RemoteGraphQLDataSource.js +64 -18
  18. package/dist/datasources/RemoteGraphQLDataSource.js.map +1 -1
  19. package/dist/datasources/index.d.ts +1 -1
  20. package/dist/datasources/index.d.ts.map +1 -1
  21. package/dist/datasources/index.js +1 -0
  22. package/dist/datasources/index.js.map +1 -1
  23. package/dist/datasources/parseCacheControlHeader.d.ts +2 -0
  24. package/dist/datasources/parseCacheControlHeader.d.ts.map +1 -0
  25. package/dist/datasources/parseCacheControlHeader.js +16 -0
  26. package/dist/datasources/parseCacheControlHeader.js.map +1 -0
  27. package/dist/datasources/types.d.ts +16 -1
  28. package/dist/datasources/types.d.ts.map +1 -1
  29. package/dist/datasources/types.js +7 -0
  30. package/dist/datasources/types.js.map +1 -1
  31. package/dist/executeQueryPlan.d.ts +2 -1
  32. package/dist/executeQueryPlan.d.ts.map +1 -1
  33. package/dist/executeQueryPlan.js +200 -113
  34. package/dist/executeQueryPlan.js.map +1 -1
  35. package/dist/index.d.ts +64 -80
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +554 -233
  38. package/dist/index.js.map +1 -1
  39. package/dist/loadServicesFromRemoteEndpoint.d.ts +9 -9
  40. package/dist/loadServicesFromRemoteEndpoint.d.ts.map +1 -1
  41. package/dist/loadServicesFromRemoteEndpoint.js +13 -8
  42. package/dist/loadServicesFromRemoteEndpoint.js.map +1 -1
  43. package/dist/loadSupergraphSdlFromStorage.d.ts +21 -0
  44. package/dist/loadSupergraphSdlFromStorage.d.ts.map +1 -0
  45. package/dist/loadSupergraphSdlFromStorage.js +128 -0
  46. package/dist/loadSupergraphSdlFromStorage.js.map +1 -0
  47. package/dist/operationContext.d.ts +17 -0
  48. package/dist/operationContext.d.ts.map +1 -0
  49. package/dist/operationContext.js +42 -0
  50. package/dist/operationContext.js.map +1 -0
  51. package/dist/outOfBandReporter.d.ts +13 -0
  52. package/dist/outOfBandReporter.d.ts.map +1 -0
  53. package/dist/outOfBandReporter.js +85 -0
  54. package/dist/outOfBandReporter.js.map +1 -0
  55. package/dist/utilities/array.d.ts +1 -2
  56. package/dist/utilities/array.d.ts.map +1 -1
  57. package/dist/utilities/array.js +7 -14
  58. package/dist/utilities/array.js.map +1 -1
  59. package/dist/utilities/assert.d.ts +2 -0
  60. package/dist/utilities/assert.d.ts.map +1 -0
  61. package/dist/utilities/assert.js +10 -0
  62. package/dist/utilities/assert.js.map +1 -0
  63. package/dist/utilities/cleanErrorOfInaccessibleNames.d.ts +3 -0
  64. package/dist/utilities/cleanErrorOfInaccessibleNames.d.ts.map +1 -0
  65. package/dist/utilities/cleanErrorOfInaccessibleNames.js +27 -0
  66. package/dist/utilities/cleanErrorOfInaccessibleNames.js.map +1 -0
  67. package/dist/utilities/deepMerge.js +2 -2
  68. package/dist/utilities/deepMerge.js.map +1 -1
  69. package/dist/utilities/graphql.d.ts +1 -4
  70. package/dist/utilities/graphql.d.ts.map +1 -1
  71. package/dist/utilities/graphql.js +3 -36
  72. package/dist/utilities/graphql.js.map +1 -1
  73. package/dist/utilities/opentelemetry.d.ts +10 -0
  74. package/dist/utilities/opentelemetry.d.ts.map +1 -0
  75. package/dist/utilities/opentelemetry.js +19 -0
  76. package/dist/utilities/opentelemetry.js.map +1 -0
  77. package/package.json +32 -23
  78. package/src/__generated__/graphqlTypes.ts +140 -0
  79. package/src/__mocks__/apollo-server-env.ts +56 -0
  80. package/src/__mocks__/make-fetch-happen-fetcher.ts +57 -0
  81. package/src/__mocks__/tsconfig.json +7 -0
  82. package/src/__tests__/build-query-plan.feature +40 -311
  83. package/src/__tests__/buildQueryPlan.test.ts +246 -426
  84. package/src/__tests__/executeQueryPlan.test.ts +2289 -194
  85. package/src/__tests__/execution-utils.ts +33 -26
  86. package/src/__tests__/gateway/__snapshots__/opentelemetry.test.ts.snap +195 -0
  87. package/src/__tests__/gateway/buildService.test.ts +16 -19
  88. package/src/__tests__/gateway/composedSdl.test.ts +44 -0
  89. package/src/__tests__/gateway/endToEnd.test.ts +166 -0
  90. package/src/__tests__/gateway/executor.test.ts +49 -43
  91. package/src/__tests__/gateway/lifecycle-hooks.test.ts +58 -29
  92. package/src/__tests__/gateway/opentelemetry.test.ts +123 -0
  93. package/src/__tests__/gateway/queryPlanCache.test.ts +19 -20
  94. package/src/__tests__/gateway/reporting.test.ts +83 -59
  95. package/src/__tests__/integration/abstract-types.test.ts +1086 -22
  96. package/src/__tests__/integration/aliases.test.ts +5 -6
  97. package/src/__tests__/integration/boolean.test.ts +40 -38
  98. package/src/__tests__/integration/complex-key.test.ts +41 -56
  99. package/src/__tests__/integration/configuration.test.ts +361 -0
  100. package/src/__tests__/integration/custom-directives.test.ts +61 -46
  101. package/src/__tests__/integration/fragments.test.ts +8 -2
  102. package/src/__tests__/integration/list-key.test.ts +2 -2
  103. package/src/__tests__/integration/logger.test.ts +2 -2
  104. package/src/__tests__/integration/multiple-key.test.ts +11 -12
  105. package/src/__tests__/integration/mutations.test.ts +8 -5
  106. package/src/__tests__/integration/networkRequests.test.ts +454 -294
  107. package/src/__tests__/integration/nockMocks.ts +100 -65
  108. package/src/__tests__/integration/provides.test.ts +9 -6
  109. package/src/__tests__/integration/requires.test.ts +17 -15
  110. package/src/__tests__/integration/scope.test.ts +557 -0
  111. package/src/__tests__/integration/unions.test.ts +1 -1
  112. package/src/__tests__/integration/value-types.test.ts +35 -32
  113. package/src/__tests__/integration/variables.test.ts +8 -2
  114. package/src/__tests__/loadServicesFromRemoteEndpoint.test.ts +6 -2
  115. package/src/__tests__/loadSupergraphSdlFromStorage.test.ts +343 -0
  116. package/src/__tests__/nockAssertions.ts +20 -0
  117. package/src/__tests__/queryPlanCucumber.test.ts +11 -61
  118. package/src/__tests__/testSetup.ts +1 -4
  119. package/src/__tests__/tsconfig.json +2 -1
  120. package/src/config.ts +227 -0
  121. package/src/core/__tests__/core.test.ts +412 -0
  122. package/src/datasources/LocalGraphQLDataSource.ts +9 -10
  123. package/src/datasources/RemoteGraphQLDataSource.ts +125 -45
  124. package/src/datasources/__tests__/LocalGraphQLDataSource.test.ts +11 -4
  125. package/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts +148 -79
  126. package/src/datasources/__tests__/tsconfig.json +4 -2
  127. package/src/datasources/index.ts +1 -1
  128. package/src/datasources/parseCacheControlHeader.ts +43 -0
  129. package/src/datasources/types.ts +47 -2
  130. package/src/executeQueryPlan.ts +275 -154
  131. package/src/index.ts +939 -480
  132. package/src/loadServicesFromRemoteEndpoint.ts +24 -17
  133. package/src/loadSupergraphSdlFromStorage.ts +186 -0
  134. package/src/make-fetch-happen.d.ts +2 -2
  135. package/src/operationContext.ts +70 -0
  136. package/src/outOfBandReporter.ts +126 -0
  137. package/src/utilities/__tests__/cleanErrorOfInaccessibleElements.test.ts +104 -0
  138. package/src/utilities/__tests__/tsconfig.json +8 -0
  139. package/src/utilities/array.ts +6 -28
  140. package/src/utilities/assert.ts +14 -0
  141. package/src/utilities/cleanErrorOfInaccessibleNames.ts +29 -0
  142. package/src/utilities/graphql.ts +0 -64
  143. package/src/utilities/opentelemetry.ts +13 -0
  144. package/CHANGELOG.md +0 -226
  145. package/LICENSE.md +0 -20
  146. package/dist/FieldSet.d.ts +0 -18
  147. package/dist/FieldSet.d.ts.map +0 -1
  148. package/dist/FieldSet.js +0 -96
  149. package/dist/FieldSet.js.map +0 -1
  150. package/dist/QueryPlan.d.ts +0 -41
  151. package/dist/QueryPlan.d.ts.map +0 -1
  152. package/dist/QueryPlan.js +0 -15
  153. package/dist/QueryPlan.js.map +0 -1
  154. package/dist/buildQueryPlan.d.ts +0 -44
  155. package/dist/buildQueryPlan.d.ts.map +0 -1
  156. package/dist/buildQueryPlan.js +0 -670
  157. package/dist/buildQueryPlan.js.map +0 -1
  158. package/dist/loadServicesFromStorage.d.ts +0 -21
  159. package/dist/loadServicesFromStorage.d.ts.map +0 -1
  160. package/dist/loadServicesFromStorage.js +0 -64
  161. package/dist/loadServicesFromStorage.js.map +0 -1
  162. package/dist/snapshotSerializers/astSerializer.d.ts +0 -3
  163. package/dist/snapshotSerializers/astSerializer.d.ts.map +0 -1
  164. package/dist/snapshotSerializers/astSerializer.js +0 -14
  165. package/dist/snapshotSerializers/astSerializer.js.map +0 -1
  166. package/dist/snapshotSerializers/index.d.ts +0 -13
  167. package/dist/snapshotSerializers/index.d.ts.map +0 -1
  168. package/dist/snapshotSerializers/index.js +0 -15
  169. package/dist/snapshotSerializers/index.js.map +0 -1
  170. package/dist/snapshotSerializers/queryPlanSerializer.d.ts +0 -3
  171. package/dist/snapshotSerializers/queryPlanSerializer.d.ts.map +0 -1
  172. package/dist/snapshotSerializers/queryPlanSerializer.js +0 -78
  173. package/dist/snapshotSerializers/queryPlanSerializer.js.map +0 -1
  174. package/dist/snapshotSerializers/selectionSetSerializer.d.ts +0 -3
  175. package/dist/snapshotSerializers/selectionSetSerializer.d.ts.map +0 -1
  176. package/dist/snapshotSerializers/selectionSetSerializer.js +0 -12
  177. package/dist/snapshotSerializers/selectionSetSerializer.js.map +0 -1
  178. package/dist/snapshotSerializers/typeSerializer.d.ts +0 -3
  179. package/dist/snapshotSerializers/typeSerializer.d.ts.map +0 -1
  180. package/dist/snapshotSerializers/typeSerializer.js +0 -12
  181. package/dist/snapshotSerializers/typeSerializer.js.map +0 -1
  182. package/dist/utilities/MultiMap.d.ts +0 -4
  183. package/dist/utilities/MultiMap.d.ts.map +0 -1
  184. package/dist/utilities/MultiMap.js +0 -17
  185. package/dist/utilities/MultiMap.js.map +0 -1
  186. package/src/FieldSet.ts +0 -169
  187. package/src/QueryPlan.ts +0 -57
  188. package/src/__tests__/matchers/toCallService.ts +0 -105
  189. package/src/__tests__/matchers/toHaveBeenCalledBefore.ts +0 -40
  190. package/src/__tests__/matchers/toHaveFetched.ts +0 -81
  191. package/src/__tests__/matchers/toMatchAST.ts +0 -64
  192. package/src/buildQueryPlan.ts +0 -1190
  193. package/src/loadServicesFromStorage.ts +0 -170
  194. package/src/snapshotSerializers/astSerializer.ts +0 -21
  195. package/src/snapshotSerializers/index.ts +0 -21
  196. package/src/snapshotSerializers/queryPlanSerializer.ts +0 -144
  197. package/src/snapshotSerializers/selectionSetSerializer.ts +0 -13
  198. package/src/snapshotSerializers/typeSerializer.ts +0 -11
  199. package/src/utilities/MultiMap.ts +0 -11
@@ -1,19 +1,26 @@
1
- import { GraphQLSchema, GraphQLError, getIntrospectionQuery } from 'graphql';
2
1
  import {
3
- addResolversToSchema,
4
- GraphQLResolverMap,
5
- } from 'apollo-graphql';
2
+ buildClientSchema,
3
+ getIntrospectionQuery,
4
+ GraphQLObjectType,
5
+ print,
6
+ } from 'graphql';
7
+ import { addResolversToSchema, GraphQLResolverMap } from 'apollo-graphql';
6
8
  import gql from 'graphql-tag';
7
- import { GraphQLRequestContext } from 'apollo-server-types';
9
+ import { GraphQLExecutionResult, GraphQLRequestContext } from 'apollo-server-types';
8
10
  import { AuthenticationError } from 'apollo-server-core';
9
-
10
- import { buildQueryPlan, buildOperationContext } from '../buildQueryPlan';
11
+ import { buildOperationContext } from '../operationContext';
11
12
  import { executeQueryPlan } from '../executeQueryPlan';
12
13
  import { LocalGraphQLDataSource } from '../datasources/LocalGraphQLDataSource';
13
-
14
- import { astSerializer, queryPlanSerializer } from '../snapshotSerializers';
15
- import { getFederatedTestingSchema, buildLocalService } from './execution-utils';
16
- import { fixtures } from 'apollo-federation-integration-testsuite';
14
+ import {
15
+ astSerializer,
16
+ queryPlanSerializer,
17
+ superGraphWithInaccessible,
18
+ } from 'apollo-federation-integration-testsuite';
19
+ import { QueryPlan, QueryPlanner } from '@apollo/query-planner';
20
+ import { ApolloGateway } from '..';
21
+ import { ApolloServerBase as ApolloServer } from 'apollo-server-core';
22
+ import { getFederatedTestingSchema } from './execution-utils';
23
+ import { Schema, Operation, parseOperation, buildSchemaFromAST } from '@apollo/federation-internals';
17
24
 
18
25
  expect.addSnapshotSerializer(astSerializer);
19
26
  expect.addSnapshotSerializer(queryPlanSerializer);
@@ -23,6 +30,40 @@ describe('executeQueryPlan', () => {
23
30
  [serviceName: string]: LocalGraphQLDataSource;
24
31
  };
25
32
 
33
+ const parseOp = (operation: string, operationSchema?: Schema): Operation => {
34
+ return parseOperation((operationSchema ?? schema), operation);
35
+ }
36
+
37
+ const buildPlan = (operation: string | Operation, operationQueryPlanner?: QueryPlanner, operationSchema?: Schema): QueryPlan => {
38
+ const op = typeof operation === 'string' ? parseOp(operation, operationSchema): operation;
39
+ return (operationQueryPlanner ?? queryPlanner).buildQueryPlan(op);
40
+ }
41
+
42
+ async function executePlan(
43
+ queryPlan: QueryPlan,
44
+ operation: Operation,
45
+ executeRequestContext?: GraphQLRequestContext,
46
+ executeSchema?: Schema,
47
+ executeServiceMap?: { [serviceName: string]: LocalGraphQLDataSource }
48
+ ): Promise<GraphQLExecutionResult> {
49
+ const operationContext = buildOperationContext({
50
+ schema: (executeSchema ?? schema).toAPISchema().toGraphQLJSSchema(),
51
+ operationDocument: gql`${operation.toString()}`,
52
+ });
53
+ return executeQueryPlan(
54
+ queryPlan,
55
+ executeServiceMap ?? serviceMap,
56
+ executeRequestContext ?? buildRequestContext(),
57
+ operationContext,
58
+ );
59
+ }
60
+
61
+ async function executeOperation(operationString: string, requestContext?: GraphQLRequestContext): Promise<GraphQLExecutionResult> {
62
+ const operation = parseOp(operationString);
63
+ const queryPlan = buildPlan(operation);
64
+ return executePlan(queryPlan, operation, requestContext);
65
+ }
66
+
26
67
  function overrideResolversInService(
27
68
  serviceName: string,
28
69
  resolvers: GraphQLResolverMap,
@@ -30,15 +71,25 @@ describe('executeQueryPlan', () => {
30
71
  addResolversToSchema(serviceMap[serviceName].schema, resolvers);
31
72
  }
32
73
 
33
- let schema: GraphQLSchema;
34
- let errors: GraphQLError[];
74
+ function spyOnEntitiesResolverInService(serviceName: string) {
75
+ const entitiesField = serviceMap[serviceName].schema
76
+ .getQueryType()!
77
+ .getFields()['_entities'];
78
+ return jest.spyOn(entitiesField, 'resolve');
79
+ }
35
80
 
81
+ let schema: Schema;
82
+ let queryPlanner: QueryPlanner;
36
83
  beforeEach(() => {
37
- ({ serviceMap, schema, errors } = getFederatedTestingSchema());
38
- expect(errors).toHaveLength(0);
84
+ expect(
85
+ () =>
86
+ ({ serviceMap, schema, queryPlanner } = getFederatedTestingSchema()),
87
+ ).not.toThrow();
39
88
  });
40
89
 
41
90
  function buildRequestContext(): GraphQLRequestContext {
91
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
92
+ // @ts-ignore
42
93
  return {
43
94
  cache: undefined as any,
44
95
  context: {},
@@ -50,23 +101,18 @@ describe('executeQueryPlan', () => {
50
101
 
51
102
  describe(`errors`, () => {
52
103
  it(`should not include an empty "errors" array when no errors were encountered`, async () => {
53
- const query = gql`
104
+ const operationString = `#graphql
54
105
  query {
55
106
  me {
56
- name
107
+ name {
108
+ first
109
+ last
110
+ }
57
111
  }
58
112
  }
59
113
  `;
60
114
 
61
- const operationContext = buildOperationContext(schema, query);
62
- const queryPlan = buildQueryPlan(operationContext);
63
-
64
- const response = await executeQueryPlan(
65
- queryPlan,
66
- serviceMap,
67
- buildRequestContext(),
68
- operationContext,
69
- );
115
+ const response = await executeOperation(operationString);
70
116
 
71
117
  expect(response).not.toHaveProperty('errors');
72
118
  });
@@ -80,29 +126,25 @@ describe('executeQueryPlan', () => {
80
126
  },
81
127
  });
82
128
 
83
- const query = gql`
129
+ const operationString = `#graphql
84
130
  query {
85
131
  me {
86
- name
132
+ name {
133
+ first
134
+ last
135
+ }
87
136
  }
88
137
  }
89
138
  `;
90
139
 
91
- const operationContext = buildOperationContext(schema, query);
92
- const queryPlan = buildQueryPlan(operationContext);
93
-
94
- const response = await executeQueryPlan(
95
- queryPlan,
96
- serviceMap,
97
- buildRequestContext(),
98
- operationContext,
99
- );
140
+ const response = await executeOperation(operationString);
100
141
 
101
142
  expect(response).toHaveProperty('data.me', null);
102
143
  expect(response).toHaveProperty(
103
144
  'errors.0.message',
104
145
  'Something went wrong',
105
146
  );
147
+ expect(response).toHaveProperty('errors.0.path', undefined);
106
148
  expect(response).toHaveProperty(
107
149
  'errors.0.extensions.code',
108
150
  'UNAUTHENTICATED',
@@ -111,11 +153,254 @@ describe('executeQueryPlan', () => {
111
153
  'errors.0.extensions.serviceName',
112
154
  'accounts',
113
155
  );
114
- expect(response).toHaveProperty(
115
- 'errors.0.extensions.query',
116
- '{me{name}}',
117
- );
118
- expect(response).toHaveProperty('errors.0.extensions.variables', {});
156
+ expect(response).not.toHaveProperty('errors.0.extensions.query');
157
+ expect(response).not.toHaveProperty('errors.0.extensions.variables');
158
+ });
159
+
160
+ it(`should not send request to downstream services when all entities are undefined`, async () => {
161
+ const accountsEntitiesResolverSpy =
162
+ spyOnEntitiesResolverInService('accounts');
163
+
164
+ const operationString = `#graphql
165
+ query {
166
+ # The first 3 products are all Furniture
167
+ topProducts(first: 3) {
168
+ reviews {
169
+ body
170
+ }
171
+ ... on Book {
172
+ reviews {
173
+ author {
174
+ name {
175
+ first
176
+ last
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ `;
184
+
185
+ const response = await executeOperation(operationString);
186
+
187
+ expect(accountsEntitiesResolverSpy).not.toHaveBeenCalled();
188
+
189
+ expect(response).toMatchInlineSnapshot(`
190
+ Object {
191
+ "data": Object {
192
+ "topProducts": Array [
193
+ Object {
194
+ "reviews": Array [
195
+ Object {
196
+ "body": "Love it!",
197
+ },
198
+ Object {
199
+ "body": "Prefer something else.",
200
+ },
201
+ ],
202
+ },
203
+ Object {
204
+ "reviews": Array [
205
+ Object {
206
+ "body": "Too expensive.",
207
+ },
208
+ ],
209
+ },
210
+ Object {
211
+ "reviews": Array [
212
+ Object {
213
+ "body": "Could be better.",
214
+ },
215
+ ],
216
+ },
217
+ ],
218
+ },
219
+ }
220
+ `);
221
+ });
222
+
223
+ it(`should send a request to downstream services for the remaining entities when some entities are undefined`, async () => {
224
+ const accountsEntitiesResolverSpy =
225
+ spyOnEntitiesResolverInService('accounts');
226
+
227
+ const operationString = `#graphql
228
+ query {
229
+ # The first 3 products are all Furniture, but the next 2 are Books
230
+ topProducts(first: 5) {
231
+ reviews {
232
+ body
233
+ }
234
+ ... on Book {
235
+ reviews {
236
+ author {
237
+ name {
238
+ first
239
+ last
240
+ }
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
246
+ `;
247
+
248
+ const response = await executeOperation(operationString);
249
+
250
+ expect(accountsEntitiesResolverSpy).toHaveBeenCalledTimes(1);
251
+ expect(accountsEntitiesResolverSpy.mock.calls[0][1]).toEqual({
252
+ representations: [
253
+ { __typename: 'User', id: '2' },
254
+ { __typename: 'User', id: '2' },
255
+ ],
256
+ });
257
+
258
+ expect(response).toMatchInlineSnapshot(`
259
+ Object {
260
+ "data": Object {
261
+ "topProducts": Array [
262
+ Object {
263
+ "reviews": Array [
264
+ Object {
265
+ "body": "Love it!",
266
+ },
267
+ Object {
268
+ "body": "Prefer something else.",
269
+ },
270
+ ],
271
+ },
272
+ Object {
273
+ "reviews": Array [
274
+ Object {
275
+ "body": "Too expensive.",
276
+ },
277
+ ],
278
+ },
279
+ Object {
280
+ "reviews": Array [
281
+ Object {
282
+ "body": "Could be better.",
283
+ },
284
+ ],
285
+ },
286
+ Object {
287
+ "reviews": Array [
288
+ Object {
289
+ "author": Object {
290
+ "name": Object {
291
+ "first": "Alan",
292
+ "last": "Turing",
293
+ },
294
+ },
295
+ "body": "Wish I had read this before.",
296
+ },
297
+ ],
298
+ },
299
+ Object {
300
+ "reviews": Array [
301
+ Object {
302
+ "author": Object {
303
+ "name": Object {
304
+ "first": "Alan",
305
+ "last": "Turing",
306
+ },
307
+ },
308
+ "body": "A bit outdated.",
309
+ },
310
+ ],
311
+ },
312
+ ],
313
+ },
314
+ }
315
+ `);
316
+ });
317
+
318
+ it(`should not send request to downstream service when entities don't match type conditions`, async () => {
319
+ const reviewsEntitiesResolverSpy =
320
+ spyOnEntitiesResolverInService('reviews');
321
+
322
+ const operationString = `#graphql
323
+ query {
324
+ # The first 3 products are all Furniture
325
+ topProducts(first: 3) {
326
+ ... on Book {
327
+ reviews {
328
+ body
329
+ }
330
+ }
331
+ }
332
+ }
333
+ `;
334
+
335
+ const response = await executeOperation(operationString);
336
+
337
+ expect(reviewsEntitiesResolverSpy).not.toHaveBeenCalled();
338
+
339
+ expect(response).toMatchInlineSnapshot(`
340
+ Object {
341
+ "data": Object {
342
+ "topProducts": Array [
343
+ Object {},
344
+ Object {},
345
+ Object {},
346
+ ],
347
+ },
348
+ }
349
+ `);
350
+ });
351
+
352
+ it(`should send a request to downstream services for the remaining entities when some entities don't match type conditions`, async () => {
353
+ const reviewsEntitiesResolverSpy =
354
+ spyOnEntitiesResolverInService('reviews');
355
+
356
+ const operationString = `#graphql
357
+ query {
358
+ # The first 3 products are all Furniture, but the next 2 are Books
359
+ topProducts(first: 5) {
360
+ ... on Book {
361
+ reviews {
362
+ body
363
+ }
364
+ }
365
+ }
366
+ }
367
+ `;
368
+
369
+ const response = await executeOperation(operationString);
370
+
371
+ expect(reviewsEntitiesResolverSpy).toHaveBeenCalledTimes(1);
372
+ expect(reviewsEntitiesResolverSpy.mock.calls[0][1]).toEqual({
373
+ representations: [
374
+ { __typename: 'Book', isbn: '0262510871' },
375
+ { __typename: 'Book', isbn: '0136291554' },
376
+ ],
377
+ });
378
+
379
+ expect(response).toMatchInlineSnapshot(`
380
+ Object {
381
+ "data": Object {
382
+ "topProducts": Array [
383
+ Object {},
384
+ Object {},
385
+ Object {},
386
+ Object {
387
+ "reviews": Array [
388
+ Object {
389
+ "body": "Wish I had read this before.",
390
+ },
391
+ ],
392
+ },
393
+ Object {
394
+ "reviews": Array [
395
+ Object {
396
+ "body": "A bit outdated.",
397
+ },
398
+ ],
399
+ },
400
+ ],
401
+ },
402
+ }
403
+ `);
119
404
  });
120
405
 
121
406
  it(`should still include other root-level results if one root-level field errors out`, async () => {
@@ -127,10 +412,13 @@ describe('executeQueryPlan', () => {
127
412
  },
128
413
  });
129
414
 
130
- const query = gql`
415
+ const operationString = `#graphql
131
416
  query {
132
417
  me {
133
- name
418
+ name {
419
+ first
420
+ last
421
+ }
134
422
  }
135
423
  topReviews {
136
424
  body
@@ -138,15 +426,7 @@ describe('executeQueryPlan', () => {
138
426
  }
139
427
  `;
140
428
 
141
- const operationContext = buildOperationContext(schema, query);
142
- const queryPlan = buildQueryPlan(operationContext);
143
-
144
- const response = await executeQueryPlan(
145
- queryPlan,
146
- serviceMap,
147
- buildRequestContext(),
148
- operationContext,
149
- );
429
+ const response = await executeOperation(operationString);
150
430
 
151
431
  expect(response).toHaveProperty('data.me', null);
152
432
  expect(response).toHaveProperty('data.topReviews', expect.any(Array));
@@ -155,10 +435,13 @@ describe('executeQueryPlan', () => {
155
435
  it(`should still include data from other services if one services is unavailable`, async () => {
156
436
  delete serviceMap.accounts;
157
437
 
158
- const query = gql`
438
+ const operationString = `#graphql
159
439
  query {
160
440
  me {
161
- name
441
+ name {
442
+ first
443
+ last
444
+ }
162
445
  }
163
446
  topReviews {
164
447
  body
@@ -166,15 +449,7 @@ describe('executeQueryPlan', () => {
166
449
  }
167
450
  `;
168
451
 
169
- const operationContext = buildOperationContext(schema, query);
170
- const queryPlan = buildQueryPlan(operationContext);
171
-
172
- const response = await executeQueryPlan(
173
- queryPlan,
174
- serviceMap,
175
- buildRequestContext(),
176
- operationContext,
177
- );
452
+ const response = await executeOperation(operationString);
178
453
 
179
454
  expect(response).toHaveProperty('data.me', null);
180
455
  expect(response).toHaveProperty('data.topReviews', expect.any(Array));
@@ -182,57 +457,67 @@ describe('executeQueryPlan', () => {
182
457
  });
183
458
 
184
459
  it(`should only return fields that have been requested directly`, async () => {
185
- const query = gql`
460
+ const operationString = `#graphql
186
461
  query {
187
462
  topReviews {
188
463
  body
189
464
  author {
190
- name
465
+ name {
466
+ first
467
+ last
468
+ }
191
469
  }
192
470
  }
193
471
  }
194
472
  `;
195
473
 
196
- const operationContext = buildOperationContext(schema, query);
197
- const queryPlan = buildQueryPlan(operationContext);
198
-
199
- const response = await executeQueryPlan(
200
- queryPlan,
201
- serviceMap,
202
- buildRequestContext(),
203
- operationContext,
204
- );
474
+ const response = await executeOperation(operationString);
205
475
 
206
476
  expect(response.data).toMatchInlineSnapshot(`
207
477
  Object {
208
478
  "topReviews": Array [
209
479
  Object {
210
480
  "author": Object {
211
- "name": "Ada Lovelace",
481
+ "name": Object {
482
+ "first": "Ada",
483
+ "last": "Lovelace",
484
+ },
212
485
  },
213
486
  "body": "Love it!",
214
487
  },
215
488
  Object {
216
489
  "author": Object {
217
- "name": "Ada Lovelace",
490
+ "name": Object {
491
+ "first": "Ada",
492
+ "last": "Lovelace",
493
+ },
218
494
  },
219
495
  "body": "Too expensive.",
220
496
  },
221
497
  Object {
222
498
  "author": Object {
223
- "name": "Alan Turing",
499
+ "name": Object {
500
+ "first": "Alan",
501
+ "last": "Turing",
502
+ },
224
503
  },
225
504
  "body": "Could be better.",
226
505
  },
227
506
  Object {
228
507
  "author": Object {
229
- "name": "Alan Turing",
508
+ "name": Object {
509
+ "first": "Alan",
510
+ "last": "Turing",
511
+ },
230
512
  },
231
513
  "body": "Prefer something else.",
232
514
  },
233
515
  Object {
234
516
  "author": Object {
235
- "name": "Alan Turing",
517
+ "name": Object {
518
+ "first": "Alan",
519
+ "last": "Turing",
520
+ },
236
521
  },
237
522
  "body": "Wish I had read this before.",
238
523
  },
@@ -242,54 +527,60 @@ describe('executeQueryPlan', () => {
242
527
  });
243
528
 
244
529
  it('should not duplicate variable definitions', async () => {
245
- const query = gql`
530
+ const operationString = `#graphql
246
531
  query Test($first: Int!) {
247
532
  first: topReviews(first: $first) {
248
533
  body
249
534
  author {
250
- name
535
+ name {
536
+ first
537
+ last
538
+ }
251
539
  }
252
540
  }
253
541
  second: topReviews(first: $first) {
254
542
  body
255
543
  author {
256
- name
544
+ name {
545
+ first
546
+ last
547
+ }
257
548
  }
258
549
  }
259
550
  }
260
551
  `;
261
552
 
262
- const operationContext = buildOperationContext(schema, query);
263
- const queryPlan = buildQueryPlan(operationContext);
264
-
265
553
  const requestContext = buildRequestContext();
266
554
  requestContext.request.variables = { first: 3 };
267
-
268
- const response = await executeQueryPlan(
269
- queryPlan,
270
- serviceMap,
271
- requestContext,
272
- operationContext,
273
- );
555
+ const response = await executeOperation(operationString, requestContext);
274
556
 
275
557
  expect(response.data).toMatchInlineSnapshot(`
276
558
  Object {
277
559
  "first": Array [
278
560
  Object {
279
561
  "author": Object {
280
- "name": "Ada Lovelace",
562
+ "name": Object {
563
+ "first": "Ada",
564
+ "last": "Lovelace",
565
+ },
281
566
  },
282
567
  "body": "Love it!",
283
568
  },
284
569
  Object {
285
570
  "author": Object {
286
- "name": "Ada Lovelace",
571
+ "name": Object {
572
+ "first": "Ada",
573
+ "last": "Lovelace",
574
+ },
287
575
  },
288
576
  "body": "Too expensive.",
289
577
  },
290
578
  Object {
291
579
  "author": Object {
292
- "name": "Alan Turing",
580
+ "name": Object {
581
+ "first": "Alan",
582
+ "last": "Turing",
583
+ },
293
584
  },
294
585
  "body": "Could be better.",
295
586
  },
@@ -297,19 +588,28 @@ describe('executeQueryPlan', () => {
297
588
  "second": Array [
298
589
  Object {
299
590
  "author": Object {
300
- "name": "Ada Lovelace",
591
+ "name": Object {
592
+ "first": "Ada",
593
+ "last": "Lovelace",
594
+ },
301
595
  },
302
596
  "body": "Love it!",
303
597
  },
304
598
  Object {
305
599
  "author": Object {
306
- "name": "Ada Lovelace",
600
+ "name": Object {
601
+ "first": "Ada",
602
+ "last": "Lovelace",
603
+ },
307
604
  },
308
605
  "body": "Too expensive.",
309
606
  },
310
607
  Object {
311
608
  "author": Object {
312
- "name": "Alan Turing",
609
+ "name": Object {
610
+ "first": "Alan",
611
+ "last": "Turing",
612
+ },
313
613
  },
314
614
  "body": "Could be better.",
315
615
  },
@@ -319,30 +619,24 @@ describe('executeQueryPlan', () => {
319
619
  });
320
620
 
321
621
  it('should include variables in non-root requests', async () => {
322
- const query = gql`
622
+ const operationString = `#graphql
323
623
  query Test($locale: String) {
324
624
  topReviews {
325
625
  body
326
626
  author {
327
- name
627
+ name {
628
+ first
629
+ last
630
+ }
328
631
  birthDate(locale: $locale)
329
632
  }
330
633
  }
331
634
  }
332
635
  `;
333
636
 
334
- const operationContext = buildOperationContext(schema, query);
335
- const queryPlan = buildQueryPlan(operationContext);
336
-
337
637
  const requestContext = buildRequestContext();
338
638
  requestContext.request.variables = { locale: 'en-US' };
339
-
340
- const response = await executeQueryPlan(
341
- queryPlan,
342
- serviceMap,
343
- requestContext,
344
- operationContext,
345
- );
639
+ const response = await executeOperation(operationString, requestContext);
346
640
 
347
641
  expect(response.data).toMatchInlineSnapshot(`
348
642
  Object {
@@ -350,35 +644,50 @@ describe('executeQueryPlan', () => {
350
644
  Object {
351
645
  "author": Object {
352
646
  "birthDate": "12/10/1815",
353
- "name": "Ada Lovelace",
647
+ "name": Object {
648
+ "first": "Ada",
649
+ "last": "Lovelace",
650
+ },
354
651
  },
355
652
  "body": "Love it!",
356
653
  },
357
654
  Object {
358
655
  "author": Object {
359
656
  "birthDate": "12/10/1815",
360
- "name": "Ada Lovelace",
657
+ "name": Object {
658
+ "first": "Ada",
659
+ "last": "Lovelace",
660
+ },
361
661
  },
362
662
  "body": "Too expensive.",
363
663
  },
364
664
  Object {
365
665
  "author": Object {
366
666
  "birthDate": "6/23/1912",
367
- "name": "Alan Turing",
667
+ "name": Object {
668
+ "first": "Alan",
669
+ "last": "Turing",
670
+ },
368
671
  },
369
672
  "body": "Could be better.",
370
673
  },
371
674
  Object {
372
675
  "author": Object {
373
676
  "birthDate": "6/23/1912",
374
- "name": "Alan Turing",
677
+ "name": Object {
678
+ "first": "Alan",
679
+ "last": "Turing",
680
+ },
375
681
  },
376
682
  "body": "Prefer something else.",
377
683
  },
378
684
  Object {
379
685
  "author": Object {
380
686
  "birthDate": "6/23/1912",
381
- "name": "Alan Turing",
687
+ "name": Object {
688
+ "first": "Alan",
689
+ "last": "Turing",
690
+ },
382
691
  },
383
692
  "body": "Wish I had read this before.",
384
693
  },
@@ -388,27 +697,14 @@ describe('executeQueryPlan', () => {
388
697
  });
389
698
 
390
699
  it('can execute an introspection query', async () => {
391
- const operationContext = buildOperationContext(
392
- schema,
393
- gql`
394
- ${getIntrospectionQuery()}
395
- `,
396
- );
397
- const queryPlan = buildQueryPlan(operationContext);
398
-
399
- const response = await executeQueryPlan(
400
- queryPlan,
401
- serviceMap,
402
- buildRequestContext(),
403
- operationContext,
404
- );
700
+ const response = await executeOperation(`${getIntrospectionQuery()}`);
405
701
 
406
702
  expect(response.data).toHaveProperty('__schema');
407
703
  expect(response.errors).toBeUndefined();
408
704
  });
409
705
 
410
706
  it(`can execute queries on interface types`, async () => {
411
- const query = gql`
707
+ const operationString = `#graphql
412
708
  query {
413
709
  vehicle(id: "1") {
414
710
  description
@@ -418,15 +714,7 @@ describe('executeQueryPlan', () => {
418
714
  }
419
715
  `;
420
716
 
421
- const operationContext = buildOperationContext(schema, query);
422
- const queryPlan = buildQueryPlan(operationContext);
423
-
424
- const response = await executeQueryPlan(
425
- queryPlan,
426
- serviceMap,
427
- buildRequestContext(),
428
- operationContext,
429
- );
717
+ const response = await executeOperation(operationString);
430
718
 
431
719
  expect(response.data).toMatchInlineSnapshot(`
432
720
  Object {
@@ -440,10 +728,13 @@ describe('executeQueryPlan', () => {
440
728
  });
441
729
 
442
730
  it(`can execute queries whose fields are interface types`, async () => {
443
- const query = gql`
731
+ const operationString = `#graphql
444
732
  query {
445
733
  user(id: "1") {
446
- name
734
+ name {
735
+ first
736
+ last
737
+ }
447
738
  vehicle {
448
739
  description
449
740
  price
@@ -453,20 +744,15 @@ describe('executeQueryPlan', () => {
453
744
  }
454
745
  `;
455
746
 
456
- const operationContext = buildOperationContext(schema, query);
457
- const queryPlan = buildQueryPlan(operationContext);
458
-
459
- const response = await executeQueryPlan(
460
- queryPlan,
461
- serviceMap,
462
- buildRequestContext(),
463
- operationContext,
464
- );
747
+ const response = await executeOperation(operationString);
465
748
 
466
749
  expect(response.data).toMatchInlineSnapshot(`
467
750
  Object {
468
751
  "user": Object {
469
- "name": "Ada Lovelace",
752
+ "name": Object {
753
+ "first": "Ada",
754
+ "last": "Lovelace",
755
+ },
470
756
  "vehicle": Object {
471
757
  "description": "Humble Toyota",
472
758
  "price": "9990",
@@ -478,10 +764,13 @@ describe('executeQueryPlan', () => {
478
764
  });
479
765
 
480
766
  it(`can execute queries whose fields are union types`, async () => {
481
- const query = gql`
767
+ const operationString = `#graphql
482
768
  query {
483
769
  user(id: "1") {
484
- name
770
+ name {
771
+ first
772
+ last
773
+ }
485
774
  thing {
486
775
  ... on Vehicle {
487
776
  description
@@ -496,20 +785,15 @@ describe('executeQueryPlan', () => {
496
785
  }
497
786
  `;
498
787
 
499
- const operationContext = buildOperationContext(schema, query);
500
- const queryPlan = buildQueryPlan(operationContext);
501
-
502
- const response = await executeQueryPlan(
503
- queryPlan,
504
- serviceMap,
505
- buildRequestContext(),
506
- operationContext,
507
- );
788
+ const response = await executeOperation(operationString);
508
789
 
509
790
  expect(response.data).toMatchInlineSnapshot(`
510
791
  Object {
511
792
  "user": Object {
512
- "name": "Ada Lovelace",
793
+ "name": Object {
794
+ "first": "Ada",
795
+ "last": "Lovelace",
796
+ },
513
797
  "thing": Object {
514
798
  "description": "Humble Toyota",
515
799
  "price": "9990",
@@ -521,7 +805,7 @@ describe('executeQueryPlan', () => {
521
805
  });
522
806
 
523
807
  it('can execute queries with falsey @requires (except undefined)', async () => {
524
- const query = gql`
808
+ const operationString = `#graphql
525
809
  query {
526
810
  books {
527
811
  name # Requires title, year (on Book type)
@@ -529,15 +813,7 @@ describe('executeQueryPlan', () => {
529
813
  }
530
814
  `;
531
815
 
532
- const operationContext = buildOperationContext(schema, query);
533
- const queryPlan = buildQueryPlan(operationContext);
534
-
535
- const response = await executeQueryPlan(
536
- queryPlan,
537
- serviceMap,
538
- buildRequestContext(),
539
- operationContext,
540
- );
816
+ const response = await executeOperation(operationString);
541
817
 
542
818
  expect(response.data).toMatchInlineSnapshot(`
543
819
  Object {
@@ -566,7 +842,7 @@ describe('executeQueryPlan', () => {
566
842
  });
567
843
 
568
844
  it('can execute queries with list @requires', async () => {
569
- const query = gql`
845
+ const operationString = `#graphql
570
846
  query {
571
847
  book(isbn: "0201633612") {
572
848
  # Requires similarBooks { isbn }
@@ -578,15 +854,7 @@ describe('executeQueryPlan', () => {
578
854
  }
579
855
  `;
580
856
 
581
- const operationContext = buildOperationContext(schema, query);
582
- const queryPlan = buildQueryPlan(operationContext);
583
-
584
- const response = await executeQueryPlan(
585
- queryPlan,
586
- serviceMap,
587
- buildRequestContext(),
588
- operationContext,
589
- );
857
+ const response = await executeOperation(operationString);
590
858
 
591
859
  expect(response.errors).toMatchInlineSnapshot(`undefined`);
592
860
 
@@ -609,7 +877,7 @@ describe('executeQueryPlan', () => {
609
877
  });
610
878
 
611
879
  it('can execute queries with selections on null @requires fields', async () => {
612
- const query = gql`
880
+ const operationString = `#graphql
613
881
  query {
614
882
  book(isbn: "0987654321") {
615
883
  # Requires similarBooks { isbn }
@@ -621,15 +889,7 @@ describe('executeQueryPlan', () => {
621
889
  }
622
890
  `;
623
891
 
624
- const operationContext = buildOperationContext(schema, query);
625
- const queryPlan = buildQueryPlan(operationContext);
626
-
627
- const response = await executeQueryPlan(
628
- queryPlan,
629
- serviceMap,
630
- buildRequestContext(),
631
- operationContext,
632
- );
892
+ const response = await executeOperation(operationString);
633
893
 
634
894
  expect(response.errors).toBeUndefined();
635
895
 
@@ -641,4 +901,1839 @@ describe('executeQueryPlan', () => {
641
901
  }
642
902
  `);
643
903
  });
904
+
905
+ it(`can execute queries with @include on inline fragment with extension field`, async () => {
906
+ const operationString = `#graphql
907
+ query {
908
+ topProducts(first: 5) {
909
+ ... on Book @include(if: true) {
910
+ price
911
+ inStock
912
+ }
913
+ ... on Furniture {
914
+ price
915
+ inStock
916
+ }
917
+ }
918
+ }
919
+ `;
920
+
921
+ const response = await executeOperation(operationString);
922
+
923
+ expect(response.data).toMatchInlineSnapshot(`
924
+ Object {
925
+ "topProducts": Array [
926
+ Object {
927
+ "inStock": true,
928
+ "price": "899",
929
+ },
930
+ Object {
931
+ "inStock": false,
932
+ "price": "1299",
933
+ },
934
+ Object {
935
+ "inStock": true,
936
+ "price": "54",
937
+ },
938
+ Object {
939
+ "inStock": true,
940
+ "price": "39",
941
+ },
942
+ Object {
943
+ "inStock": false,
944
+ "price": "29",
945
+ },
946
+ ],
947
+ }
948
+ `);
949
+ });
950
+
951
+ describe('@inaccessible', () => {
952
+ it(`should not include @inaccessible fields in introspection`, async () => {
953
+ schema = buildSchemaFromAST(superGraphWithInaccessible);
954
+
955
+ const operation = parseOp(`${getIntrospectionQuery()}`, schema);
956
+ queryPlanner = new QueryPlanner(schema);
957
+ const queryPlan = queryPlanner.buildQueryPlan(operation);
958
+ const response = await executePlan(queryPlan, operation, undefined, schema);
959
+
960
+ expect(response.data).toHaveProperty('__schema');
961
+ expect(response.errors).toBeUndefined();
962
+
963
+ const introspectedSchema = buildClientSchema(response.data as any);
964
+
965
+ const userType = introspectedSchema.getType('User') as GraphQLObjectType;
966
+
967
+ expect(userType.getFields()['username']).toBeDefined();
968
+ expect(userType.getFields()['ssn']).toBeUndefined();
969
+ });
970
+
971
+ it(`should not return @inaccessible fields`, async () => {
972
+ const operationString = `#graphql
973
+ query {
974
+ topReviews {
975
+ body
976
+ author {
977
+ username
978
+ ssn
979
+ }
980
+ }
981
+ }
982
+ `;
983
+
984
+ const operation = parseOp(operationString);
985
+
986
+ schema = buildSchemaFromAST(superGraphWithInaccessible);
987
+
988
+ queryPlanner = new QueryPlanner(schema);
989
+ const queryPlan = queryPlanner.buildQueryPlan(operation);
990
+
991
+ const response = await executePlan(queryPlan, operation, undefined, schema);
992
+
993
+ expect(response.data).toMatchInlineSnapshot(`
994
+ Object {
995
+ "topReviews": Array [
996
+ Object {
997
+ "author": Object {
998
+ "username": "@ada",
999
+ },
1000
+ "body": "Love it!",
1001
+ },
1002
+ Object {
1003
+ "author": Object {
1004
+ "username": "@ada",
1005
+ },
1006
+ "body": "Too expensive.",
1007
+ },
1008
+ Object {
1009
+ "author": Object {
1010
+ "username": "@complete",
1011
+ },
1012
+ "body": "Could be better.",
1013
+ },
1014
+ Object {
1015
+ "author": Object {
1016
+ "username": "@complete",
1017
+ },
1018
+ "body": "Prefer something else.",
1019
+ },
1020
+ Object {
1021
+ "author": Object {
1022
+ "username": "@complete",
1023
+ },
1024
+ "body": "Wish I had read this before.",
1025
+ },
1026
+ ],
1027
+ }
1028
+ `);
1029
+ });
1030
+
1031
+ it(`should return a validation error when an @inaccessible field is requested`, async () => {
1032
+ // Because validation is part of the Apollo Server request pipeline,
1033
+ // we have to construct an instance of ApolloServer and execute the
1034
+ // the operation against it.
1035
+ // This test uses the same `gateway.load()` pattern as existing tests that
1036
+ // execute operations against Apollo Server (like queryPlanCache.test.ts).
1037
+ // But note that this is only one possible initialization path for the
1038
+ // gateway, and with the current duplication of logic we'd actually need
1039
+ // to test other scenarios (like loading from supergraph SDL) separately.
1040
+ const gateway = new ApolloGateway({
1041
+ supergraphSdl: print(superGraphWithInaccessible),
1042
+ });
1043
+
1044
+ const { schema, executor } = await gateway.load();
1045
+
1046
+ const server = new ApolloServer({ schema, executor });
1047
+
1048
+ const query = `#graphql
1049
+ query {
1050
+ topReviews {
1051
+ body
1052
+ author {
1053
+ username
1054
+ ssn
1055
+ }
1056
+ }
1057
+ }
1058
+ `;
1059
+
1060
+ const response = await server.executeOperation({
1061
+ query,
1062
+ });
1063
+
1064
+ expect(response.data).toBeUndefined();
1065
+ expect(response.errors).toMatchInlineSnapshot(`
1066
+ Array [
1067
+ [ValidationError: Cannot query field "ssn" on type "User".],
1068
+ ]
1069
+ `);
1070
+ });
1071
+
1072
+ // THIS TEST SHOULD BE MODIFIED AFTER THE ISSUE OUTLINED IN
1073
+ // https://github.com/apollographql/federation/issues/981 HAS BEEN RESOLVED.
1074
+ // IT IS BEING LEFT HERE AS A TEST THAT WILL INTENTIONALLY FAIL WHEN
1075
+ // IT IS RESOLVED IF IT'S NOT ADDRESSED.
1076
+ //
1077
+ // This test became relevant after a combination of two things:
1078
+ // 1. when the gateway started surfacing errors from subgraphs happened in
1079
+ // https://github.com/apollographql/federation/pull/159
1080
+ // 2. the idea of field redaction became necessary after
1081
+ // https://github.com/apollographql/federation/pull/893,
1082
+ // which introduced the notion of inaccessible fields.
1083
+ // The redaction started in
1084
+ // https://github.com/apollographql/federation/issues/974, which added
1085
+ // the following test.
1086
+ //
1087
+ // However, the error surfacing (first, above) needed to be reverted, thus
1088
+ // de-necessitating this redaction logic which is no longer tested.
1089
+ it(`doesn't leak @inaccessible typenames in error messages`, async () => {
1090
+ const operationString = `#graphql
1091
+ query {
1092
+ vehicle(id: "1") {
1093
+ id
1094
+ }
1095
+ }
1096
+ `;
1097
+
1098
+ const operation = parseOp(operationString);
1099
+
1100
+ // Vehicle ID #1 is a "Car" type.
1101
+ // This supergraph marks the "Car" type as inaccessible.
1102
+ schema = buildSchemaFromAST(superGraphWithInaccessible);
1103
+
1104
+ queryPlanner = new QueryPlanner(schema);
1105
+ const queryPlan = queryPlanner.buildQueryPlan(operation);
1106
+
1107
+ const response = await executePlan(queryPlan, operation, undefined, schema);
1108
+
1109
+ expect(response.data?.vehicle).toEqual(null);
1110
+ expect(response.errors).toBeUndefined();
1111
+ // SEE COMMENT ABOVE THIS TEST. SHOULD BE RE-ENABLED AFTER #981 IS FIXED!
1112
+ // expect(response.errors).toMatchInlineSnapshot(`
1113
+ // Array [
1114
+ // [GraphQLError: Abstract type "Vehicle" was resolve to a type [inaccessible type] that does not exist inside schema.],
1115
+ // ]
1116
+ // `);
1117
+ });
1118
+ });
1119
+
1120
+ describe('reusing root types', () => {
1121
+ it('can query other subgraphs when the Query type is the type of a field', async () => {
1122
+ const s1 = gql`
1123
+ type Query {
1124
+ getA: A
1125
+ }
1126
+
1127
+ type A {
1128
+ q: Query
1129
+ }
1130
+ `;
1131
+
1132
+ const s2 = gql`
1133
+ type Query {
1134
+ one: Int
1135
+ }
1136
+ `;
1137
+
1138
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([
1139
+ { name: 'S1', typeDefs: s1 },
1140
+ { name: 'S2', typeDefs: s2 }
1141
+ ]);
1142
+
1143
+ addResolversToSchema(serviceMap['S1'].schema, {
1144
+ Query: {
1145
+ getA() {
1146
+ return {
1147
+ getA: {}
1148
+ };
1149
+ },
1150
+ },
1151
+ A: {
1152
+ q() {
1153
+ return Object.create(null);
1154
+ }
1155
+ }
1156
+ });
1157
+
1158
+ addResolversToSchema(serviceMap['S2'].schema, {
1159
+ Query: {
1160
+ one() {
1161
+ return 1;
1162
+ },
1163
+ },
1164
+ });
1165
+
1166
+ const operation = parseOp(`
1167
+ query {
1168
+ getA {
1169
+ q {
1170
+ one
1171
+ }
1172
+ }
1173
+ }
1174
+ `, schema);
1175
+
1176
+ const queryPlan = buildPlan(operation, queryPlanner);
1177
+
1178
+ expect(queryPlan).toMatchInlineSnapshot(`
1179
+ QueryPlan {
1180
+ Sequence {
1181
+ Fetch(service: "S1") {
1182
+ {
1183
+ getA {
1184
+ q {
1185
+ __typename
1186
+ }
1187
+ }
1188
+ }
1189
+ },
1190
+ Flatten(path: "getA.q") {
1191
+ Fetch(service: "S2") {
1192
+ {
1193
+ ... on Query {
1194
+ one
1195
+ }
1196
+ }
1197
+ },
1198
+ },
1199
+ },
1200
+ }
1201
+ `);
1202
+
1203
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
1204
+
1205
+ expect(response.data).toMatchInlineSnapshot(`
1206
+ Object {
1207
+ "getA": Object {
1208
+ "q": Object {
1209
+ "one": 1,
1210
+ },
1211
+ },
1212
+ }
1213
+ `);
1214
+ })
1215
+
1216
+ it('can query other subgraphs when the Query type is the type of a field after a mutation', async () => {
1217
+ const s1 = gql`
1218
+ type Query {
1219
+ one: Int
1220
+ }
1221
+
1222
+ type Mutation {
1223
+ mutateSomething: Query
1224
+ }
1225
+ `;
1226
+
1227
+ const s2 = gql`
1228
+ type Query {
1229
+ two: Int
1230
+ }
1231
+ `;
1232
+
1233
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([
1234
+ { name: 'S1', typeDefs: s1 },
1235
+ { name: 'S2', typeDefs: s2 }
1236
+ ]);
1237
+
1238
+ let hasMutated = false;
1239
+
1240
+ addResolversToSchema(serviceMap['S1'].schema, {
1241
+ Query: {
1242
+ one() {
1243
+ return 1;
1244
+ },
1245
+ },
1246
+ Mutation: {
1247
+ mutateSomething() {
1248
+ hasMutated = true;
1249
+ return {};
1250
+ },
1251
+ }
1252
+ });
1253
+
1254
+ addResolversToSchema(serviceMap['S2'].schema, {
1255
+ Query: {
1256
+ two() {
1257
+ return 2;
1258
+ },
1259
+ },
1260
+ });
1261
+
1262
+ const operation = parseOp(`
1263
+ mutation {
1264
+ mutateSomething {
1265
+ one
1266
+ two
1267
+ }
1268
+ }
1269
+ `, schema);
1270
+
1271
+ const queryPlan = buildPlan(operation, queryPlanner);
1272
+
1273
+ expect(queryPlan).toMatchInlineSnapshot(`
1274
+ QueryPlan {
1275
+ Sequence {
1276
+ Fetch(service: "S1") {
1277
+ {
1278
+ mutateSomething {
1279
+ __typename
1280
+ one
1281
+ }
1282
+ }
1283
+ },
1284
+ Flatten(path: "mutateSomething") {
1285
+ Fetch(service: "S2") {
1286
+ {
1287
+ ... on Query {
1288
+ two
1289
+ }
1290
+ }
1291
+ },
1292
+ },
1293
+ },
1294
+ }
1295
+ `);
1296
+
1297
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
1298
+
1299
+ expect(hasMutated).toBeTruthy();
1300
+ expect(response.data).toMatchInlineSnapshot(`
1301
+ Object {
1302
+ "mutateSomething": Object {
1303
+ "one": 1,
1304
+ "two": 2,
1305
+ },
1306
+ }
1307
+ `);
1308
+ })
1309
+
1310
+ it('can mutate other subgraphs when the Mutation type is the type of a field', async () => {
1311
+ const s1 = gql`
1312
+ type Query {
1313
+ getA: A
1314
+ }
1315
+
1316
+ type Mutation {
1317
+ mutateOne: Int
1318
+ }
1319
+
1320
+ type A {
1321
+ m: Mutation
1322
+ }
1323
+ `;
1324
+
1325
+ const s2 = gql`
1326
+ type Mutation {
1327
+ mutateTwo: Int
1328
+ }
1329
+ `;
1330
+
1331
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([
1332
+ { name: 'S1', typeDefs: s1 },
1333
+ { name: 'S2', typeDefs: s2 }
1334
+ ]);
1335
+
1336
+ let mutateOneCalled = false;
1337
+ let mutateTwoCalled = false;
1338
+
1339
+ addResolversToSchema(serviceMap['S1'].schema, {
1340
+ Query: {
1341
+ getA() {
1342
+ return {
1343
+ getA: {}
1344
+ };
1345
+ },
1346
+ },
1347
+ A: {
1348
+ m() {
1349
+ return Object.create(null);
1350
+ }
1351
+ },
1352
+ Mutation: {
1353
+ mutateOne() {
1354
+ mutateOneCalled = true;
1355
+ return 1;
1356
+ }
1357
+ }
1358
+ });
1359
+
1360
+ addResolversToSchema(serviceMap['S2'].schema, {
1361
+ Mutation: {
1362
+ mutateTwo() {
1363
+ mutateTwoCalled = true;
1364
+ return 2;
1365
+ },
1366
+ },
1367
+ });
1368
+
1369
+ const operation = parseOp(`
1370
+ query {
1371
+ getA {
1372
+ m {
1373
+ mutateOne
1374
+ mutateTwo
1375
+ }
1376
+ }
1377
+ }
1378
+ `, schema);
1379
+
1380
+ const queryPlan = buildPlan(operation, queryPlanner);
1381
+
1382
+ expect(queryPlan).toMatchInlineSnapshot(`
1383
+ QueryPlan {
1384
+ Sequence {
1385
+ Fetch(service: "S1") {
1386
+ {
1387
+ getA {
1388
+ m {
1389
+ __typename
1390
+ mutateOne
1391
+ }
1392
+ }
1393
+ }
1394
+ },
1395
+ Flatten(path: "getA.m") {
1396
+ Fetch(service: "S2") {
1397
+ {
1398
+ ... on Mutation {
1399
+ mutateTwo
1400
+ }
1401
+ }
1402
+ },
1403
+ },
1404
+ },
1405
+ }
1406
+ `);
1407
+
1408
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
1409
+ expect(mutateOneCalled).toBeTruthy();
1410
+ expect(mutateTwoCalled).toBeTruthy();
1411
+ expect(response.data).toMatchInlineSnapshot(`
1412
+ Object {
1413
+ "getA": Object {
1414
+ "m": Object {
1415
+ "mutateOne": 1,
1416
+ "mutateTwo": 2,
1417
+ },
1418
+ },
1419
+ }
1420
+ `);
1421
+ })
1422
+
1423
+ it('can mutate other subgraphs when the Mutation type is the type of a field after a mutation', async () => {
1424
+ const s1 = gql`
1425
+ type Query {
1426
+ one: Int
1427
+ }
1428
+
1429
+ type Mutation {
1430
+ mutateSomething: Mutation
1431
+ }
1432
+ `;
1433
+
1434
+ const s2 = gql`
1435
+ type Mutation {
1436
+ mutateTwo: Int
1437
+ }
1438
+ `;
1439
+
1440
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([
1441
+ { name: 'S1', typeDefs: s1 },
1442
+ { name: 'S2', typeDefs: s2 }
1443
+ ]);
1444
+
1445
+ let somethingMutationCount = 0;
1446
+ let hasMutatedTwo = false;
1447
+
1448
+ addResolversToSchema(serviceMap['S1'].schema, {
1449
+ Query: {
1450
+ one() {
1451
+ return 1;
1452
+ },
1453
+ },
1454
+ Mutation: {
1455
+ mutateSomething() {
1456
+ ++somethingMutationCount;
1457
+ return {};
1458
+ },
1459
+ }
1460
+ });
1461
+
1462
+ addResolversToSchema(serviceMap['S2'].schema, {
1463
+ Mutation: {
1464
+ mutateTwo() {
1465
+ hasMutatedTwo = true;
1466
+ return 2;
1467
+ },
1468
+ },
1469
+ });
1470
+
1471
+ const operation = parseOp(`
1472
+ mutation {
1473
+ mutateSomething {
1474
+ mutateSomething {
1475
+ mutateTwo
1476
+ }
1477
+ }
1478
+ }
1479
+ `, schema);
1480
+
1481
+ const queryPlan = buildPlan(operation, queryPlanner);
1482
+
1483
+ expect(queryPlan).toMatchInlineSnapshot(`
1484
+ QueryPlan {
1485
+ Sequence {
1486
+ Fetch(service: "S1") {
1487
+ {
1488
+ mutateSomething {
1489
+ mutateSomething {
1490
+ __typename
1491
+ }
1492
+ }
1493
+ }
1494
+ },
1495
+ Flatten(path: "mutateSomething.mutateSomething") {
1496
+ Fetch(service: "S2") {
1497
+ {
1498
+ ... on Mutation {
1499
+ mutateTwo
1500
+ }
1501
+ }
1502
+ },
1503
+ },
1504
+ },
1505
+ }
1506
+ `);
1507
+
1508
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
1509
+
1510
+ expect(somethingMutationCount).toBe(2);
1511
+ expect(hasMutatedTwo).toBeTruthy();
1512
+ expect(response.data).toMatchInlineSnapshot(`
1513
+ Object {
1514
+ "mutateSomething": Object {
1515
+ "mutateSomething": Object {
1516
+ "mutateTwo": 2,
1517
+ },
1518
+ },
1519
+ }
1520
+ `);
1521
+ })
1522
+ });
1523
+
1524
+ describe('interfaces on interfaces', () => {
1525
+ it('can execute queries on an interface only implemented by other interfaces', async () => {
1526
+ const s1 = gql`
1527
+ type Query {
1528
+ allValues: [TopInterface!]!
1529
+ }
1530
+
1531
+ interface TopInterface {
1532
+ a: Int
1533
+ }
1534
+
1535
+ interface SubInterface1 implements TopInterface {
1536
+ a: Int
1537
+ b: String
1538
+ }
1539
+
1540
+ interface SubInterface2 implements TopInterface {
1541
+ a: Int
1542
+ c: String
1543
+ }
1544
+
1545
+ type T1 implements SubInterface1 & TopInterface {
1546
+ a: Int
1547
+ b: String
1548
+ }
1549
+
1550
+ type T2 implements SubInterface1 & TopInterface @key(fields: "b") {
1551
+ a: Int @external
1552
+ b: String
1553
+ }
1554
+
1555
+ type T3 implements SubInterface2 & TopInterface {
1556
+ a: Int
1557
+ c: String
1558
+ }
1559
+
1560
+ type T4 implements SubInterface1 & SubInterface2 & TopInterface @key(fields: "a") {
1561
+ a: Int
1562
+ b: String @external
1563
+ c: String @external
1564
+ }
1565
+ `;
1566
+
1567
+ const s2 = gql`
1568
+ type T2 @key(fields: "b") {
1569
+ a: Int
1570
+ b: String
1571
+ }
1572
+
1573
+ type T4 @key(fields: "a") {
1574
+ a: Int
1575
+ b: String
1576
+ c: String
1577
+ }
1578
+ `;
1579
+
1580
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([
1581
+ { name: 'S1', typeDefs: s1 },
1582
+ { name: 'S2', typeDefs: s2 }
1583
+ ]);
1584
+
1585
+ const t1s_s1: any[] = [{ __typename: 'T1', a: 1, b: 'T1_v1'}, {__typename: 'T1', a: 2, b: 'T1_v2'}];
1586
+ const t2s_s1: any[] = [{__typename: 'T2', b: 'k1'}, {__typename: 'T2', b: 'k2'}];
1587
+ const t3s_s1: any[] = [{__typename: 'T3', a: 42, c: 'T3_v1'}];
1588
+ const t4s_s1: any[] = [{__typename: 'T4', a: 0}, {__typename: 'T4', a: 10}, {__typename: 'T4', a: 20}];
1589
+
1590
+ const t2s_s2 = new Map<string, {a: number, b: string}>();
1591
+ t2s_s2.set('k1', {a: 12 , b: 'k1'});
1592
+ t2s_s2.set('k2', {a: 24 , b: 'k2'});
1593
+
1594
+ const t4s_s2 = new Map<number, {a: number, b: string, c: string}>();
1595
+ t4s_s2.set(0, {a: 0, b: 'b_0', c: 'c_0'});
1596
+ t4s_s2.set(10, {a: 10, b: 'b_10', c: 'c_10'});
1597
+ t4s_s2.set(20, {a: 20, b: 'b_20', c: 'c_20'});
1598
+
1599
+ addResolversToSchema(serviceMap['S1'].schema, {
1600
+ Query: {
1601
+ allValues() {
1602
+ return t1s_s1.concat(t2s_s1).concat(t3s_s1).concat(t4s_s1);
1603
+ },
1604
+ },
1605
+ });
1606
+
1607
+ addResolversToSchema(serviceMap['S2'].schema, {
1608
+ T2: {
1609
+ __resolveReference(ref) {
1610
+ return t2s_s2.get(ref.b);
1611
+ }
1612
+ },
1613
+ T4: {
1614
+ __resolveReference(ref) {
1615
+ return t4s_s2.get(ref.a);
1616
+ }
1617
+ },
1618
+ });
1619
+
1620
+ const operation = parseOp(`
1621
+ query {
1622
+ allValues {
1623
+ a
1624
+ ... on SubInterface1 {
1625
+ b
1626
+ }
1627
+ ... on SubInterface2 {
1628
+ c
1629
+ }
1630
+ }
1631
+ }
1632
+ `, schema);
1633
+
1634
+ const queryPlan = buildPlan(operation, queryPlanner);
1635
+
1636
+ expect(queryPlan).toMatchInlineSnapshot(`
1637
+ QueryPlan {
1638
+ Sequence {
1639
+ Fetch(service: "S1") {
1640
+ {
1641
+ allValues {
1642
+ __typename
1643
+ ... on T1 {
1644
+ a
1645
+ b
1646
+ }
1647
+ ... on T2 {
1648
+ __typename
1649
+ b
1650
+ }
1651
+ ... on T3 {
1652
+ a
1653
+ c
1654
+ }
1655
+ ... on T4 {
1656
+ __typename
1657
+ a
1658
+ }
1659
+ }
1660
+ }
1661
+ },
1662
+ Flatten(path: "allValues.@") {
1663
+ Fetch(service: "S2") {
1664
+ {
1665
+ ... on T2 {
1666
+ __typename
1667
+ b
1668
+ }
1669
+ ... on T4 {
1670
+ __typename
1671
+ a
1672
+ }
1673
+ } =>
1674
+ {
1675
+ ... on T2 {
1676
+ a
1677
+ }
1678
+ ... on T4 {
1679
+ b
1680
+ c
1681
+ }
1682
+ }
1683
+ },
1684
+ },
1685
+ },
1686
+ }
1687
+ `);
1688
+
1689
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
1690
+
1691
+ expect(response.data).toMatchInlineSnapshot(`
1692
+ Object {
1693
+ "allValues": Array [
1694
+ Object {
1695
+ "a": 1,
1696
+ "b": "T1_v1",
1697
+ },
1698
+ Object {
1699
+ "a": 2,
1700
+ "b": "T1_v2",
1701
+ },
1702
+ Object {
1703
+ "a": 12,
1704
+ "b": "k1",
1705
+ },
1706
+ Object {
1707
+ "a": 24,
1708
+ "b": "k2",
1709
+ },
1710
+ Object {
1711
+ "a": 42,
1712
+ "c": "T3_v1",
1713
+ },
1714
+ Object {
1715
+ "a": 0,
1716
+ "b": "b_0",
1717
+ "c": "c_0",
1718
+ },
1719
+ Object {
1720
+ "a": 10,
1721
+ "b": "b_10",
1722
+ "c": "c_10",
1723
+ },
1724
+ Object {
1725
+ "a": 20,
1726
+ "b": "b_20",
1727
+ "c": "c_20",
1728
+ },
1729
+ ],
1730
+ }
1731
+ `);
1732
+ });
1733
+
1734
+ it('does not type explode when it does not need to', async () => {
1735
+ // Fairly similar example than the previous one, but ensure field `a` don't need
1736
+ // type explosion and unsure it isn't type-exploded.
1737
+ const s1 = gql`
1738
+ type Query {
1739
+ allValues: [TopInterface!]!
1740
+ }
1741
+
1742
+ interface TopInterface {
1743
+ a: Int
1744
+ }
1745
+
1746
+ interface SubInterface1 implements TopInterface {
1747
+ a: Int
1748
+ b: String
1749
+ }
1750
+
1751
+ interface SubInterface2 implements TopInterface {
1752
+ a: Int
1753
+ c: String
1754
+ }
1755
+
1756
+ type T1 implements SubInterface1 & TopInterface {
1757
+ a: Int
1758
+ b: String
1759
+ }
1760
+
1761
+ type T2 implements SubInterface1 & TopInterface @key(fields: "a") {
1762
+ a: Int
1763
+ b: String @external
1764
+ }
1765
+
1766
+ type T3 implements SubInterface2 & TopInterface {
1767
+ a: Int
1768
+ c: String
1769
+ }
1770
+
1771
+ type T4 implements SubInterface1 & SubInterface2 & TopInterface @key(fields: "a") {
1772
+ a: Int
1773
+ b: String @external
1774
+ c: String @external
1775
+ }
1776
+ `;
1777
+
1778
+ const s2 = gql`
1779
+ type T2 @key(fields: "a") {
1780
+ a: Int
1781
+ b: String
1782
+ }
1783
+
1784
+ type T4 @key(fields: "a") {
1785
+ a: Int
1786
+ b: String
1787
+ c: String
1788
+ }
1789
+ `;
1790
+
1791
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([
1792
+ { name: 'S1', typeDefs: s1 },
1793
+ { name: 'S2', typeDefs: s2 }
1794
+ ]);
1795
+
1796
+ const t1s_s1: any[] = [{ __typename: 'T1', a: 1, b: 'T1_v1'}, {__typename: 'T1', a: 2, b: 'T1_v2'}];
1797
+ const t2s_s1: any[] = [{__typename: 'T2', a: 12}, {__typename: 'T2', a: 24}];
1798
+ const t3s_s1: any[] = [{__typename: 'T3', a: 42, c: 'T3_v1'}];
1799
+ const t4s_s1: any[] = [{__typename: 'T4', a: 0}, {__typename: 'T4', a: 10}, {__typename: 'T4', a: 20}];
1800
+
1801
+ const t2s_s2 = new Map<number, {a: number, b: string}>();
1802
+ t2s_s2.set(12, {a: 12 , b: 'k1'});
1803
+ t2s_s2.set(24, {a: 24 , b: 'k2'});
1804
+
1805
+ const t4s_s2 = new Map<number, {a: number, b: string, c: string}>();
1806
+ t4s_s2.set(0, {a: 0, b: 'b_0', c: 'c_0'});
1807
+ t4s_s2.set(10, {a: 10, b: 'b_10', c: 'c_10'});
1808
+ t4s_s2.set(20, {a: 20, b: 'b_20', c: 'c_20'});
1809
+
1810
+ addResolversToSchema(serviceMap['S1'].schema, {
1811
+ Query: {
1812
+ allValues() {
1813
+ return t1s_s1.concat(t2s_s1).concat(t3s_s1).concat(t4s_s1);
1814
+ },
1815
+ },
1816
+ });
1817
+
1818
+ addResolversToSchema(serviceMap['S2'].schema, {
1819
+ T2: {
1820
+ __resolveReference(ref) {
1821
+ return t2s_s2.get(ref.b);
1822
+ }
1823
+ },
1824
+ T4: {
1825
+ __resolveReference(ref) {
1826
+ return t4s_s2.get(ref.a);
1827
+ }
1828
+ },
1829
+ });
1830
+
1831
+ let operation = parseOp(`
1832
+ query {
1833
+ allValues {
1834
+ a
1835
+ }
1836
+ }
1837
+ `, schema);
1838
+
1839
+ let queryPlan = buildPlan(operation, queryPlanner);
1840
+
1841
+ expect(queryPlan).toMatchInlineSnapshot(`
1842
+ QueryPlan {
1843
+ Fetch(service: "S1") {
1844
+ {
1845
+ allValues {
1846
+ __typename
1847
+ a
1848
+ }
1849
+ }
1850
+ },
1851
+ }
1852
+ `);
1853
+
1854
+ let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
1855
+ expect(response.data).toMatchInlineSnapshot(`
1856
+ Object {
1857
+ "allValues": Array [
1858
+ Object {
1859
+ "a": 1,
1860
+ },
1861
+ Object {
1862
+ "a": 2,
1863
+ },
1864
+ Object {
1865
+ "a": 12,
1866
+ },
1867
+ Object {
1868
+ "a": 24,
1869
+ },
1870
+ Object {
1871
+ "a": 42,
1872
+ },
1873
+ Object {
1874
+ "a": 0,
1875
+ },
1876
+ Object {
1877
+ "a": 10,
1878
+ },
1879
+ Object {
1880
+ "a": 20,
1881
+ },
1882
+ ],
1883
+ }
1884
+ `);
1885
+
1886
+ operation = parseOp(`
1887
+ query {
1888
+ allValues {
1889
+ ... on SubInterface1 {
1890
+ a
1891
+ }
1892
+ }
1893
+ }
1894
+ `, schema);
1895
+
1896
+ queryPlan = buildPlan(operation, queryPlanner);
1897
+
1898
+ // TODO: we're actually type-exploding in this case because currently, as soon as we need to type-explode, we do
1899
+ // so into all the runtime types, while here it could make sense to only type-explode into the direct sub-types=
1900
+ // (the sub-interfaces). We should fix this (but it's only sub-optimal, not incorrect).
1901
+ expect(queryPlan).toMatchInlineSnapshot(`
1902
+ QueryPlan {
1903
+ Fetch(service: "S1") {
1904
+ {
1905
+ allValues {
1906
+ __typename
1907
+ ... on T1 {
1908
+ a
1909
+ }
1910
+ ... on T2 {
1911
+ a
1912
+ }
1913
+ ... on T4 {
1914
+ a
1915
+ }
1916
+ }
1917
+ }
1918
+ },
1919
+ }
1920
+ `);
1921
+
1922
+ response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
1923
+ expect(response.data).toMatchInlineSnapshot(`
1924
+ Object {
1925
+ "allValues": Array [
1926
+ Object {
1927
+ "a": 1,
1928
+ },
1929
+ Object {
1930
+ "a": 2,
1931
+ },
1932
+ Object {
1933
+ "a": 12,
1934
+ },
1935
+ Object {
1936
+ "a": 24,
1937
+ },
1938
+ Object {},
1939
+ Object {
1940
+ "a": 0,
1941
+ },
1942
+ Object {
1943
+ "a": 10,
1944
+ },
1945
+ Object {
1946
+ "a": 20,
1947
+ },
1948
+ ],
1949
+ }
1950
+ `);
1951
+ });
1952
+ });
1953
+
1954
+ test('do not send subgraphs an interface they do not know', async () => {
1955
+ // This test validates that the issue described on https://github.com/apollographql/federation/issues/817 is fixed.
1956
+ const s1 = {
1957
+ name: 'S1',
1958
+ typeDefs: gql`
1959
+ type Query {
1960
+ myField: MyInterface
1961
+ }
1962
+
1963
+ interface MyInterface {
1964
+ name: String
1965
+ }
1966
+
1967
+ type MyTypeA implements MyInterface {
1968
+ name: String
1969
+ }
1970
+
1971
+ type MyTypeB implements MyInterface {
1972
+ name: String
1973
+ }
1974
+ `
1975
+ }
1976
+
1977
+ const s2 = {
1978
+ name: 'S2',
1979
+ typeDefs: gql`
1980
+ interface MyInterface {
1981
+ name: String
1982
+ }
1983
+
1984
+ type MyTypeC implements MyInterface {
1985
+ name: String
1986
+ }
1987
+ `
1988
+ }
1989
+
1990
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
1991
+
1992
+ addResolversToSchema(serviceMap['S1'].schema, {
1993
+ Query: {
1994
+ myField() {
1995
+ return { __typename: 'MyTypeA', name: "foo" };
1996
+ },
1997
+ },
1998
+ });
1999
+
2000
+ // First, we just query the field without conditions.
2001
+ // Note that there is no reason to type-explode this: clearly, `myField` will never return a `MyTypeC` since
2002
+ // it's resolved by S1 which doesn't know that type, but that doesn't impact the plan.
2003
+ let operation = parseOp(`
2004
+ query {
2005
+ myField {
2006
+ name
2007
+ }
2008
+ }
2009
+ `, schema);
2010
+ let queryPlan = buildPlan(operation, queryPlanner);
2011
+ expect(queryPlan).toMatchInlineSnapshot(`
2012
+ QueryPlan {
2013
+ Fetch(service: "S1") {
2014
+ {
2015
+ myField {
2016
+ __typename
2017
+ name
2018
+ }
2019
+ }
2020
+ },
2021
+ }
2022
+ `);
2023
+ let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2024
+ expect(response.data).toMatchInlineSnapshot(`
2025
+ Object {
2026
+ "myField": Object {
2027
+ "name": "foo",
2028
+ },
2029
+ }
2030
+ `);
2031
+
2032
+ // Now forcing the query planning to notice that `MyTypeC` can never happen and making
2033
+ // sure it doesn't ask it from S1, which doesn't know it.
2034
+ operation = parseOp(`
2035
+ query {
2036
+ myField {
2037
+ ... on MyTypeA {
2038
+ name
2039
+ }
2040
+ ... on MyTypeC {
2041
+ name
2042
+ }
2043
+ }
2044
+ }
2045
+ `, schema);
2046
+ queryPlan = buildPlan(operation, queryPlanner);
2047
+ expect(queryPlan).toMatchInlineSnapshot(`
2048
+ QueryPlan {
2049
+ Fetch(service: "S1") {
2050
+ {
2051
+ myField {
2052
+ __typename
2053
+ ... on MyTypeA {
2054
+ name
2055
+ }
2056
+ }
2057
+ }
2058
+ },
2059
+ }
2060
+ `);
2061
+
2062
+ response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2063
+ expect(response.data).toMatchInlineSnapshot(`
2064
+ Object {
2065
+ "myField": Object {
2066
+ "name": "foo",
2067
+ },
2068
+ }
2069
+ `);
2070
+
2071
+
2072
+ // Testing only getting name for `MyTypeB`, which is known by S1, but not returned
2073
+ // by `myField` in practice (so the result is "empty").
2074
+ operation = parseOp(`
2075
+ query {
2076
+ myField {
2077
+ ... on MyTypeB {
2078
+ name
2079
+ }
2080
+ }
2081
+ }
2082
+ `, schema);
2083
+
2084
+ queryPlan = buildPlan(operation, queryPlanner);
2085
+ expect(queryPlan).toMatchInlineSnapshot(`
2086
+ QueryPlan {
2087
+ Fetch(service: "S1") {
2088
+ {
2089
+ myField {
2090
+ __typename
2091
+ ... on MyTypeB {
2092
+ name
2093
+ }
2094
+ }
2095
+ }
2096
+ },
2097
+ }
2098
+ `);
2099
+ response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2100
+ expect(response.data).toMatchInlineSnapshot(`
2101
+ Object {
2102
+ "myField": Object {},
2103
+ }
2104
+ `);
2105
+
2106
+ operation = parseOp(`
2107
+ query {
2108
+ myField {
2109
+ ... on MyTypeC {
2110
+ name
2111
+ }
2112
+ }
2113
+ }
2114
+ `, schema);
2115
+
2116
+ // Lastly, same with only getting name for `MyTypeC`. It isn't known by S1 so the condition should not
2117
+ // be included in the query, but we should still query `myField` to know if it resolve to "something"
2118
+ // (and all we know it can't be a `MyTypeC`) or to `null`. In particular, the end response should be
2119
+ // the same than in the previous example with `MyTypeB` since from the end-use POV, this is the same
2120
+ // example.
2121
+ queryPlan = buildPlan(operation, queryPlanner);
2122
+ expect(queryPlan).toMatchInlineSnapshot(`
2123
+ QueryPlan {
2124
+ Fetch(service: "S1") {
2125
+ {
2126
+ myField {
2127
+ __typename
2128
+ }
2129
+ }
2130
+ },
2131
+ }
2132
+ `);
2133
+
2134
+ response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2135
+ expect(response.data).toMatchInlineSnapshot(`
2136
+ Object {
2137
+ "myField": Object {},
2138
+ }
2139
+ `);
2140
+ });
2141
+
2142
+ describe('@requires', () => {
2143
+ test('handles null in required field correctly (with nullable fields)', async () => {
2144
+ const s1_data = [
2145
+ { id: 0, f1: "foo" },
2146
+ { id: 1, f1: null },
2147
+ { id: 2, f1: "bar" },
2148
+ ];
2149
+
2150
+ const s1 = {
2151
+ name: 'S1',
2152
+ typeDefs: gql`
2153
+ type T1 @key(fields: "id") {
2154
+ id: Int!
2155
+ f1: String
2156
+ }
2157
+ `,
2158
+ resolvers: {
2159
+ T1: {
2160
+ __resolveReference(ref: {id: number}) {
2161
+ return s1_data[ref.id];
2162
+ },
2163
+ },
2164
+ }
2165
+ }
2166
+
2167
+ const s2 = {
2168
+ name: 'S2',
2169
+ typeDefs: gql`
2170
+ type Query {
2171
+ getT1s: [T1]
2172
+ }
2173
+
2174
+ type T1 @key(fields: "id") {
2175
+ id: Int!
2176
+ f1: String @external
2177
+ f2: T2 @requires(fields: "f1")
2178
+ }
2179
+
2180
+ type T2 {
2181
+ a: String
2182
+ }
2183
+ `,
2184
+ resolvers: {
2185
+ Query: {
2186
+ getT1s() {
2187
+ return [{id: 0}, {id: 1}, {id: 2}];
2188
+ },
2189
+ },
2190
+ T1: {
2191
+ __resolveReference(ref: { id: number }) {
2192
+ // the ref has already the id and f1 is a require is triggered, and we resolve f2 below
2193
+ return ref;
2194
+ },
2195
+ f2(o: { f1: string }) {
2196
+ return o.f1 === null ? null : { a: `t1:${o.f1}` };
2197
+ }
2198
+ }
2199
+ }
2200
+ }
2201
+
2202
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
2203
+
2204
+ const operation = parseOp(`
2205
+ query {
2206
+ getT1s {
2207
+ id
2208
+ f1
2209
+ f2 {
2210
+ a
2211
+ }
2212
+ }
2213
+ }
2214
+ `, schema);
2215
+ const queryPlan = buildPlan(operation, queryPlanner);
2216
+ expect(queryPlan).toMatchInlineSnapshot(`
2217
+ QueryPlan {
2218
+ Sequence {
2219
+ Fetch(service: "S2") {
2220
+ {
2221
+ getT1s {
2222
+ __typename
2223
+ id
2224
+ }
2225
+ }
2226
+ },
2227
+ Flatten(path: "getT1s.@") {
2228
+ Fetch(service: "S1") {
2229
+ {
2230
+ ... on T1 {
2231
+ __typename
2232
+ id
2233
+ }
2234
+ } =>
2235
+ {
2236
+ ... on T1 {
2237
+ f1
2238
+ }
2239
+ }
2240
+ },
2241
+ },
2242
+ Flatten(path: "getT1s.@") {
2243
+ Fetch(service: "S2") {
2244
+ {
2245
+ ... on T1 {
2246
+ __typename
2247
+ f1
2248
+ id
2249
+ }
2250
+ } =>
2251
+ {
2252
+ ... on T1 {
2253
+ f2 {
2254
+ a
2255
+ }
2256
+ }
2257
+ }
2258
+ },
2259
+ },
2260
+ },
2261
+ }
2262
+ `);
2263
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2264
+ expect(response.data).toMatchInlineSnapshot(`
2265
+ Object {
2266
+ "getT1s": Array [
2267
+ Object {
2268
+ "f1": "foo",
2269
+ "f2": Object {
2270
+ "a": "t1:foo",
2271
+ },
2272
+ "id": 0,
2273
+ },
2274
+ Object {
2275
+ "f1": null,
2276
+ "f2": null,
2277
+ "id": 1,
2278
+ },
2279
+ Object {
2280
+ "f1": "bar",
2281
+ "f2": Object {
2282
+ "a": "t1:bar",
2283
+ },
2284
+ "id": 2,
2285
+ },
2286
+ ],
2287
+ }
2288
+ `);
2289
+ expect(response.errors).toBeUndefined();
2290
+ });
2291
+
2292
+ test('handles null in required field correctly (with @require field non-nullable)', async () => {
2293
+ const s1_data = [
2294
+ { id: 0, f1: "foo" },
2295
+ { id: 1, f1: null },
2296
+ { id: 2, f1: "bar" },
2297
+ ];
2298
+
2299
+ const s1 = {
2300
+ name: 'S1',
2301
+ typeDefs: gql`
2302
+ type T1 @key(fields: "id") {
2303
+ id: Int!
2304
+ f1: String
2305
+ }
2306
+ `,
2307
+ resolvers: {
2308
+ T1: {
2309
+ __resolveReference(ref: { id: number }) {
2310
+ return s1_data[ref.id];
2311
+ },
2312
+ },
2313
+ }
2314
+ }
2315
+
2316
+ const s2 = {
2317
+ name: 'S2',
2318
+ typeDefs: gql`
2319
+ type Query {
2320
+ getT1s: [T1]
2321
+ }
2322
+
2323
+ type T1 @key(fields: "id") {
2324
+ id: Int!
2325
+ f1: String @external
2326
+ f2: T2! @requires(fields: "f1")
2327
+ }
2328
+
2329
+ type T2 {
2330
+ a: String
2331
+ }
2332
+ `,
2333
+ resolvers: {
2334
+ Query: {
2335
+ getT1s() {
2336
+ return [{id: 0}, {id: 1}, {id: 2}];
2337
+ },
2338
+ },
2339
+ T1: {
2340
+ __resolveReference(ref: { id: number }) {
2341
+ // the ref has already the id and f1 is a require is triggered, and we resolve f2 below
2342
+ return ref;
2343
+ },
2344
+ f2(o: { f1: string }) {
2345
+ return o.f1 === null ? null : { a: `t1:${o.f1}` };
2346
+ }
2347
+ }
2348
+ }
2349
+ }
2350
+
2351
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
2352
+
2353
+ const operation = parseOp(`
2354
+ query {
2355
+ getT1s {
2356
+ id
2357
+ f1
2358
+ f2 {
2359
+ a
2360
+ }
2361
+ }
2362
+ }
2363
+ `, schema);
2364
+ const queryPlan = buildPlan(operation, queryPlanner);
2365
+ expect(queryPlan).toMatchInlineSnapshot(`
2366
+ QueryPlan {
2367
+ Sequence {
2368
+ Fetch(service: "S2") {
2369
+ {
2370
+ getT1s {
2371
+ __typename
2372
+ id
2373
+ }
2374
+ }
2375
+ },
2376
+ Flatten(path: "getT1s.@") {
2377
+ Fetch(service: "S1") {
2378
+ {
2379
+ ... on T1 {
2380
+ __typename
2381
+ id
2382
+ }
2383
+ } =>
2384
+ {
2385
+ ... on T1 {
2386
+ f1
2387
+ }
2388
+ }
2389
+ },
2390
+ },
2391
+ Flatten(path: "getT1s.@") {
2392
+ Fetch(service: "S2") {
2393
+ {
2394
+ ... on T1 {
2395
+ __typename
2396
+ f1
2397
+ id
2398
+ }
2399
+ } =>
2400
+ {
2401
+ ... on T1 {
2402
+ f2 {
2403
+ a
2404
+ }
2405
+ }
2406
+ }
2407
+ },
2408
+ },
2409
+ },
2410
+ }
2411
+ `);
2412
+
2413
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2414
+ // `null` should bubble up since `f2` is now non-nullable. But we should still get the `id: 0` response.
2415
+ expect(response.data).toMatchInlineSnapshot(`
2416
+ Object {
2417
+ "getT1s": Array [
2418
+ Object {
2419
+ "f1": "foo",
2420
+ "f2": Object {
2421
+ "a": "t1:foo",
2422
+ },
2423
+ "id": 0,
2424
+ },
2425
+ null,
2426
+ Object {
2427
+ "f1": "bar",
2428
+ "f2": Object {
2429
+ "a": "t1:bar",
2430
+ },
2431
+ "id": 2,
2432
+ },
2433
+ ],
2434
+ }
2435
+ `);
2436
+
2437
+ // We returning `null` for f2 which isn't nullable, so it bubbled up and we should have an error
2438
+ expect(response.errors?.map((e) => e.message)).toStrictEqual(['Cannot return null for non-nullable field T1.f2.']);
2439
+ });
2440
+
2441
+ test('handles null in required field correctly (with non-nullable required field)', async () => {
2442
+ const s1 = {
2443
+ name: 'S1',
2444
+ typeDefs: gql`
2445
+ type T1 @key(fields: "id") {
2446
+ id: Int!
2447
+ f1: String!
2448
+ }
2449
+ `,
2450
+ resolvers: {
2451
+ T1: {
2452
+ __resolveReference(ref: { id: number}) {
2453
+ return s1_data[ref.id];
2454
+ },
2455
+ },
2456
+ }
2457
+ }
2458
+
2459
+ const s2 = {
2460
+ name: 'S2',
2461
+ typeDefs: gql`
2462
+ type Query {
2463
+ getT1s: [T1]
2464
+ }
2465
+
2466
+ type T1 @key(fields: "id") {
2467
+ id: Int!
2468
+ f1: String! @external
2469
+ f2: T2 @requires(fields: "f1")
2470
+ }
2471
+
2472
+ type T2 {
2473
+ a: String
2474
+ }
2475
+ `,
2476
+ resolvers: {
2477
+ Query: {
2478
+ getT1s() {
2479
+ return [{id: 0}, {id: 1}, {id: 2}];
2480
+ },
2481
+ },
2482
+ T1: {
2483
+ __resolveReference(ref: { id: number }) {
2484
+ // the ref has already the id and f1 is a require is triggered, and we resolve f2 below
2485
+ return ref;
2486
+ },
2487
+ f2(o: { f1: string }) {
2488
+ return o.f1 === null ? null : { a: `t1:${o.f1}` };
2489
+ }
2490
+ }
2491
+ }
2492
+ }
2493
+
2494
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
2495
+
2496
+ const s1_data = [
2497
+ { id: 0, f1: "foo" },
2498
+ { id: 1, f1: null },
2499
+ { id: 2, f1: "bar" },
2500
+ ];
2501
+
2502
+ const operation = parseOp(`
2503
+ query {
2504
+ getT1s {
2505
+ id
2506
+ f1
2507
+ f2 {
2508
+ a
2509
+ }
2510
+ }
2511
+ }
2512
+ `, schema);
2513
+ const queryPlan = buildPlan(operation, queryPlanner);
2514
+ expect(queryPlan).toMatchInlineSnapshot(`
2515
+ QueryPlan {
2516
+ Sequence {
2517
+ Fetch(service: "S2") {
2518
+ {
2519
+ getT1s {
2520
+ __typename
2521
+ id
2522
+ }
2523
+ }
2524
+ },
2525
+ Flatten(path: "getT1s.@") {
2526
+ Fetch(service: "S1") {
2527
+ {
2528
+ ... on T1 {
2529
+ __typename
2530
+ id
2531
+ }
2532
+ } =>
2533
+ {
2534
+ ... on T1 {
2535
+ f1
2536
+ }
2537
+ }
2538
+ },
2539
+ },
2540
+ Flatten(path: "getT1s.@") {
2541
+ Fetch(service: "S2") {
2542
+ {
2543
+ ... on T1 {
2544
+ __typename
2545
+ f1
2546
+ id
2547
+ }
2548
+ } =>
2549
+ {
2550
+ ... on T1 {
2551
+ f2 {
2552
+ a
2553
+ }
2554
+ }
2555
+ }
2556
+ },
2557
+ },
2558
+ },
2559
+ }
2560
+ `);
2561
+
2562
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2563
+ // `null` should bubble up since `f2` is now non-nullable. But we should still get the `id: 0` response.
2564
+ expect(response.data).toMatchInlineSnapshot(`
2565
+ Object {
2566
+ "getT1s": Array [
2567
+ Object {
2568
+ "f1": "foo",
2569
+ "f2": Object {
2570
+ "a": "t1:foo",
2571
+ },
2572
+ "id": 0,
2573
+ },
2574
+ null,
2575
+ Object {
2576
+ "f1": "bar",
2577
+ "f2": Object {
2578
+ "a": "t1:bar",
2579
+ },
2580
+ "id": 2,
2581
+ },
2582
+ ],
2583
+ }
2584
+ `);
2585
+ expect(response.errors?.map((e) => e.message)).toStrictEqual(['Cannot return null for non-nullable field T1.f1.']);
2586
+ });
2587
+
2588
+ test('handles errors in required field correctly (with nullable fields)', async () => {
2589
+ const s1 = {
2590
+ name: 'S1',
2591
+ typeDefs: gql`
2592
+ type T1 @key(fields: "id") {
2593
+ id: Int!
2594
+ f1: String
2595
+ }
2596
+ `,
2597
+ resolvers: {
2598
+ T1: {
2599
+ __resolveReference(ref: { id: number }) {
2600
+ return ref;
2601
+ },
2602
+ f1(o: { id: number }) {
2603
+ switch (o.id) {
2604
+ case 0: return "foo";
2605
+ case 1: return [ "invalid" ]; // This will effectively throw
2606
+ case 2: return "bar";
2607
+ default: throw new Error('Not handled');
2608
+ }
2609
+ }
2610
+ },
2611
+ }
2612
+ }
2613
+
2614
+ const s2 = {
2615
+ name: 'S2',
2616
+ typeDefs: gql`
2617
+ type Query {
2618
+ getT1s: [T1]
2619
+ }
2620
+
2621
+ type T1 @key(fields: "id") {
2622
+ id: Int!
2623
+ f1: String @external
2624
+ f2: T2 @requires(fields: "f1")
2625
+ }
2626
+
2627
+ type T2 {
2628
+ a: String
2629
+ }
2630
+ `,
2631
+ resolvers: {
2632
+ Query: {
2633
+ getT1s() {
2634
+ return [{id: 0}, {id: 1}, {id: 2}];
2635
+ },
2636
+ },
2637
+ T1: {
2638
+ __resolveReference(ref: { id: number }) {
2639
+ // the ref has already the id and f1 is a require is triggered, and we resolve f2 below
2640
+ return ref;
2641
+ },
2642
+ f2(o: { f1: string }) {
2643
+ return o.f1 === null ? null : { a: `t1:${o.f1}` };
2644
+ }
2645
+ }
2646
+ }
2647
+ }
2648
+
2649
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
2650
+
2651
+ const operation = parseOp(`
2652
+ query {
2653
+ getT1s {
2654
+ id
2655
+ f1
2656
+ f2 {
2657
+ a
2658
+ }
2659
+ }
2660
+ }
2661
+ `, schema);
2662
+ const queryPlan = buildPlan(operation, queryPlanner);
2663
+ expect(queryPlan).toMatchInlineSnapshot(`
2664
+ QueryPlan {
2665
+ Sequence {
2666
+ Fetch(service: "S2") {
2667
+ {
2668
+ getT1s {
2669
+ __typename
2670
+ id
2671
+ }
2672
+ }
2673
+ },
2674
+ Flatten(path: "getT1s.@") {
2675
+ Fetch(service: "S1") {
2676
+ {
2677
+ ... on T1 {
2678
+ __typename
2679
+ id
2680
+ }
2681
+ } =>
2682
+ {
2683
+ ... on T1 {
2684
+ f1
2685
+ }
2686
+ }
2687
+ },
2688
+ },
2689
+ Flatten(path: "getT1s.@") {
2690
+ Fetch(service: "S2") {
2691
+ {
2692
+ ... on T1 {
2693
+ __typename
2694
+ f1
2695
+ id
2696
+ }
2697
+ } =>
2698
+ {
2699
+ ... on T1 {
2700
+ f2 {
2701
+ a
2702
+ }
2703
+ }
2704
+ }
2705
+ },
2706
+ },
2707
+ },
2708
+ }
2709
+ `);
2710
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2711
+ expect(response.data).toMatchInlineSnapshot(`
2712
+ Object {
2713
+ "getT1s": Array [
2714
+ Object {
2715
+ "f1": "foo",
2716
+ "f2": Object {
2717
+ "a": "t1:foo",
2718
+ },
2719
+ "id": 0,
2720
+ },
2721
+ Object {
2722
+ "f1": null,
2723
+ "f2": null,
2724
+ "id": 1,
2725
+ },
2726
+ Object {
2727
+ "f1": "bar",
2728
+ "f2": Object {
2729
+ "a": "t1:bar",
2730
+ },
2731
+ "id": 2,
2732
+ },
2733
+ ],
2734
+ }
2735
+ `);
2736
+ expect(response.errors?.map((e) => e.message)).toStrictEqual(['String cannot represent value: ["invalid"]']);
2737
+ });
2738
+ });
644
2739
  });