@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.
- package/LICENSE +95 -0
- package/README.md +1 -1
- package/dist/__generated__/graphqlTypes.d.ts +130 -0
- package/dist/__generated__/graphqlTypes.d.ts.map +1 -0
- package/dist/__generated__/graphqlTypes.js +25 -0
- package/dist/__generated__/graphqlTypes.js.map +1 -0
- package/dist/config.d.ts +106 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +47 -0
- package/dist/config.js.map +1 -0
- package/dist/datasources/LocalGraphQLDataSource.d.ts +3 -3
- package/dist/datasources/LocalGraphQLDataSource.d.ts.map +1 -1
- package/dist/datasources/LocalGraphQLDataSource.js +5 -5
- package/dist/datasources/LocalGraphQLDataSource.js.map +1 -1
- package/dist/datasources/RemoteGraphQLDataSource.d.ts +6 -4
- package/dist/datasources/RemoteGraphQLDataSource.d.ts.map +1 -1
- package/dist/datasources/RemoteGraphQLDataSource.js +64 -18
- package/dist/datasources/RemoteGraphQLDataSource.js.map +1 -1
- package/dist/datasources/index.d.ts +1 -1
- package/dist/datasources/index.d.ts.map +1 -1
- package/dist/datasources/index.js +1 -0
- package/dist/datasources/index.js.map +1 -1
- package/dist/datasources/parseCacheControlHeader.d.ts +2 -0
- package/dist/datasources/parseCacheControlHeader.d.ts.map +1 -0
- package/dist/datasources/parseCacheControlHeader.js +16 -0
- package/dist/datasources/parseCacheControlHeader.js.map +1 -0
- package/dist/datasources/types.d.ts +16 -1
- package/dist/datasources/types.d.ts.map +1 -1
- package/dist/datasources/types.js +7 -0
- package/dist/datasources/types.js.map +1 -1
- package/dist/executeQueryPlan.d.ts +2 -1
- package/dist/executeQueryPlan.d.ts.map +1 -1
- package/dist/executeQueryPlan.js +200 -113
- package/dist/executeQueryPlan.js.map +1 -1
- package/dist/index.d.ts +64 -80
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +554 -233
- package/dist/index.js.map +1 -1
- package/dist/loadServicesFromRemoteEndpoint.d.ts +9 -9
- package/dist/loadServicesFromRemoteEndpoint.d.ts.map +1 -1
- package/dist/loadServicesFromRemoteEndpoint.js +13 -8
- package/dist/loadServicesFromRemoteEndpoint.js.map +1 -1
- package/dist/loadSupergraphSdlFromStorage.d.ts +21 -0
- package/dist/loadSupergraphSdlFromStorage.d.ts.map +1 -0
- package/dist/loadSupergraphSdlFromStorage.js +128 -0
- package/dist/loadSupergraphSdlFromStorage.js.map +1 -0
- package/dist/operationContext.d.ts +17 -0
- package/dist/operationContext.d.ts.map +1 -0
- package/dist/operationContext.js +42 -0
- package/dist/operationContext.js.map +1 -0
- package/dist/outOfBandReporter.d.ts +13 -0
- package/dist/outOfBandReporter.d.ts.map +1 -0
- package/dist/outOfBandReporter.js +85 -0
- package/dist/outOfBandReporter.js.map +1 -0
- package/dist/utilities/array.d.ts +1 -2
- package/dist/utilities/array.d.ts.map +1 -1
- package/dist/utilities/array.js +7 -14
- package/dist/utilities/array.js.map +1 -1
- package/dist/utilities/assert.d.ts +2 -0
- package/dist/utilities/assert.d.ts.map +1 -0
- package/dist/utilities/assert.js +10 -0
- package/dist/utilities/assert.js.map +1 -0
- package/dist/utilities/cleanErrorOfInaccessibleNames.d.ts +3 -0
- package/dist/utilities/cleanErrorOfInaccessibleNames.d.ts.map +1 -0
- package/dist/utilities/cleanErrorOfInaccessibleNames.js +27 -0
- package/dist/utilities/cleanErrorOfInaccessibleNames.js.map +1 -0
- package/dist/utilities/deepMerge.js +2 -2
- package/dist/utilities/deepMerge.js.map +1 -1
- package/dist/utilities/graphql.d.ts +1 -4
- package/dist/utilities/graphql.d.ts.map +1 -1
- package/dist/utilities/graphql.js +3 -36
- package/dist/utilities/graphql.js.map +1 -1
- package/dist/utilities/opentelemetry.d.ts +10 -0
- package/dist/utilities/opentelemetry.d.ts.map +1 -0
- package/dist/utilities/opentelemetry.js +19 -0
- package/dist/utilities/opentelemetry.js.map +1 -0
- package/package.json +32 -23
- package/src/__generated__/graphqlTypes.ts +140 -0
- package/src/__mocks__/apollo-server-env.ts +56 -0
- package/src/__mocks__/make-fetch-happen-fetcher.ts +57 -0
- package/src/__mocks__/tsconfig.json +7 -0
- package/src/__tests__/build-query-plan.feature +40 -311
- package/src/__tests__/buildQueryPlan.test.ts +246 -426
- package/src/__tests__/executeQueryPlan.test.ts +2289 -194
- package/src/__tests__/execution-utils.ts +33 -26
- package/src/__tests__/gateway/__snapshots__/opentelemetry.test.ts.snap +195 -0
- package/src/__tests__/gateway/buildService.test.ts +16 -19
- package/src/__tests__/gateway/composedSdl.test.ts +44 -0
- package/src/__tests__/gateway/endToEnd.test.ts +166 -0
- package/src/__tests__/gateway/executor.test.ts +49 -43
- package/src/__tests__/gateway/lifecycle-hooks.test.ts +58 -29
- package/src/__tests__/gateway/opentelemetry.test.ts +123 -0
- package/src/__tests__/gateway/queryPlanCache.test.ts +19 -20
- package/src/__tests__/gateway/reporting.test.ts +83 -59
- package/src/__tests__/integration/abstract-types.test.ts +1086 -22
- package/src/__tests__/integration/aliases.test.ts +5 -6
- package/src/__tests__/integration/boolean.test.ts +40 -38
- package/src/__tests__/integration/complex-key.test.ts +41 -56
- package/src/__tests__/integration/configuration.test.ts +361 -0
- package/src/__tests__/integration/custom-directives.test.ts +61 -46
- package/src/__tests__/integration/fragments.test.ts +8 -2
- package/src/__tests__/integration/list-key.test.ts +2 -2
- package/src/__tests__/integration/logger.test.ts +2 -2
- package/src/__tests__/integration/multiple-key.test.ts +11 -12
- package/src/__tests__/integration/mutations.test.ts +8 -5
- package/src/__tests__/integration/networkRequests.test.ts +454 -294
- package/src/__tests__/integration/nockMocks.ts +100 -65
- package/src/__tests__/integration/provides.test.ts +9 -6
- package/src/__tests__/integration/requires.test.ts +17 -15
- package/src/__tests__/integration/scope.test.ts +557 -0
- package/src/__tests__/integration/unions.test.ts +1 -1
- package/src/__tests__/integration/value-types.test.ts +35 -32
- package/src/__tests__/integration/variables.test.ts +8 -2
- package/src/__tests__/loadServicesFromRemoteEndpoint.test.ts +6 -2
- package/src/__tests__/loadSupergraphSdlFromStorage.test.ts +343 -0
- package/src/__tests__/nockAssertions.ts +20 -0
- package/src/__tests__/queryPlanCucumber.test.ts +11 -61
- package/src/__tests__/testSetup.ts +1 -4
- package/src/__tests__/tsconfig.json +2 -1
- package/src/config.ts +227 -0
- package/src/core/__tests__/core.test.ts +412 -0
- package/src/datasources/LocalGraphQLDataSource.ts +9 -10
- package/src/datasources/RemoteGraphQLDataSource.ts +125 -45
- package/src/datasources/__tests__/LocalGraphQLDataSource.test.ts +11 -4
- package/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts +148 -79
- package/src/datasources/__tests__/tsconfig.json +4 -2
- package/src/datasources/index.ts +1 -1
- package/src/datasources/parseCacheControlHeader.ts +43 -0
- package/src/datasources/types.ts +47 -2
- package/src/executeQueryPlan.ts +275 -154
- package/src/index.ts +939 -480
- package/src/loadServicesFromRemoteEndpoint.ts +24 -17
- package/src/loadSupergraphSdlFromStorage.ts +186 -0
- package/src/make-fetch-happen.d.ts +2 -2
- package/src/operationContext.ts +70 -0
- package/src/outOfBandReporter.ts +126 -0
- package/src/utilities/__tests__/cleanErrorOfInaccessibleElements.test.ts +104 -0
- package/src/utilities/__tests__/tsconfig.json +8 -0
- package/src/utilities/array.ts +6 -28
- package/src/utilities/assert.ts +14 -0
- package/src/utilities/cleanErrorOfInaccessibleNames.ts +29 -0
- package/src/utilities/graphql.ts +0 -64
- package/src/utilities/opentelemetry.ts +13 -0
- package/CHANGELOG.md +0 -226
- package/LICENSE.md +0 -20
- package/dist/FieldSet.d.ts +0 -18
- package/dist/FieldSet.d.ts.map +0 -1
- package/dist/FieldSet.js +0 -96
- package/dist/FieldSet.js.map +0 -1
- package/dist/QueryPlan.d.ts +0 -41
- package/dist/QueryPlan.d.ts.map +0 -1
- package/dist/QueryPlan.js +0 -15
- package/dist/QueryPlan.js.map +0 -1
- package/dist/buildQueryPlan.d.ts +0 -44
- package/dist/buildQueryPlan.d.ts.map +0 -1
- package/dist/buildQueryPlan.js +0 -670
- package/dist/buildQueryPlan.js.map +0 -1
- package/dist/loadServicesFromStorage.d.ts +0 -21
- package/dist/loadServicesFromStorage.d.ts.map +0 -1
- package/dist/loadServicesFromStorage.js +0 -64
- package/dist/loadServicesFromStorage.js.map +0 -1
- package/dist/snapshotSerializers/astSerializer.d.ts +0 -3
- package/dist/snapshotSerializers/astSerializer.d.ts.map +0 -1
- package/dist/snapshotSerializers/astSerializer.js +0 -14
- package/dist/snapshotSerializers/astSerializer.js.map +0 -1
- package/dist/snapshotSerializers/index.d.ts +0 -13
- package/dist/snapshotSerializers/index.d.ts.map +0 -1
- package/dist/snapshotSerializers/index.js +0 -15
- package/dist/snapshotSerializers/index.js.map +0 -1
- package/dist/snapshotSerializers/queryPlanSerializer.d.ts +0 -3
- package/dist/snapshotSerializers/queryPlanSerializer.d.ts.map +0 -1
- package/dist/snapshotSerializers/queryPlanSerializer.js +0 -78
- package/dist/snapshotSerializers/queryPlanSerializer.js.map +0 -1
- package/dist/snapshotSerializers/selectionSetSerializer.d.ts +0 -3
- package/dist/snapshotSerializers/selectionSetSerializer.d.ts.map +0 -1
- package/dist/snapshotSerializers/selectionSetSerializer.js +0 -12
- package/dist/snapshotSerializers/selectionSetSerializer.js.map +0 -1
- package/dist/snapshotSerializers/typeSerializer.d.ts +0 -3
- package/dist/snapshotSerializers/typeSerializer.d.ts.map +0 -1
- package/dist/snapshotSerializers/typeSerializer.js +0 -12
- package/dist/snapshotSerializers/typeSerializer.js.map +0 -1
- package/dist/utilities/MultiMap.d.ts +0 -4
- package/dist/utilities/MultiMap.d.ts.map +0 -1
- package/dist/utilities/MultiMap.js +0 -17
- package/dist/utilities/MultiMap.js.map +0 -1
- package/src/FieldSet.ts +0 -169
- package/src/QueryPlan.ts +0 -57
- package/src/__tests__/matchers/toCallService.ts +0 -105
- package/src/__tests__/matchers/toHaveBeenCalledBefore.ts +0 -40
- package/src/__tests__/matchers/toHaveFetched.ts +0 -81
- package/src/__tests__/matchers/toMatchAST.ts +0 -64
- package/src/buildQueryPlan.ts +0 -1190
- package/src/loadServicesFromStorage.ts +0 -170
- package/src/snapshotSerializers/astSerializer.ts +0 -21
- package/src/snapshotSerializers/index.ts +0 -21
- package/src/snapshotSerializers/queryPlanSerializer.ts +0 -144
- package/src/snapshotSerializers/selectionSetSerializer.ts +0 -13
- package/src/snapshotSerializers/typeSerializer.ts +0 -11
- package/src/utilities/MultiMap.ts +0 -11
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
import { GraphQLSchema, GraphQLError, getIntrospectionQuery } from 'graphql';
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
(
|
|
38
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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":
|
|
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
|
|
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
|
|
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":
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
});
|