UNPKG

@graphql-mesh/serve-runtime

Version:
487 lines (485 loc) 23.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createServeRuntime = createServeRuntime; const tslib_1 = require("tslib"); const graphql_1 = require("graphql"); const graphql_yoga_1 = require("graphql-yoga"); const apollo_1 = require("@graphql-hive/apollo"); const cross_helpers_1 = require("@graphql-mesh/cross-helpers"); const fusion_runtime_1 = require("@graphql-mesh/fusion-runtime"); const plugin_hive_1 = tslib_1.__importDefault(require("@graphql-mesh/plugin-hive")); const utils_1 = require("@graphql-mesh/utils"); const batch_delegate_1 = require("@graphql-tools/batch-delegate"); const delegate_1 = require("@graphql-tools/delegate"); const executor_yoga_1 = require("@graphql-tools/executor-yoga"); const utils_2 = require("@graphql-tools/utils"); const wrap_1 = require("@graphql-tools/wrap"); const disposablestack_1 = require("@whatwg-node/disposablestack"); const getProxyExecutor_js_1 = require("./getProxyExecutor.js"); const handleUnifiedGraphConfig_js_1 = require("./handleUnifiedGraphConfig.js"); const landing_page_html_js_1 = tslib_1.__importDefault(require("./landing-page-html.js")); const useChangingSchema_js_1 = require("./useChangingSchema.js"); const useCompleteSubscriptionsOnDispose_js_1 = require("./useCompleteSubscriptionsOnDispose.js"); const useCompleteSubscriptionsOnSchemaChange_js_1 = require("./useCompleteSubscriptionsOnSchemaChange.js"); const useFetchDebug_js_1 = require("./useFetchDebug.js"); const useRequestId_js_1 = require("./useRequestId.js"); const useSubgraphExecuteDebug_js_1 = require("./useSubgraphExecuteDebug.js"); const utils_js_1 = require("./utils.js"); function createServeRuntime(config = {}) { let fetchAPI = config.fetchAPI; let logger; if (config.logging == null) { logger = new utils_1.DefaultLogger(); } else if (typeof config.logging === 'boolean') { logger = config.logging ? new utils_1.DefaultLogger() : new utils_1.DefaultLogger('', utils_1.LogLevel.silent); } if (typeof config.logging === 'number') { logger = new utils_1.DefaultLogger(undefined, config.logging); } else if (typeof config.logging === 'object') { logger = config.logging; } const onFetchHooks = []; const wrappedFetchFn = (0, utils_1.wrapFetchWithHooks)(onFetchHooks); const configContext = { fetch: wrappedFetchFn, logger, cwd: 'cwd' in config ? config.cwd : cross_helpers_1.process.cwd?.(), cache: 'cache' in config ? config.cache : undefined, pubsub: 'pubsub' in config ? config.pubsub : undefined, }; let unifiedGraphPlugin; const readinessCheckEndpoint = config.readinessCheckEndpoint || '/readiness'; const onSubgraphExecuteHooks = []; // TODO: Will be deleted after v0 const onDelegateHooks = []; let unifiedGraph; let schemaInvalidator; let getSchema = () => unifiedGraph; let schemaChanged; let contextBuilder; let readinessChecker; let registryPlugin = {}; let subgraphInformationHTMLRenderer = () => ''; const disposableStack = new disposablestack_1.AsyncDisposableStack(); if ('proxy' in config) { const proxyExecutor = (0, getProxyExecutor_js_1.getProxyExecutor)({ config, configContext, getSchema: () => unifiedGraph, onSubgraphExecuteHooks, disposableStack, }); const executorPlugin = (0, executor_yoga_1.useExecutor)(proxyExecutor); executorPlugin.onSchemaChange = function onSchemaChange(payload) { unifiedGraph = payload.schema; }; if (config.skipValidation) { executorPlugin.onValidate = function ({ setResult }) { setResult([]); }; } unifiedGraphPlugin = executorPlugin; readinessChecker = () => { const res$ = proxyExecutor({ document: (0, graphql_1.parse)(`query { __typename }`), }); return (0, utils_1.mapMaybePromise)(res$, res => !(0, graphql_yoga_1.isAsyncIterable)(res) && !!res.data?.__typename); }; schemaInvalidator = () => executorPlugin.invalidateUnifiedGraph(); subgraphInformationHTMLRenderer = () => { const endpoint = config.proxy.endpoint || '#'; return `<section class="supergraph-information"><h3>Proxy (<a href="${endpoint}">${endpoint}</a>): ${unifiedGraph ? 'Loaded ✅' : 'Not yet ❌'}</h3></section>`; }; } else if ('subgraph' in config) { const subgraphInConfig = config.subgraph; let getSubschemaConfig$; let subschemaConfig; function getSubschemaConfig() { return (0, utils_1.mapMaybePromise)((0, handleUnifiedGraphConfig_js_1.handleUnifiedGraphConfig)(subgraphInConfig, configContext), newUnifiedGraph => { unifiedGraph = newUnifiedGraph; unifiedGraph = (0, fusion_runtime_1.restoreExtraDirectives)(unifiedGraph); subschemaConfig = { name: (0, utils_1.getDirectiveExtensions)(unifiedGraph)?.transport?.[0]?.subgraph, schema: unifiedGraph, }; const transportEntryMap = {}; const additionalTypeDefs = []; const additionalResolvers = []; const stitchingDirectivesTransformer = (0, fusion_runtime_1.getStitchingDirectivesTransformerForSubschema)(); const onSubgraphExecute = (0, fusion_runtime_1.getOnSubgraphExecute)({ onSubgraphExecuteHooks, transports: config.transports, transportContext: configContext, transportEntryMap, getSubgraphSchema() { return unifiedGraph; }, transportExecutorStack: disposableStack, }); subschemaConfig = (0, fusion_runtime_1.handleFederationSubschema)({ subschemaConfig, transportEntryMap, additionalTypeDefs, additionalResolvers, stitchingDirectivesTransformer, onSubgraphExecute, }); // TODO: Find better alternative later unifiedGraph = (0, wrap_1.wrapSchema)(subschemaConfig); unifiedGraph = (0, graphql_yoga_1.mergeSchemas)({ assumeValid: true, assumeValidSDL: true, schemas: [unifiedGraph], typeDefs: [ (0, graphql_1.parse)(/* GraphQL */ ` type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! } scalar _Any union _Entity = ${Object.keys(subschemaConfig.merge || {}).join(' | ')} type _Service { sdl: String } `), ], resolvers: { Query: { _entities(_root, args, context, info) { if (Array.isArray(args.representations)) { return args.representations.map(representation => { const typeName = representation.__typename; const mergeConfig = subschemaConfig.merge[typeName]; const entryPoints = mergeConfig?.entryPoints || [mergeConfig]; const satisfiedEntryPoint = entryPoints.find(entryPoint => { if (entryPoint.selectionSet) { const selectionSet = (0, utils_2.parseSelectionSet)(entryPoint.selectionSet, { noLocation: true, }); return (0, utils_js_1.checkIfDataSatisfiesSelectionSet)(selectionSet, representation); } return true; }); if (satisfiedEntryPoint) { if (satisfiedEntryPoint.key) { return (0, utils_1.mapMaybePromise)((0, batch_delegate_1.batchDelegateToSchema)({ schema: subschemaConfig, fieldName: satisfiedEntryPoint.fieldName, key: satisfiedEntryPoint.key(representation), argsFromKeys: satisfiedEntryPoint.argsFromKeys, valuesFromResults: satisfiedEntryPoint.valuesFromResults, context, info, }), res => (0, utils_2.mergeDeep)([representation, res])); } if (satisfiedEntryPoint.args) { return (0, utils_1.mapMaybePromise)((0, delegate_1.delegateToSchema)({ schema: subschemaConfig, fieldName: satisfiedEntryPoint.fieldName, args: satisfiedEntryPoint.args(representation), context, info, }), res => (0, utils_2.mergeDeep)([representation, res])); } } return representation; }); } return []; }, _service() { return { sdl() { return (0, handleUnifiedGraphConfig_js_1.getUnifiedGraphSDL)(newUnifiedGraph); }, }; }, }, }, }); schemaChanged(unifiedGraph); return true; }); } unifiedGraphPlugin = { // @ts-expect-error PromiseLike is not compatible with Promise onRequestParse() { if (!subschemaConfig) { getSubschemaConfig$ ||= getSubschemaConfig(); return getSubschemaConfig$; } }, }; } else { let unifiedGraphFetcher; if ('supergraph' in config) { unifiedGraphFetcher = () => (0, handleUnifiedGraphConfig_js_1.handleUnifiedGraphConfig)(config.supergraph, configContext); } else if (('hive' in config && config.hive.endpoint) || cross_helpers_1.process.env.HIVE_CDN_ENDPOINT) { const cdnEndpoint = 'hive' in config ? config.hive.endpoint : cross_helpers_1.process.env.HIVE_CDN_ENDPOINT; const cdnKey = 'hive' in config ? config.hive.key : cross_helpers_1.process.env.HIVE_CDN_KEY; if (!cdnKey) { throw new Error('You must provide HIVE_CDN_KEY environment variables or `key` in the hive config'); } const fetcher = (0, apollo_1.createSupergraphSDLFetcher)({ endpoint: cdnEndpoint, key: cdnKey, }); unifiedGraphFetcher = () => fetcher().then(({ supergraphSdl }) => supergraphSdl); } else { const errorMessage = 'You must provide a supergraph schema in the `supergraph` config or point to a supergraph file with `--supergraph` parameter or `HIVE_CDN_ENDPOINT` environment variable or `./supergraph.graphql` file'; // Falls back to `./supergraph.graphql` by default unifiedGraphFetcher = () => { try { const res$ = (0, handleUnifiedGraphConfig_js_1.handleUnifiedGraphConfig)('./supergraph.graphql', configContext); if ('catch' in res$ && typeof res$.catch === 'function') { return res$.catch(e => { if (e.code === 'ENOENT') { throw new Error(errorMessage); } throw e; }); } return res$; } catch (e) { if (e.code === 'ENOENT') { throw new Error(errorMessage); } throw e; } }; } const hiveToken = 'hive' in config ? config.hive.token : cross_helpers_1.process.env.HIVE_REGISTRY_TOKEN; if (hiveToken) { registryPlugin = (0, plugin_hive_1.default)({ enabled: true, ...configContext, logger: configContext.logger.child('Hive'), ...('hive' in config ? config.hive : {}), token: hiveToken, }); } const unifiedGraphManager = new fusion_runtime_1.UnifiedGraphManager({ getUnifiedGraph: unifiedGraphFetcher, handleUnifiedGraph: opts => { // when handleUnifiedGraph is called, we're sure that the schema // _really_ changed, we can therefore confidently notify about the schema change schemaChanged(opts.unifiedGraph); return (0, fusion_runtime_1.handleFederationSupergraph)(opts); }, transports: config.transports, transportEntryAdditions: config.transportEntries, polling: config.polling, additionalResolvers: config.additionalResolvers, transportContext: configContext, onDelegateHooks, onSubgraphExecuteHooks, }); getSchema = () => unifiedGraphManager.getUnifiedGraph(); readinessChecker = () => (0, utils_1.mapMaybePromise)(unifiedGraphManager.getUnifiedGraph(), schema => !!schema); schemaInvalidator = () => unifiedGraphManager.invalidateUnifiedGraph(); contextBuilder = base => unifiedGraphManager.getContext(base); disposableStack.use(unifiedGraphManager); subgraphInformationHTMLRenderer = async () => { const htmlParts = []; let supergraphLoadedPlace = './supergraph.graphql'; if ('hive' in config && config.hive.endpoint) { supergraphLoadedPlace = 'Hive CDN <br>' + config.hive.endpoint; } else if ('supergraph' in config) { if (typeof config.supergraph === 'function') { const fnName = config.supergraph.name || ''; supergraphLoadedPlace = `a custom loader ${fnName}`; } else if (typeof config.supergraph === 'string') { supergraphLoadedPlace = config.supergraph; } } let loaded = false; let loadError; try { // TODO: Workaround for the issue // When you go to landing page, then GraphiQL, GW stops working const schema = await getSchema(); schemaChanged(schema); loaded = true; } catch (e) { loaded = false; loadError = e; } if (loaded) { htmlParts.push(`<h3>Supergraph Status: Loaded ✅</h3>`); htmlParts.push(`<p><strong>Source: </strong> <i>${supergraphLoadedPlace}</i></p>`); htmlParts.push(`<table>`); htmlParts.push(`<tr><th>Subgraph</th><th>Transport</th><th>Location</th></tr>`); for (const subgraphName in unifiedGraphManager._transportEntryMap) { const transportEntry = unifiedGraphManager._transportEntryMap[subgraphName]; htmlParts.push(`<tr>`); htmlParts.push(`<td>${subgraphName}</td>`); htmlParts.push(`<td>${transportEntry.kind}</td>`); htmlParts.push(`<td><a href="${transportEntry.location}">${transportEntry.location}</a></td>`); htmlParts.push(`</tr>`); } htmlParts.push(`</table>`); } else { htmlParts.push(`<h3>Status: Failed ❌</h3>`); htmlParts.push(`<p><strong>Source: </strong> <i>${supergraphLoadedPlace}</i></p>`); htmlParts.push(`<h3>Error:</h3>`); htmlParts.push(`<pre>${loadError.stack}</pre>`); } return `<section class="supergraph-information">${htmlParts.join('')}</section>`; }; } const readinessCheckPlugin = (0, graphql_yoga_1.useReadinessCheck)({ endpoint: readinessCheckEndpoint, // @ts-expect-error PromiseLike is not compatible with Promise check: readinessChecker, }); const defaultMeshPlugin = { onFetch({ setFetchFn }) { setFetchFn(fetchAPI.fetch); }, onPluginInit({ plugins }) { onFetchHooks.splice(0, onFetchHooks.length); onSubgraphExecuteHooks.splice(0, onSubgraphExecuteHooks.length); onDelegateHooks.splice(0, onDelegateHooks.length); for (const plugin of plugins) { if (plugin.onFetch) { onFetchHooks.push(plugin.onFetch); } if (plugin.onSubgraphExecute) { onSubgraphExecuteHooks.push(plugin.onSubgraphExecute); } // @ts-expect-error For backward compatibility if (plugin.onDelegate) { // @ts-expect-error For backward compatibility onDelegateHooks.push(plugin.onDelegate); } if ((0, utils_1.isDisposable)(plugin)) { disposableStack.use(plugin); } } }, }; let graphiqlOptionsOrFactory; if (config.graphiql == null || config.graphiql === true) { graphiqlOptionsOrFactory = { title: 'GraphiQL Mesh', }; } else if (config.graphiql === false) { graphiqlOptionsOrFactory = false; } else if (typeof config.graphiql === 'object') { graphiqlOptionsOrFactory = { title: 'GraphiQL Mesh', ...config.graphiql, }; } else if (typeof config.graphiql === 'function') { const userGraphiqlFactory = config.graphiql; // @ts-expect-error PromiseLike is not compatible with Promise graphiqlOptionsOrFactory = function graphiqlOptionsFactoryForMesh(...args) { const options = userGraphiqlFactory(...args); return (0, utils_1.mapMaybePromise)(options, resolvedOpts => { if (resolvedOpts === false) { return false; } if (resolvedOpts === true) { return { title: 'GraphiQL Mesh', }; } return { title: 'GraphiQL Mesh', ...resolvedOpts, }; }); }; } let landingPageRenderer; if (config.landingPage == null || config.landingPage === true) { landingPageRenderer = async function meshLandingPageRenderer(opts) { return new opts.fetchAPI.Response(landing_page_html_js_1.default .replace(/__GRAPHIQL_LINK__/g, opts.graphqlEndpoint) .replace(/__REQUEST_PATH__/g, opts.url.pathname) .replace(/__SUBGRAPH_HTML__/g, await subgraphInformationHTMLRenderer()), { status: 200, statusText: 'OK', headers: { 'Content-Type': 'text/html', }, }); }; } else if (typeof config.landingPage === 'function') { landingPageRenderer = config.landingPage; } else if (config.landingPage === false) { landingPageRenderer = false; } const yoga = (0, graphql_yoga_1.createYoga)({ fetchAPI: config.fetchAPI, logging: logger, plugins: [ defaultMeshPlugin, unifiedGraphPlugin, readinessCheckPlugin, registryPlugin, (0, useChangingSchema_js_1.useChangingSchema)(getSchema, cb => (schemaChanged = cb)), (0, useCompleteSubscriptionsOnDispose_js_1.useCompleteSubscriptionsOnDispose)(disposableStack), (0, useCompleteSubscriptionsOnSchemaChange_js_1.useCompleteSubscriptionsOnSchemaChange)(), (0, useRequestId_js_1.useRequestId)(), (0, useSubgraphExecuteDebug_js_1.useSubgraphExecuteDebug)(configContext), (0, useFetchDebug_js_1.useFetchDebug)(configContext), ...(config.plugins?.(configContext) || []), ], // @ts-expect-error PromiseLike is not compatible with Promise context({ request, params, ...rest }) { // TODO: I dont like this cast, but it's necessary const { req, connectionParams } = rest; const baseContext = { ...configContext, request, params, headers: // Maybe Node-like environment req?.headers ? (0, utils_1.getHeadersObj)(req.headers) : // Fetch environment request?.headers ? (0, utils_1.getHeadersObj)(request.headers) : // Unknown environment {}, connectionParams, }; if (contextBuilder) { return contextBuilder(baseContext); } return baseContext; }, cors: config.cors, graphiql: graphiqlOptionsOrFactory, batching: config.batching, graphqlEndpoint: config.graphqlEndpoint, maskedErrors: config.maskedErrors, healthCheckEndpoint: config.healthCheckEndpoint || '/healthcheck', landingPage: landingPageRenderer, }); fetchAPI ||= yoga.fetchAPI; Object.defineProperties(yoga, { invalidateUnifiedGraph: { value: schemaInvalidator, configurable: true, }, }); return (0, utils_1.makeAsyncDisposable)(yoga, () => disposableStack.disposeAsync()); }