@apollo/gateway
Version:
196 lines (177 loc) • 5.11 kB
text/typescript
import { GraphQLError } from 'graphql';
import retry from 'async-retry';
import { AbortController } from "node-abort-controller";
import { SupergraphSdlUpdate } from '../../config';
import { SupergraphSdlQuery } from '../../__generated__/graphqlTypes';
import type {
FetcherResponse,
Fetcher,
FetcherRequestInit,
} from '@apollo/utils.fetcher';
import type { Logger } from '@apollo/utils.logger';
// Magic /* GraphQL */ comment below is for codegen, do not remove
export const SUPERGRAPH_SDL_QUERY = /* GraphQL */`#graphql
query SupergraphSdl($apiKey: String!, $ref: String!, $ifAfterId: ID) {
routerConfig(ref: $ref, apiKey: $apiKey, ifAfterId: $ifAfterId) {
__typename
... on RouterConfigResult {
id
supergraphSdl: supergraphSDL
minDelaySeconds
}
... on FetchError {
code
message
}
}
}
`;
type SupergraphSdlQueryResult =
| SupergraphSdlQuerySuccess
| SupergraphSdlQueryFailure;
interface SupergraphSdlQuerySuccess {
data: SupergraphSdlQuery;
}
interface SupergraphSdlQueryFailure {
data?: SupergraphSdlQuery;
errors: GraphQLError[];
}
const { name, version } = require('../../../package.json');
const fetchErrorMsg = "An error occurred while fetching your schema from Apollo: ";
export class UplinkFetcherError extends Error {
constructor(message: string) {
super(message);
this.name = 'UplinkFetcherError';
}
}
export async function loadSupergraphSdlFromUplinks({
graphRef,
apiKey,
endpoints,
fetcher,
compositionId,
maxRetries,
requestTimeoutMs,
roundRobinSeed,
logger,
}: {
graphRef: string;
apiKey: string;
endpoints: string[];
fetcher: Fetcher;
compositionId: string | null;
maxRetries: number,
requestTimeoutMs: number,
roundRobinSeed: number,
logger: Logger,
}) : Promise<SupergraphSdlUpdate | null> {
// This Promise resolves with either an updated supergraph or null if no change.
// This Promise can reject in the case that none of the retries are successful,
// in which case it will reject with the most frequently encountered error.
return retry(
() =>
loadSupergraphSdlFromStorage({
graphRef,
apiKey,
endpoint: endpoints[roundRobinSeed++ % endpoints.length],
fetcher,
requestTimeoutMs,
compositionId,
logger,
}),
{
retries: maxRetries,
maxTimeout: 60_000,
onRetry(e, attempt) {
logger.debug(`Unable to fetch supergraph SDL (attempt ${attempt}), waiting before retry: ${e}`);
},
},
);
}
export async function loadSupergraphSdlFromStorage({
graphRef,
apiKey,
endpoint,
fetcher,
requestTimeoutMs,
compositionId,
logger,
}: {
graphRef: string;
apiKey: string;
endpoint: string;
fetcher: Fetcher;
requestTimeoutMs: number;
compositionId: string | null;
logger: Logger;
}) : Promise<SupergraphSdlUpdate | null> {
const requestBody = JSON.stringify({
query: SUPERGRAPH_SDL_QUERY,
variables: {
ref: graphRef,
apiKey,
ifAfterId: compositionId,
},
})
const controller = new AbortController();
const signal = setTimeout(() => {
logger.debug(`Aborting request due to timeout`);
controller.abort();
}, requestTimeoutMs);
const requestDetails: FetcherRequestInit = {
method: 'POST',
body: requestBody,
headers: {
'apollographql-client-name': name,
'apollographql-client-version': version,
'user-agent': `${name}/${version}`,
'content-type': 'application/json',
},
signal: controller.signal,
};
logger.debug(`🔧 Fetching ${graphRef} supergraph schema from ${endpoint} ifAfterId ${compositionId}`);
let result: FetcherResponse;
try {
result = await fetcher(endpoint, requestDetails);
} catch (e) {
throw new UplinkFetcherError(fetchErrorMsg + (e.message ?? e));
} finally {
clearTimeout(signal);
}
let response: SupergraphSdlQueryResult;
if (result.ok || result.status === 400) {
try {
response = await result.json();
} catch (e) {
// Bad response
throw new UplinkFetcherError(fetchErrorMsg + result.status + ' ' + e.message ?? e);
}
if ('errors' in response) {
throw new UplinkFetcherError(
[fetchErrorMsg, ...response.errors.map((error) => error.message)].join(
'\n',
),
);
}
} else {
throw new UplinkFetcherError(fetchErrorMsg + result.status + ' ' + result.statusText);
}
const { routerConfig } = response.data;
if (routerConfig.__typename === 'RouterConfigResult') {
const {
id,
supergraphSdl,
minDelaySeconds,
// messages,
} = routerConfig;
return { id, supergraphSdl, minDelaySeconds };
} else if (routerConfig.__typename === 'FetchError') {
// FetchError case
const { code, message } = routerConfig;
throw new UplinkFetcherError(`${code}: ${message}`);
} else if (routerConfig.__typename === 'Unchanged') {
return null;
} else {
throw new UplinkFetcherError('Programming error: unhandled response failure');
}
}