UNPKG

@graphql-tools/federation

Version:

Useful tools to create and manipulate GraphQL schemas.

240 lines (239 loc) • 12.4 kB
var _SupergraphSchemaManager_lastSeenId, _SupergraphSchemaManager_retries, _SupergraphSchemaManager_timeout, _SupergraphSchemaManager_fetchSchema, _SupergraphSchemaManager_retryOnError, _SupergraphSchemaManager_log; import { __classPrivateFieldGet, __classPrivateFieldSet } from "tslib"; import { fetch as defaultFetch } from '@whatwg-node/fetch'; import { getStitchedSchemaFromSupergraphSdl, } from './supergraph.js'; import { EventEmitter } from './utils.js'; /** * The default managed federation up links. In case of failure, you should try to cycle through these up links. * * The first one is Apollo's managed federation up link on GCP, the second one is on AWS. */ export const DEFAULT_UPLINKS = [ 'https://uplink.api.apollographql.com/', 'https://aws.uplink.api.apollographql.com/', ]; /** * Fetches the supergraph SDL from a managed federation GraphOS up link. * @param options * @throws When the fetch fails or the response is not a valid. * @returns An object with the supergraph SDL when possible. It also includes metadata to handle polling and retry logic. * * If `lastSeenId` is provided and the supergraph has not changed, `supergraphSdl` is not present. * * If The up link report a fetch error (which is not a local fetch error), it will be returned along with polling/retry metadata. * Any local fetch error will be thrown as an exception. */ export async function fetchSupergraphSdlFromManagedFederation(options = {}) { const userDefinedUplinks = process.env['APOLLO_SCHEMA_CONFIG_DELIVERY_ENDPOINT']?.split(',') ?? []; const { upLink = userDefinedUplinks[0] || DEFAULT_UPLINKS[0], loggerByMessageLevel = DEFAULT_MESSAGE_LOGGER, fetch = defaultFetch, ...variables } = options; if (!variables.graphRef) { variables.graphRef = process.env['APOLLO_GRAPH_REF']; } if (!variables.apiKey) { variables.apiKey = process.env['APOLLO_KEY']; } const response = await fetch(upLink, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: /* GraphQL */ ` query ($apiKey: String!, $graphRef: String!, $lastSeenId: ID) { routerConfig(ref: $graphRef, apiKey: $apiKey, ifAfterId: $lastSeenId) { __typename ... on FetchError { code message minDelaySeconds } ... on Unchanged { id minDelaySeconds } ... on RouterConfigResult { id supergraphSdl: supergraphSDL minDelaySeconds messages { level body } } } } `, variables, }), }); const responseBody = await response.text(); if (!response.ok) { throw new Error(`Failed to fetch supergraph SDL from managed federation up link '${upLink}': [${response.status} ${response.statusText}] ${responseBody}`); } let result; try { result = JSON.parse(responseBody); } catch (err) { throw new Error(`Failed to parse response from managed federation up link '${upLink}': ${err.message}\n\n${responseBody}`); } if (result.errors) { const errors = result.errors.map(({ message }) => '\n' + message).join(''); throw new Error(`Failed to fetch supergraph SDL from managed federation up link '${upLink}': ${errors}`); } if (!result.data?.routerConfig) { throw new Error(`Failed to fetch supergraph SDL from managed federation up link '${upLink}': ${responseBody}`); } const { routerConfig } = result.data; if (routerConfig.__typename === 'FetchError') { return { error: { code: routerConfig.code, message: routerConfig.message }, minDelaySeconds: routerConfig.minDelaySeconds, }; } if (routerConfig.__typename === 'Unchanged') { return { id: routerConfig.id, minDelaySeconds: routerConfig.minDelaySeconds }; } for (const message of routerConfig.messages) { loggerByMessageLevel[message.level](message.body); } return { supergraphSdl: routerConfig.supergraphSdl, id: routerConfig.id, minDelaySeconds: routerConfig.minDelaySeconds, }; } /** * Fetches the supergraph SDL from a managed federation GraphOS up link and stitches it into an executable schema. * @param options * @throws When the fetch fails, the response is not a valid or the stitching fails. * @returns An object with the supergraph SDL and the stitched schema when possible. It also includes metadata to handle polling and retry logic. * * If `lastSeenId` is provided and the supergraph has not changed, `supergraphSdl` is not present. * * If The up link report a fetch error (which is not a local fetch error), it will be returned along with polling/retry metadata. * Any local fetch error will be thrown as an exception. */ export async function getStitchedSchemaFromManagedFederation(options) { const result = await fetchSupergraphSdlFromManagedFederation({ graphRef: options.graphRef, apiKey: options.apiKey, upLink: options.upLink, lastSeenId: options.lastSeenId, fetch: options.fetch, loggerByMessageLevel: options.loggerByMessageLevel, }); if ('supergraphSdl' in result) { return { ...result, schema: getStitchedSchemaFromSupergraphSdl({ supergraphSdl: result.supergraphSdl, onStitchingOptions: options.onStitchingOptions, httpExecutorOpts: options.httpExecutorOpts, onSubschemaConfig: options.onSubschemaConfig, batch: options.batch, }), }; } return result; } const DEFAULT_MESSAGE_LOGGER = { ERROR: (message) => console.error('[Managed Federation] Uplink message: [ERROR]', message), WARN: (message) => console.warn('[Managed Federation] Uplink message: [WARN]', message), INFO: (message) => console.info('[Managed Federation] Uplink message: [INFO]', message), }; export class SupergraphSchemaManager extends EventEmitter { constructor(options) { super(); this.options = options; this.schema = undefined; _SupergraphSchemaManager_lastSeenId.set(this, void 0); _SupergraphSchemaManager_retries.set(this, 1); _SupergraphSchemaManager_timeout.set(this, void 0); _SupergraphSchemaManager_fetchSchema.set(this, async () => { const { retryDelaySeconds = 0, minDelaySeconds = 0 } = this.options; try { __classPrivateFieldGet(this, _SupergraphSchemaManager_log, "f").call(this, 'info', 'Fetch schema from managed federation'); const result = await getStitchedSchemaFromManagedFederation({ ...this.options, loggerByMessageLevel: { ERROR: message => this.emit('log', { source: 'uplink', level: 'error', message }), WARN: message => this.emit('log', { source: 'uplink', level: 'warn', message }), INFO: message => this.emit('log', { source: 'uplink', level: 'info', message }), }, lastSeenId: __classPrivateFieldGet(this, _SupergraphSchemaManager_lastSeenId, "f"), }); if ('error' in result) { __classPrivateFieldSet(this, _SupergraphSchemaManager_lastSeenId, undefined, "f"); // When an error is reported, Apollo doesn't provide an id. this.emit('error', result.error); __classPrivateFieldGet(this, _SupergraphSchemaManager_retryOnError, "f").call(this, result.error, Math.max(result.minDelaySeconds, minDelaySeconds)); return; } if ('schema' in result) { __classPrivateFieldSet(this, _SupergraphSchemaManager_lastSeenId, result.id, "f"); this.schema = result.schema; this.emit('schema', result.schema); __classPrivateFieldGet(this, _SupergraphSchemaManager_log, "f").call(this, 'info', 'Supergraph successfully updated'); } else { __classPrivateFieldGet(this, _SupergraphSchemaManager_log, "f").call(this, 'info', 'Supergraph is up to date'); } __classPrivateFieldSet(this, _SupergraphSchemaManager_retries, 1, "f"); const delay = Math.max(result.minDelaySeconds, minDelaySeconds); __classPrivateFieldSet(this, _SupergraphSchemaManager_timeout, setTimeout(__classPrivateFieldGet(this, _SupergraphSchemaManager_fetchSchema, "f"), delay * 1000), "f"); __classPrivateFieldGet(this, _SupergraphSchemaManager_log, "f").call(this, 'info', `Next pull in ${delay.toFixed(1)} seconds`); } catch (e) { __classPrivateFieldGet(this, _SupergraphSchemaManager_retryOnError, "f").call(this, e, retryDelaySeconds ?? 0); this.emit('error', e); } }); _SupergraphSchemaManager_retryOnError.set(this, (error, delayInSeconds) => { var _a; const { maxRetries = 3 } = this.options; __classPrivateFieldGet(this, _SupergraphSchemaManager_log, "f").call(this, 'error', 'Failed to pull schema from managed federation:'); if (__classPrivateFieldGet(this, _SupergraphSchemaManager_retries, "f") >= maxRetries) { __classPrivateFieldGet(this, _SupergraphSchemaManager_log, "f").call(this, 'error', 'Max retries reached, giving up'); this.emit('failure', error); return; } __classPrivateFieldSet(this, _SupergraphSchemaManager_retries, (_a = __classPrivateFieldGet(this, _SupergraphSchemaManager_retries, "f"), _a++, _a), "f"); __classPrivateFieldGet(this, _SupergraphSchemaManager_log, "f").call(this, 'info', `Retrying (${__classPrivateFieldGet(this, _SupergraphSchemaManager_retries, "f")}/${maxRetries})${delayInSeconds ? ` in ${delayInSeconds.toFixed(1)} seconds` : ''}`); __classPrivateFieldSet(this, _SupergraphSchemaManager_timeout, setTimeout(__classPrivateFieldGet(this, _SupergraphSchemaManager_fetchSchema, "f"), delayInSeconds * 1000), "f"); }); _SupergraphSchemaManager_log.set(this, (level, message) => { this.emit('log', { source: 'manager', level, message }); }); registerCleanup(() => { this.stop(); }); } start() { __classPrivateFieldGet(this, _SupergraphSchemaManager_log, "f").call(this, 'info', 'Polling started'); __classPrivateFieldGet(this, _SupergraphSchemaManager_fetchSchema, "f").call(this); } forcePull() { __classPrivateFieldGet(this, _SupergraphSchemaManager_fetchSchema, "f").call(this); __classPrivateFieldSet(this, _SupergraphSchemaManager_retries, 1, "f"); if (__classPrivateFieldGet(this, _SupergraphSchemaManager_timeout, "f")) { clearTimeout(__classPrivateFieldGet(this, _SupergraphSchemaManager_timeout, "f")); __classPrivateFieldSet(this, _SupergraphSchemaManager_timeout, undefined, "f"); } } stop() { __classPrivateFieldGet(this, _SupergraphSchemaManager_log, "f").call(this, 'info', 'Polling stopped'); if (__classPrivateFieldGet(this, _SupergraphSchemaManager_timeout, "f")) { clearTimeout(__classPrivateFieldGet(this, _SupergraphSchemaManager_timeout, "f")); __classPrivateFieldSet(this, _SupergraphSchemaManager_timeout, undefined, "f"); } } } _SupergraphSchemaManager_lastSeenId = new WeakMap(), _SupergraphSchemaManager_retries = new WeakMap(), _SupergraphSchemaManager_timeout = new WeakMap(), _SupergraphSchemaManager_fetchSchema = new WeakMap(), _SupergraphSchemaManager_retryOnError = new WeakMap(), _SupergraphSchemaManager_log = new WeakMap(); function registerCleanup(cleanupFn) { if (typeof global.process === 'object') { for (const signal of ['SIGINT', 'SIGTERM', 'SIGQUIT']) process.on(signal, () => { cleanupFn(); }); } }