@graphql-mesh/serve-runtime
Version:
487 lines (485 loc) • 23.4 kB
JavaScript
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());
}
;