@apollo/gateway 0.300.0-alpha.2 → 2.0.0-alpha.2
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 +104 -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 +60 -17
- 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 +199 -112
- package/dist/executeQueryPlan.js.map +1 -1
- package/dist/index.d.ts +62 -80
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +543 -234
- 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 +13 -0
- package/dist/loadSupergraphSdlFromStorage.d.ts.map +1 -0
- package/dist/loadSupergraphSdlFromStorage.js +101 -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 +15 -0
- package/dist/outOfBandReporter.d.ts.map +1 -0
- package/dist/outOfBandReporter.js +88 -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 +30 -21
- package/src/__generated__/graphqlTypes.ts +140 -0
- package/src/__mocks__/apollo-server-env.ts +56 -0
- package/src/__mocks__/make-fetch-happen-fetcher.ts +55 -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 +1691 -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 +76 -55
- 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 +321 -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 +447 -289
- package/src/__tests__/integration/nockMocks.ts +95 -66
- 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 +694 -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 +225 -0
- package/src/core/__tests__/core.test.ts +412 -0
- package/src/datasources/LocalGraphQLDataSource.ts +9 -10
- package/src/datasources/RemoteGraphQLDataSource.ts +117 -43
- 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 +264 -153
- package/src/index.ts +925 -480
- package/src/loadServicesFromRemoteEndpoint.ts +24 -17
- package/src/loadSupergraphSdlFromStorage.ts +140 -0
- package/src/make-fetch-happen.d.ts +2 -2
- package/src/operationContext.ts +70 -0
- package/src/outOfBandReporter.ts +128 -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,23 +1,26 @@
|
|
|
1
1
|
import { GraphQLRequest } from 'apollo-server-types';
|
|
2
2
|
import { parse } from 'graphql';
|
|
3
3
|
import { Headers, HeadersInit } from 'node-fetch';
|
|
4
|
-
import { GraphQLDataSource } from './datasources/types';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { GraphQLDataSource, GraphQLDataSourceRequestKind } from './datasources/types';
|
|
5
|
+
import { SERVICE_DEFINITION_QUERY } from './';
|
|
6
|
+
import { CompositionUpdate, ServiceEndpointDefinition } from './config';
|
|
7
|
+
import { ServiceDefinition } from '@apollo/federation-internals';
|
|
8
|
+
|
|
9
|
+
type Service = ServiceEndpointDefinition & {
|
|
10
|
+
dataSource: GraphQLDataSource;
|
|
11
|
+
};
|
|
7
12
|
|
|
8
13
|
export async function getServiceDefinitionsFromRemoteEndpoint({
|
|
9
14
|
serviceList,
|
|
10
|
-
|
|
15
|
+
getServiceIntrospectionHeaders,
|
|
11
16
|
serviceSdlCache,
|
|
12
17
|
}: {
|
|
13
|
-
serviceList:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}[];
|
|
18
|
-
headers?: HeadersInit;
|
|
18
|
+
serviceList: Service[];
|
|
19
|
+
getServiceIntrospectionHeaders: (
|
|
20
|
+
service: ServiceEndpointDefinition,
|
|
21
|
+
) => Promise<HeadersInit | undefined>;
|
|
19
22
|
serviceSdlCache: Map<string, string>;
|
|
20
|
-
}):
|
|
23
|
+
}): Promise<CompositionUpdate> {
|
|
21
24
|
if (!serviceList || !serviceList.length) {
|
|
22
25
|
throw new Error(
|
|
23
26
|
'Tried to load services from remote endpoints but none provided',
|
|
@@ -26,7 +29,7 @@ export async function getServiceDefinitionsFromRemoteEndpoint({
|
|
|
26
29
|
|
|
27
30
|
let isNewSchema = false;
|
|
28
31
|
// for each service, fetch its introspection schema
|
|
29
|
-
const promiseOfServiceList = serviceList.map(({ name, url, dataSource }) => {
|
|
32
|
+
const promiseOfServiceList = serviceList.map(async ({ name, url, dataSource }) => {
|
|
30
33
|
if (!url) {
|
|
31
34
|
throw new Error(
|
|
32
35
|
`Tried to load schema for '${name}' but no 'url' was specified.`);
|
|
@@ -37,12 +40,16 @@ export async function getServiceDefinitionsFromRemoteEndpoint({
|
|
|
37
40
|
http: {
|
|
38
41
|
url,
|
|
39
42
|
method: 'POST',
|
|
40
|
-
headers: new Headers(
|
|
43
|
+
headers: new Headers(await getServiceIntrospectionHeaders({ name, url })),
|
|
41
44
|
},
|
|
42
45
|
};
|
|
43
46
|
|
|
44
47
|
return dataSource
|
|
45
|
-
.process({
|
|
48
|
+
.process({
|
|
49
|
+
kind: GraphQLDataSourceRequestKind.LOADING_SCHEMA,
|
|
50
|
+
request,
|
|
51
|
+
context: {},
|
|
52
|
+
})
|
|
46
53
|
.then(({ data, errors }): ServiceDefinition => {
|
|
47
54
|
if (data && !errors) {
|
|
48
55
|
const typeDefs = data._service.sdl as string;
|
|
@@ -60,12 +67,12 @@ export async function getServiceDefinitionsFromRemoteEndpoint({
|
|
|
60
67
|
};
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
throw new Error(errors?.map(e => e.message).join(
|
|
70
|
+
throw new Error(errors?.map((e) => e.message).join('\n'));
|
|
64
71
|
})
|
|
65
|
-
.catch(err => {
|
|
72
|
+
.catch((err) => {
|
|
66
73
|
const errorMessage =
|
|
67
74
|
`Couldn't load service definitions for "${name}" at ${url}` +
|
|
68
|
-
(err && err.message ?
|
|
75
|
+
(err && err.message ? ': ' + err.message || err : '');
|
|
69
76
|
|
|
70
77
|
throw new Error(errorMessage);
|
|
71
78
|
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { fetch, Response, Request } from 'apollo-server-env';
|
|
2
|
+
import { GraphQLError } from 'graphql';
|
|
3
|
+
import { OutOfBandReporter } from './outOfBandReporter';
|
|
4
|
+
import { SupergraphSdlQuery } from './__generated__/graphqlTypes';
|
|
5
|
+
|
|
6
|
+
// Magic /* GraphQL */ comment below is for codegen, do not remove
|
|
7
|
+
export const SUPERGRAPH_SDL_QUERY = /* GraphQL */`#graphql
|
|
8
|
+
query SupergraphSdl($apiKey: String!, $ref: String!, $ifAfterId: ID) {
|
|
9
|
+
routerConfig(ref: $ref, apiKey: $apiKey, ifAfterId: $ifAfterId) {
|
|
10
|
+
__typename
|
|
11
|
+
... on RouterConfigResult {
|
|
12
|
+
id
|
|
13
|
+
supergraphSdl: supergraphSDL
|
|
14
|
+
}
|
|
15
|
+
... on FetchError {
|
|
16
|
+
code
|
|
17
|
+
message
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
type SupergraphSdlQueryResult =
|
|
25
|
+
| SupergraphSdlQuerySuccess
|
|
26
|
+
| SupergraphSdlQueryFailure;
|
|
27
|
+
|
|
28
|
+
interface SupergraphSdlQuerySuccess {
|
|
29
|
+
data: SupergraphSdlQuery;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SupergraphSdlQueryFailure {
|
|
33
|
+
data?: SupergraphSdlQuery;
|
|
34
|
+
errors: GraphQLError[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { name, version } = require('../package.json');
|
|
38
|
+
|
|
39
|
+
const fetchErrorMsg = "An error occurred while fetching your schema from Apollo: ";
|
|
40
|
+
|
|
41
|
+
export async function loadSupergraphSdlFromStorage({
|
|
42
|
+
graphRef,
|
|
43
|
+
apiKey,
|
|
44
|
+
endpoint,
|
|
45
|
+
fetcher,
|
|
46
|
+
compositionId,
|
|
47
|
+
}: {
|
|
48
|
+
graphRef: string;
|
|
49
|
+
apiKey: string;
|
|
50
|
+
endpoint: string;
|
|
51
|
+
fetcher: typeof fetch;
|
|
52
|
+
compositionId: string | null;
|
|
53
|
+
}) {
|
|
54
|
+
let result: Response;
|
|
55
|
+
const requestDetails = {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
query: SUPERGRAPH_SDL_QUERY,
|
|
59
|
+
variables: {
|
|
60
|
+
ref: graphRef,
|
|
61
|
+
apiKey,
|
|
62
|
+
ifAfterId: compositionId,
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
headers: {
|
|
66
|
+
'apollographql-client-name': name,
|
|
67
|
+
'apollographql-client-version': version,
|
|
68
|
+
'user-agent': `${name}/${version}`,
|
|
69
|
+
'content-type': 'application/json',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const request: Request = new Request(endpoint, requestDetails);
|
|
74
|
+
|
|
75
|
+
const OOBReport = new OutOfBandReporter();
|
|
76
|
+
const startTime = new Date()
|
|
77
|
+
try {
|
|
78
|
+
result = await fetcher(endpoint, requestDetails);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
const endTime = new Date();
|
|
81
|
+
|
|
82
|
+
await OOBReport.submitOutOfBandReportIfConfigured({
|
|
83
|
+
error: e,
|
|
84
|
+
request,
|
|
85
|
+
startedAt: startTime,
|
|
86
|
+
endedAt: endTime,
|
|
87
|
+
fetcher
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
throw new Error(fetchErrorMsg + (e.message ?? e));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const endTime = new Date();
|
|
94
|
+
let response: SupergraphSdlQueryResult;
|
|
95
|
+
|
|
96
|
+
if (result.ok || result.status === 400) {
|
|
97
|
+
try {
|
|
98
|
+
response = await result.json();
|
|
99
|
+
} catch (e) {
|
|
100
|
+
// Bad response
|
|
101
|
+
throw new Error(fetchErrorMsg + result.status + ' ' + e.message ?? e);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if ('errors' in response) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
[fetchErrorMsg, ...response.errors.map((error) => error.message)].join(
|
|
107
|
+
'\n',
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
await OOBReport.submitOutOfBandReportIfConfigured({
|
|
113
|
+
error: new Error(fetchErrorMsg + result.status + ' ' + result.statusText),
|
|
114
|
+
request,
|
|
115
|
+
response: result,
|
|
116
|
+
startedAt: startTime,
|
|
117
|
+
endedAt: endTime,
|
|
118
|
+
fetcher
|
|
119
|
+
});
|
|
120
|
+
throw new Error(fetchErrorMsg + result.status + ' ' + result.statusText);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const { routerConfig } = response.data;
|
|
124
|
+
if (routerConfig.__typename === 'RouterConfigResult') {
|
|
125
|
+
const {
|
|
126
|
+
id,
|
|
127
|
+
supergraphSdl,
|
|
128
|
+
// messages,
|
|
129
|
+
} = routerConfig;
|
|
130
|
+
return { id, supergraphSdl: supergraphSdl! };
|
|
131
|
+
} else if (routerConfig.__typename === 'FetchError') {
|
|
132
|
+
// FetchError case
|
|
133
|
+
const { code, message } = routerConfig;
|
|
134
|
+
throw new Error(`${code}: ${message}`);
|
|
135
|
+
} else if (routerConfig.__typename === 'Unchanged') {
|
|
136
|
+
return null;
|
|
137
|
+
} else {
|
|
138
|
+
throw new Error('Programming error: unhandled response failure');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -36,7 +36,7 @@ declare module 'make-fetch-happen' {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export interface CacheManager {
|
|
39
|
-
delete(req: Request): Promise<
|
|
39
|
+
delete(req: Request): Promise<boolean>;
|
|
40
40
|
put(req: Request, res: Response): Promise<Response>;
|
|
41
41
|
match(req: Request): Promise<Response | undefined>;
|
|
42
42
|
}
|
|
@@ -52,7 +52,7 @@ declare module 'make-fetch-happen' {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
let fetch: Fetcher & {
|
|
55
|
-
defaults(opts?: RequestInit & FetcherOptions):
|
|
55
|
+
defaults(opts?: RequestInit & FetcherOptions): typeof fetch;
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
export default fetch;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DocumentNode,
|
|
3
|
+
FragmentDefinitionNode,
|
|
4
|
+
GraphQLError,
|
|
5
|
+
GraphQLSchema,
|
|
6
|
+
Kind,
|
|
7
|
+
OperationDefinitionNode,
|
|
8
|
+
} from 'graphql';
|
|
9
|
+
|
|
10
|
+
type FragmentMap = { [fragmentName: string]: FragmentDefinitionNode };
|
|
11
|
+
|
|
12
|
+
export type OperationContext = {
|
|
13
|
+
schema: GraphQLSchema;
|
|
14
|
+
operation: OperationDefinitionNode;
|
|
15
|
+
fragments: FragmentMap;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Adapted from buildExecutionContext in graphql-js
|
|
19
|
+
interface BuildOperationContextOptions {
|
|
20
|
+
schema: GraphQLSchema;
|
|
21
|
+
operationDocument: DocumentNode;
|
|
22
|
+
operationName?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildOperationContext({
|
|
26
|
+
schema,
|
|
27
|
+
operationDocument,
|
|
28
|
+
operationName,
|
|
29
|
+
}: BuildOperationContextOptions): OperationContext {
|
|
30
|
+
let operation: OperationDefinitionNode | undefined;
|
|
31
|
+
let operationCount = 0;
|
|
32
|
+
const fragments: {
|
|
33
|
+
[fragmentName: string]: FragmentDefinitionNode;
|
|
34
|
+
} = Object.create(null);
|
|
35
|
+
operationDocument.definitions.forEach(definition => {
|
|
36
|
+
switch (definition.kind) {
|
|
37
|
+
case Kind.OPERATION_DEFINITION:
|
|
38
|
+
operationCount++;
|
|
39
|
+
if (!operationName && operationCount > 1) {
|
|
40
|
+
throw new GraphQLError(
|
|
41
|
+
'Must provide operation name if query contains ' +
|
|
42
|
+
'multiple operations.',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
!operationName ||
|
|
47
|
+
(definition.name && definition.name.value === operationName)
|
|
48
|
+
) {
|
|
49
|
+
operation = definition;
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
case Kind.FRAGMENT_DEFINITION:
|
|
53
|
+
fragments[definition.name.value] = definition;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (!operation) {
|
|
58
|
+
if (operationName) {
|
|
59
|
+
throw new GraphQLError(`Unknown operation named "${operationName}".`);
|
|
60
|
+
} else {
|
|
61
|
+
throw new GraphQLError('Must provide an operation.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
schema,
|
|
67
|
+
operation,
|
|
68
|
+
fragments,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { fetch, Response, Request } from 'apollo-server-env';
|
|
2
|
+
import { GraphQLError } from 'graphql';
|
|
3
|
+
import {
|
|
4
|
+
ErrorCode,
|
|
5
|
+
OobReportMutation,
|
|
6
|
+
OobReportMutationVariables,
|
|
7
|
+
} from './__generated__/graphqlTypes';
|
|
8
|
+
|
|
9
|
+
// Magic /* GraphQL */ comment below is for codegen, do not remove
|
|
10
|
+
export const OUT_OF_BAND_REPORTER_QUERY = /* GraphQL */`#graphql
|
|
11
|
+
mutation OOBReport($input: APIMonitoringReport) {
|
|
12
|
+
reportError(report: $input)
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const { name, version } = require('../package.json');
|
|
17
|
+
|
|
18
|
+
type OobReportMutationResult =
|
|
19
|
+
| OobReportMutationSuccess
|
|
20
|
+
| OobReportMutationFailure;
|
|
21
|
+
|
|
22
|
+
interface OobReportMutationSuccess {
|
|
23
|
+
data: OobReportMutation;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface OobReportMutationFailure {
|
|
27
|
+
data?: OobReportMutation;
|
|
28
|
+
errors: GraphQLError[];
|
|
29
|
+
}
|
|
30
|
+
export class OutOfBandReporter {
|
|
31
|
+
static endpoint: string | null = process.env.APOLLO_OUT_OF_BAND_REPORTER_ENDPOINT || null;
|
|
32
|
+
|
|
33
|
+
async submitOutOfBandReportIfConfigured({
|
|
34
|
+
error,
|
|
35
|
+
request,
|
|
36
|
+
response,
|
|
37
|
+
startedAt,
|
|
38
|
+
endedAt,
|
|
39
|
+
tags,
|
|
40
|
+
fetcher,
|
|
41
|
+
}: {
|
|
42
|
+
error: Error;
|
|
43
|
+
request: Request;
|
|
44
|
+
response?: Response;
|
|
45
|
+
startedAt: Date;
|
|
46
|
+
endedAt: Date;
|
|
47
|
+
tags?: string[];
|
|
48
|
+
fetcher: typeof fetch;
|
|
49
|
+
}) {
|
|
50
|
+
|
|
51
|
+
// don't send report if the endpoint url is not configured
|
|
52
|
+
if (!OutOfBandReporter.endpoint) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let errorCode: ErrorCode;
|
|
57
|
+
if (!response) {
|
|
58
|
+
errorCode = ErrorCode.ConnectionFailed;
|
|
59
|
+
} else {
|
|
60
|
+
// possible error situations to check against
|
|
61
|
+
switch (response.status) {
|
|
62
|
+
case 400:
|
|
63
|
+
case 413:
|
|
64
|
+
case 422:
|
|
65
|
+
errorCode = ErrorCode.InvalidBody;
|
|
66
|
+
break;
|
|
67
|
+
case 408:
|
|
68
|
+
case 504:
|
|
69
|
+
errorCode = ErrorCode.Timeout;
|
|
70
|
+
break;
|
|
71
|
+
case 502:
|
|
72
|
+
case 503:
|
|
73
|
+
errorCode = ErrorCode.ConnectionFailed;
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
errorCode = ErrorCode.Other;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const responseBody: string | undefined = await response?.text();
|
|
81
|
+
|
|
82
|
+
const variables: OobReportMutationVariables = {
|
|
83
|
+
input: {
|
|
84
|
+
error: {
|
|
85
|
+
code: errorCode,
|
|
86
|
+
message: error.message,
|
|
87
|
+
},
|
|
88
|
+
request: {
|
|
89
|
+
url: request.url,
|
|
90
|
+
body: await request.text(),
|
|
91
|
+
},
|
|
92
|
+
response: response
|
|
93
|
+
? {
|
|
94
|
+
httpStatusCode: response.status,
|
|
95
|
+
body: responseBody,
|
|
96
|
+
}
|
|
97
|
+
: null,
|
|
98
|
+
startedAt: startedAt.toISOString(),
|
|
99
|
+
endedAt: endedAt.toISOString(),
|
|
100
|
+
tags: tags,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const oobResponse = await fetcher(OutOfBandReporter.endpoint, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
query: OUT_OF_BAND_REPORTER_QUERY,
|
|
109
|
+
variables,
|
|
110
|
+
}),
|
|
111
|
+
headers: {
|
|
112
|
+
'apollographql-client-name': name,
|
|
113
|
+
'apollographql-client-version': version,
|
|
114
|
+
'user-agent': `${name}/${version}`,
|
|
115
|
+
'content-type': 'application/json',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
const parsedResponse: OobReportMutationResult = await oobResponse.json();
|
|
119
|
+
if (!parsedResponse?.data?.reportError) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Out-of-band error reporting failed: ${oobResponse.status} ${oobResponse.statusText}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
} catch (e) {
|
|
125
|
+
throw new Error(`Out-of-band error reporting failed: ${e.message ?? e}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { execute, GraphQLError, parse } from "graphql";
|
|
2
|
+
import { cleanErrorOfInaccessibleNames } from "../cleanErrorOfInaccessibleNames";
|
|
3
|
+
import { buildSchema } from "@apollo/federation-internals";
|
|
4
|
+
|
|
5
|
+
describe('cleanErrorOfInaccessibleNames', () => {
|
|
6
|
+
const coreSchema = buildSchema(`
|
|
7
|
+
directive @core(
|
|
8
|
+
feature: String!,
|
|
9
|
+
as: String,
|
|
10
|
+
for: core__Purpose
|
|
11
|
+
) repeatable on SCHEMA
|
|
12
|
+
|
|
13
|
+
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
|
|
14
|
+
|
|
15
|
+
schema
|
|
16
|
+
@core(feature: "https://specs.apollo.dev/core/v0.2")
|
|
17
|
+
@core(feature: "https://specs.apollo.dev/inaccessible/v0.1")
|
|
18
|
+
{
|
|
19
|
+
query: Query
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
enum core__Purpose {
|
|
23
|
+
EXECUTION
|
|
24
|
+
SECURITY
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Query {
|
|
28
|
+
fooField: Foo
|
|
29
|
+
bazField: Baz
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Foo {
|
|
33
|
+
someField: String
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type Bar implements Foo @inaccessible {
|
|
37
|
+
someField: String
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type Bar2 @inaccessible {
|
|
41
|
+
anotherField: String
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type Baz {
|
|
45
|
+
goodField: String
|
|
46
|
+
inaccessibleField: String @inaccessible
|
|
47
|
+
}
|
|
48
|
+
`);
|
|
49
|
+
const schema = coreSchema.toAPISchema().toGraphQLJSSchema();
|
|
50
|
+
|
|
51
|
+
it('removes inaccessible type names from error messages', async () => {
|
|
52
|
+
const result = await execute(schema, parse('{fooField{someField}}'), {
|
|
53
|
+
fooField: {
|
|
54
|
+
__typename: 'Bar',
|
|
55
|
+
someField: 'test',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const cleaned = cleanErrorOfInaccessibleNames(schema, result.errors![0]!);
|
|
60
|
+
expect(cleaned.message).toMatchInlineSnapshot(
|
|
61
|
+
`"Abstract type \\"Foo\\" was resolve to a type [inaccessible type] that does not exist inside schema."`,
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('removes multiple/repeated inaccessible type names from error messages', async () => {
|
|
66
|
+
const contrivedError = new GraphQLError(
|
|
67
|
+
`Something something "Bar" and "Bar" again, as well as "Bar2".`,
|
|
68
|
+
);
|
|
69
|
+
const cleaned = cleanErrorOfInaccessibleNames(schema, contrivedError);
|
|
70
|
+
expect(cleaned.message).toMatchInlineSnapshot(
|
|
71
|
+
`"Something something [inaccessible type] and [inaccessible type] again, as well as [inaccessible type]."`,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('removes inaccessible field names from error messages', async () => {
|
|
76
|
+
const contrivedError = new GraphQLError(
|
|
77
|
+
`Can't query inaccessible field "Baz.inaccessibleField".`,
|
|
78
|
+
);
|
|
79
|
+
const cleaned = cleanErrorOfInaccessibleNames(schema, contrivedError);
|
|
80
|
+
expect(cleaned.message).toMatchInlineSnapshot(
|
|
81
|
+
`"Can't query inaccessible field [inaccessible field]."`,
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('removes multiple/repeated inaccessible field names from error messages', async () => {
|
|
86
|
+
const contrivedError = new GraphQLError(
|
|
87
|
+
`Can't query inaccessible field "Baz.inaccessibleField" and "Baz.inaccessibleField", as well as "Bar2.anotherField".`,
|
|
88
|
+
);
|
|
89
|
+
const cleaned = cleanErrorOfInaccessibleNames(schema, contrivedError);
|
|
90
|
+
expect(cleaned.message).toMatchInlineSnapshot(
|
|
91
|
+
`"Can't query inaccessible field [inaccessible field] and [inaccessible field], as well as [inaccessible field]."`,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("doesn't remove special-case double-quoted words from graphql error messages", () => {
|
|
96
|
+
const graphqlError = new GraphQLError(
|
|
97
|
+
`Something something "resolveType" something something "isTypeOf".`,
|
|
98
|
+
);
|
|
99
|
+
const cleaned = cleanErrorOfInaccessibleNames(schema, graphqlError);
|
|
100
|
+
expect(cleaned.message).toMatchInlineSnapshot(
|
|
101
|
+
`"Something something \\"resolveType\\" something something \\"isTypeOf\\"."`,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
});
|
package/src/utilities/array.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
export function isNotNullOrUndefined<T>(
|
|
2
|
+
value: T | null | undefined,
|
|
3
|
+
): value is T {
|
|
4
|
+
return value !== null && typeof value !== 'undefined';
|
|
5
|
+
}
|
|
2
6
|
|
|
3
7
|
export function compactMap<T, U>(
|
|
4
8
|
array: T[],
|
|
@@ -16,32 +20,6 @@ export function compactMap<T, U>(
|
|
|
16
20
|
);
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
export function partition<T, U extends T>(
|
|
20
|
-
array: T[],
|
|
21
|
-
predicate: (element: T, index: number, array: T[]) => element is U,
|
|
22
|
-
): [U[], T[]];
|
|
23
|
-
export function partition<T>(
|
|
24
|
-
array: T[],
|
|
25
|
-
predicate: (element: T, index: number, array: T[]) => boolean,
|
|
26
|
-
): [T[], T[]];
|
|
27
|
-
export function partition<T>(
|
|
28
|
-
array: T[],
|
|
29
|
-
predicate: (element: T, index: number, array: T[]) => boolean,
|
|
30
|
-
): [T[], T[]] {
|
|
31
|
-
array.map;
|
|
32
|
-
return array.reduce(
|
|
33
|
-
(accumulator, element, index) => {
|
|
34
|
-
return (
|
|
35
|
-
predicate(element, index, array)
|
|
36
|
-
? accumulator[0].push(element)
|
|
37
|
-
: accumulator[1].push(element),
|
|
38
|
-
accumulator
|
|
39
|
-
);
|
|
40
|
-
},
|
|
41
|
-
[[], []] as [T[], T[]],
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
23
|
export function findAndExtract<T>(
|
|
46
24
|
array: T[],
|
|
47
25
|
predicate: (element: T, index: number, array: T[]) => boolean,
|
|
@@ -49,7 +27,7 @@ export function findAndExtract<T>(
|
|
|
49
27
|
const index = array.findIndex(predicate);
|
|
50
28
|
if (index === -1) return [undefined, array];
|
|
51
29
|
|
|
52
|
-
|
|
30
|
+
const remaining = array.slice(0, index);
|
|
53
31
|
if (index < array.length - 1) {
|
|
54
32
|
remaining.push(...array.slice(index + 1));
|
|
55
33
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For lack of a "home of federation utilities", this function is copy/pasted
|
|
3
|
+
* verbatim across the federation, gateway, and query-planner packages. Any changes
|
|
4
|
+
* made here should be reflected in the other two locations as well.
|
|
5
|
+
*
|
|
6
|
+
* @param condition
|
|
7
|
+
* @param message
|
|
8
|
+
* @throws {@link Error}
|
|
9
|
+
*/
|
|
10
|
+
export function assert(condition: any, message: string): asserts condition {
|
|
11
|
+
if (!condition) {
|
|
12
|
+
throw new Error(message);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { GraphQLError, GraphQLSchema } from 'graphql';
|
|
2
|
+
|
|
3
|
+
export function cleanErrorOfInaccessibleNames(
|
|
4
|
+
schema: GraphQLSchema,
|
|
5
|
+
error: GraphQLError,
|
|
6
|
+
): GraphQLError {
|
|
7
|
+
|
|
8
|
+
const typeDotFieldRegex = /"([_A-Za-z][_0-9A-Za-z]*)\.([_A-Za-z][_0-9A-Za-z]*)"/g;
|
|
9
|
+
error.message = error.message.replace(typeDotFieldRegex, (match: string) => {
|
|
10
|
+
const [typeName, fieldName] = match.replace(/"/g, '',).split('.');
|
|
11
|
+
const type = schema.getType(typeName);
|
|
12
|
+
if (!type) {
|
|
13
|
+
return '[inaccessible field]';
|
|
14
|
+
} else {
|
|
15
|
+
const field = 'getFields' in type ? type.getFields()[fieldName] : null;
|
|
16
|
+
return field ? match : '[inaccessible field]';
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const typeRegex = /"([_A-Za-z][_0-9A-Za-z]*)"/g;
|
|
21
|
+
error.message = error.message.replace(typeRegex, (match: string) => {
|
|
22
|
+
// Special cases in graphql-js that happen to match our regex.
|
|
23
|
+
if (match === '"isTypeOf"' || match === '"resolveType"') return match;
|
|
24
|
+
const typeName = match.replace(/"/g, '',);
|
|
25
|
+
return schema.getType(typeName) ? match : '[inaccessible type]';
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return error;
|
|
29
|
+
}
|