UNPKG

@graphql-mesh/runtime

Version:
277 lines (276 loc) • 11.8 kB
import { getOperationAST, specifiedRules, validate } from 'graphql'; import { envelop, useEngine, useExtendContext, useSchema } from '@envelop/core'; import { process } from '@graphql-mesh/cross-helpers'; import { applySchemaTransforms, DefaultLogger, getHeadersObj, getInContextSDK, groupTransforms, makeDisposable, parseWithCache, PubSub, wrapFetchWithHooks, } from '@graphql-mesh/utils'; import { normalizedExecutor } from '@graphql-tools/executor'; import { createGraphQLError, getRootTypeMap, isAsyncIterable, mapAsyncIterator, memoize1, } from '@graphql-tools/utils'; import { wrapSchema } from '@graphql-tools/wrap'; import { fetch as defaultFetchFn } from '@whatwg-node/fetch'; import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import { MESH_CONTEXT_SYMBOL } from './constants.js'; import { getOriginalError } from './utils.js'; const memoizedGetEnvelopedFactory = memoize1(function getEnvelopedFactory(plugins) { return envelop({ plugins, }); }); export function wrapFetchWithPlugins(plugins) { const onFetchHooks = []; for (const plugin of plugins) { if (plugin?.onFetch != null) { onFetchHooks.push(plugin.onFetch); } } return wrapFetchWithHooks(onFetchHooks); } // Use in-context-sdk for tracing function createProxyingResolverFactory(apiName, rootTypeMap) { return function createProxyingResolver({ operation }) { const rootType = rootTypeMap.get(operation); return function proxyingResolver(root, args, context, info) { if (!context?.[apiName]?.[rootType.name]?.[info.fieldName]) { throw new Error(`${info.fieldName} couldn't find in ${rootType.name} of ${apiName} as a ${operation}`); } return context[apiName][rootType.name][info.fieldName]({ root, args, context, info }); }; }; } export async function getMesh(options) { const rawSources = []; const { pubsub = new PubSub(), cache, logger = new DefaultLogger(''), additionalEnvelopPlugins = [], sources, merger, additionalResolvers = [], additionalTypeDefs = [], transforms = [], fetchFn = defaultFetchFn, } = options; const getMeshLogger = logger.child('GetMesh'); getMeshLogger.debug(`Getting subschemas from source handlers`); let failed = false; const initialPluginList = [ // TODO: Not a good practise to expect users to be a Yoga user useExtendContext(({ request, req, connectionParams, }) => { // Maybe Node-like environment if (req?.headers) { return { headers: getHeadersObj(req.headers), connectionParams, }; } // Fetch environment if (request?.headers) { return { headers: getHeadersObj(request.headers), connectionParams, }; } return {}; }), useExtendContext(() => ({ pubsub, cache, logger, fetch: wrappedFetchFn, [MESH_CONTEXT_SYMBOL]: true, })), { onFetch({ setFetchFn }) { setFetchFn(fetchFn); }, }, ...additionalEnvelopPlugins, ]; const wrappedFetchFn = wrapFetchWithPlugins(initialPluginList); await Promise.allSettled(sources.map(async (apiSource, index) => { const apiName = apiSource.name; const sourceLogger = logger.child({ source: apiName }); sourceLogger.debug(`Generating the schema`); try { const source = await apiSource.handler.getMeshSource({ fetchFn: wrappedFetchFn, }); sourceLogger.debug(`The schema has been generated successfully`); let apiSchema = source.schema; sourceLogger.debug(`Analyzing transforms`); let transforms; const { wrapTransforms, noWrapTransforms } = groupTransforms(apiSource.transforms); if (!wrapTransforms?.length && noWrapTransforms?.length) { sourceLogger.debug(`${noWrapTransforms.length} bare transforms found and applying`); apiSchema = applySchemaTransforms(apiSchema, source, null, noWrapTransforms); } else { transforms = apiSource.transforms; } const rootTypeMap = getRootTypeMap(apiSchema); rawSources[index] = { name: apiName, schema: apiSchema, executor: source.executor, transforms, contextVariables: source.contextVariables || {}, handler: apiSource.handler, batch: 'batch' in source ? source.batch : true, merge: source.merge, createProxyingResolver: createProxyingResolverFactory(apiName, rootTypeMap), }; } catch (e) { sourceLogger.debug(e); sourceLogger.error(`Failed to generate the schema for the source\n ${e.message}`); failed = true; } })); if (failed) { throw new Error(`Schemas couldn't be generated successfully. Check for the logs by running Mesh${process.env.DEBUG == null ? ' with DEBUG=1 environmental variable to get more verbose output' : ''}.`); } getMeshLogger.debug(`Schemas have been generated by the source handlers`); getMeshLogger.debug(`Merging schemas using the defined merging strategy.`); const unifiedSubschema = await merger.getUnifiedSchema({ rawSources, typeDefs: additionalTypeDefs, resolvers: additionalResolvers, }); unifiedSubschema.transforms = unifiedSubschema.transforms || []; for (const rootLevelTransform of transforms) { if (rootLevelTransform.noWrap) { if (rootLevelTransform.transformSchema) { unifiedSubschema.schema = rootLevelTransform.transformSchema(unifiedSubschema.schema, unifiedSubschema); } } else { unifiedSubschema.transforms.push(rootLevelTransform); } } let inContextSDK; let schema = unifiedSubschema.schema; if (unifiedSubschema.executor != null || unifiedSubschema.transforms?.length) { schema = wrapSchema(unifiedSubschema); } const plugins = [ useEngine({ execute: normalizedExecutor, subscribe: normalizedExecutor, validate, parse: parseWithCache, specifiedRules, }), useSchema(schema), useExtendContext(() => { if (!inContextSDK) { const onDelegateHooks = []; for (const plugin of initialPluginList) { if (plugin?.onDelegate != null) { onDelegateHooks.push(plugin.onDelegate); } } inContextSDK = getInContextSDK(schema, rawSources, logger, onDelegateHooks); } return inContextSDK; }), { onExecute() { return { onExecuteDone({ result, setResult }) { if (result.errors) { // Print errors with stack trace in development if (process.env.NODE_ENV === 'production') { for (const error of result.errors) { const origError = getOriginalError(error); if (origError) { logger.error(origError); } } } else { setResult({ ...result, errors: result.errors.map(error => { const origError = getOriginalError(error); if (origError) { return createGraphQLError(error.message, { ...error, extensions: { ...error.extensions, originalError: { name: origError.name, message: origError.message, stack: origError.stack, }, }, }); } return error; }), }); } } }, }; }, }, ...initialPluginList, ]; const EMPTY_ROOT_VALUE = {}; const EMPTY_CONTEXT_VALUE = {}; const EMPTY_VARIABLES_VALUE = {}; function createExecutor(globalContext = EMPTY_CONTEXT_VALUE) { const getEnveloped = memoizedGetEnvelopedFactory(plugins); const { schema, parse, execute, subscribe, contextFactory } = getEnveloped(globalContext); return function meshExecutor(documentOrSDL, variableValues = EMPTY_VARIABLES_VALUE, contextValue = EMPTY_CONTEXT_VALUE, rootValue = EMPTY_ROOT_VALUE, operationName) { const document = typeof documentOrSDL === 'string' ? parse(documentOrSDL) : documentOrSDL; const operationAST = getOperationAST(document, operationName); if (!operationAST) { throw new Error(`Cannot execute a request without a valid operation.`); } const isSubscription = operationAST.operation === 'subscription'; const executeFn = isSubscription ? subscribe : execute; return handleMaybePromise(() => contextFactory(contextValue), contextValue => executeFn({ schema, document, contextValue, rootValue, variableValues, operationName, })); }; } function sdkRequesterFactory(globalContext) { const executor = createExecutor(globalContext); return function sdkRequester(...args) { return handleMaybePromise(() => executor(...args), function handleExecutorResultForSdk(result) { if (isAsyncIterable(result)) { return mapAsyncIterator(result, extractDataOrThrowErrors); } return extractDataOrThrowErrors(result); }); }; } function meshDestroy() { return pubsub.publish('destroy', undefined); } return makeDisposable({ schema, rawSources, cache, pubsub, destroy: meshDestroy, logger, plugins, get getEnveloped() { return memoizedGetEnvelopedFactory(plugins); }, createExecutor, get execute() { return createExecutor(); }, get subscribe() { return createExecutor(); }, sdkRequesterFactory, }, meshDestroy); } function extractDataOrThrowErrors(result) { if (result.errors) { if (result.errors.length === 1) { throw result.errors[0]; } throw new AggregateError(result.errors); } return result.data; }