@graphql-tools/federation
Version:
Useful tools to create and manipulate GraphQL schemas.
240 lines (239 loc) • 12.4 kB
JavaScript
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();
});
}
}