UNPKG

@graphql-mesh/runtime

Version:
430 lines (424 loc) 19.7 kB
import { getNamedType, isLeafType, Kind, introspectionFromSchema, getOperationAST } from 'graphql'; import { printWithCache, applySchemaTransforms, applyRequestTransforms, applyResultTransforms, PubSub, DefaultLogger, groupTransforms, getHeadersObj, parseWithCache } from '@graphql-mesh/utils'; import { parseSelectionSet, isDocumentNode, isAsyncIterable, getOperationASTFromRequest, memoize1, mapAsyncIterator as mapAsyncIterator$1, AggregateError } from '@graphql-tools/utils'; import { mapAsyncIterator, envelop, useExtendContext, enableIf } from '@envelop/core'; import { useExtendedValidation, OneOfInputObjectsRule } from '@envelop/extended-validation'; import { batchDelegateToSchema } from '@graphql-tools/batch-delegate'; import { delegateToSchema, createDefaultExecutor } from '@graphql-tools/delegate'; import { WrapQuery } from '@graphql-tools/wrap'; const MESH_CONTEXT_SYMBOL = Symbol('isMeshContext'); const MESH_API_CONTEXT_SYMBOL = Symbol('isMeshAPIContext'); async function getInContextSDK(unifiedSchema, rawSources, logger) { const inContextSDK = {}; const sourceMap = unifiedSchema.extensions.sourceMap; for (const rawSource of rawSources) { const rawSourceLogger = logger.child(`${rawSource.name}`); const rawSourceContext = { rawSource, [MESH_API_CONTEXT_SYMBOL]: true, }; // TODO: Somehow rawSource reference got lost in somewhere let rawSourceSubSchemaConfig; const stitchingInfo = unifiedSchema.extensions.stitchingInfo; if (stitchingInfo) { for (const [subschemaConfig, subschema] of stitchingInfo.subschemaMap) { if (subschemaConfig.name === rawSource.name) { rawSourceSubSchemaConfig = subschema; break; } } } else { rawSourceSubSchemaConfig = rawSource; } // If there is a single source, there is no unifiedSchema const transformedSchema = sourceMap.get(rawSource); const rootTypes = { query: transformedSchema.getQueryType(), mutation: transformedSchema.getMutationType(), subscription: transformedSchema.getSubscriptionType(), }; rawSourceLogger.debug(`Generating In Context SDK`); for (const operationType in rootTypes) { const rootType = rootTypes[operationType]; if (rootType) { rawSourceContext[rootType.name] = {}; const rootTypeFieldMap = rootType.getFields(); for (const fieldName in rootTypeFieldMap) { const rootTypeField = rootTypeFieldMap[fieldName]; const inContextSdkLogger = rawSourceLogger.child(`InContextSDK.${rootType.name}.${fieldName}`); const namedReturnType = getNamedType(rootTypeField.type); const shouldHaveSelectionSet = !isLeafType(namedReturnType); rawSourceContext[rootType.name][fieldName] = ({ root, args, context, info = { fieldName, fieldNodes: [], returnType: namedReturnType, parentType: rootType, path: { typename: rootType.name, key: fieldName, prev: undefined, }, schema: transformedSchema, fragments: {}, rootValue: root, operation: { kind: Kind.OPERATION_DEFINITION, operation: operationType, selectionSet: { kind: Kind.SELECTION_SET, selections: [], }, }, variableValues: {}, cacheControl: { setCacheHint: () => { }, cacheHint: {}, }, }, selectionSet, key, argsFromKeys, valuesFromResults, }) => { inContextSdkLogger.debug(`Called with`, { args, key, }); const commonDelegateOptions = { schema: rawSourceSubSchemaConfig, rootValue: root, operation: operationType, fieldName, context, transformedSchema, info, }; // If there isn't an extraction of a value if (typeof selectionSet !== 'function') { commonDelegateOptions.returnType = rootTypeField.type; } if (shouldHaveSelectionSet) { let selectionCount = 0; for (const fieldNode of info.fieldNodes) { if (fieldNode.selectionSet != null) { selectionCount += fieldNode.selectionSet.selections.length; } } if (selectionCount === 0) { if (!selectionSet) { throw new Error(`You have to provide 'selectionSet' for context.${rawSource.name}.${rootType.name}.${fieldName}`); } commonDelegateOptions.info = { ...info, fieldNodes: [ { ...info.fieldNodes[0], selectionSet: { kind: Kind.SELECTION_SET, selections: [ { kind: Kind.FIELD, name: { kind: Kind.NAME, value: '__typename', }, }, ], }, }, ...info.fieldNodes.slice(1), ], }; } } if (key && argsFromKeys) { const batchDelegationOptions = { ...commonDelegateOptions, key, argsFromKeys, valuesFromResults, }; if (selectionSet) { const selectionSetFactory = normalizeSelectionSetParamOrFactory(selectionSet); const path = [fieldName]; const wrapQueryTransform = new WrapQuery(path, selectionSetFactory, identical); batchDelegationOptions.transforms = [wrapQueryTransform]; } return batchDelegateToSchema(batchDelegationOptions); } else { const regularDelegateOptions = { ...commonDelegateOptions, args, }; if (selectionSet) { const selectionSetFactory = normalizeSelectionSetParamOrFactory(selectionSet); const path = [fieldName]; const wrapQueryTransform = new WrapQuery(path, selectionSetFactory, valuesFromResults || identical); regularDelegateOptions.transforms = [wrapQueryTransform]; } return delegateToSchema(regularDelegateOptions); } }; } } } inContextSDK[rawSource.name] = rawSourceContext; } return inContextSDK; } function normalizeSelectionSetParam(selectionSetParam) { if (typeof selectionSetParam === 'string') { return parseSelectionSet(selectionSetParam); } if (isDocumentNode(selectionSetParam)) { return parseSelectionSet(printWithCache(selectionSetParam)); } return selectionSetParam; } function normalizeSelectionSetParamOrFactory(selectionSetParamOrFactory) { return function getSelectionSet(subtree) { if (typeof selectionSetParamOrFactory === 'function') { const selectionSetParam = selectionSetParamOrFactory(subtree); return normalizeSelectionSetParam(selectionSetParam); } else { return normalizeSelectionSetParam(selectionSetParamOrFactory); } }; } function identical(val) { return val; } function getExecuteFnByArgs(args, subschema) { var _a, _b; const transformationContext = {}; const originalRequest = { document: args.document, variables: args.variableValues, operationName: (_a = args.operationName) !== null && _a !== void 0 ? _a : undefined, rootValue: args.rootValue, context: args.contextValue, }; const operationAST = getOperationASTFromRequest(originalRequest); const delegationContext = { subschema, subschemaConfig: subschema, targetSchema: args.schema, operation: operationAST.operation, fieldName: '', context: args.contextValue, rootValue: args.rootValue, transforms: subschema.transforms, transformedSchema: args.schema, skipTypeMerging: true, returnType: {}, // Might not work }; const executor = (_b = subschema.executor) !== null && _b !== void 0 ? _b : createDefaultExecutor(subschema.schema); return async function subschemaExecute() { const transformedRequest = applyRequestTransforms(originalRequest, delegationContext, transformationContext, subschema.transforms); const originalResult = await executor(transformedRequest); if (isAsyncIterable(originalResult)) { return mapAsyncIterator(originalResult, singleResult => applyResultTransforms(singleResult, delegationContext, transformationContext, subschema.transforms)); } const transformedResult = applyResultTransforms(originalResult, delegationContext, transformationContext, subschema.transforms); return transformedResult; }; } // Creates an envelop plugin to execute a subschema inside Envelop function useSubschema(subschema) { const transformedSchema = applySchemaTransforms(subschema.schema, subschema, subschema.schema, subschema.transforms); const plugin = { onPluginInit({ setSchema }) { setSchema(transformedSchema); }, onExecute({ args, setExecuteFn, setResultAndStopExecution }) { // TODO: This is a hack to make introspection work properly if (args.operationName === 'IntrospectionQuery') { setResultAndStopExecution({ data: introspectionFromSchema(args.schema), }); } setExecuteFn(getExecuteFnByArgs(args, subschema)); }, onSubscribe({ args, setSubscribeFn }) { setSubscribeFn(getExecuteFnByArgs(args, subschema)); }, }; return { transformedSchema, plugin, }; } const memoizedGetOperationType = memoize1((document) => { const operationAST = getOperationAST(document, undefined); if (!operationAST) { throw new Error('Must provide document with a valid operation'); } return operationAST.operation; }); const memoizedGetEnvelopedFactory = memoize1(function getEnvelopedFactory(plugins) { const getEnveloped = envelop({ plugins }); return memoize1(function getEnvelopedByContext(initialContext) { return getEnveloped(initialContext); }); }); async function getMesh(options) { const rawSources = []; const { pubsub = new PubSub(), cache, logger = new DefaultLogger('🕸️ Mesh'), additionalEnvelopPlugins = [], sources, merger, additionalResolvers = [], additionalTypeDefs = [], transforms = [], } = options; const getMeshLogger = logger.child('GetMesh'); getMeshLogger.debug(`Getting subschemas from source handlers`); let failed = false; await Promise.allSettled(sources.map(async (apiSource) => { const apiName = apiSource.name; const sourceLogger = logger.child(apiName); sourceLogger.debug(`Generating the schema`); try { const source = await apiSource.handler.getMeshSource(); 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 === null || wrapTransforms === void 0 ? void 0 : wrapTransforms.length) && (noWrapTransforms === null || noWrapTransforms === void 0 ? void 0 : noWrapTransforms.length)) { sourceLogger.debug(`${noWrapTransforms.length} bare transforms found and applying`); apiSchema = applySchemaTransforms(apiSchema, source, null, noWrapTransforms); } else { transforms = apiSource.transforms; } rawSources.push({ name: apiName, schema: apiSchema, executor: source.executor, transforms, contextVariables: source.contextVariables || {}, handler: apiSource.handler, batch: 'batch' in source ? source.batch : true, merge: apiSource.merge, }); } catch (e) { sourceLogger.error(`Failed to generate the schema`, e); failed = true; } })); if (failed) { throw new Error(`Schemas couldn't be generated successfully. Check for the logs by running Mesh 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 || []; unifiedSubschema.transforms.push(...transforms); let inContextSDK$; const { plugin: subschemaPlugin, transformedSchema: finalSchema } = useSubschema(unifiedSubschema); finalSchema.extensions = unifiedSubschema.schema.extensions; const plugins = [ subschemaPlugin, // TODO: Not a good practise to expect users to be a Yoga user useExtendContext(({ request, req }) => { // Maybe Node-like environment if (req === null || req === void 0 ? void 0 : req.headers) { return { headers: req.headers, }; } // Fetch environment if (request === null || request === void 0 ? void 0 : request.headers) { return { headers: getHeadersObj(request.headers), }; } return {}; }), useExtendContext(() => ({ pubsub, cache, logger, [MESH_CONTEXT_SYMBOL]: true, })), useExtendContext(() => { if (!inContextSDK$) { inContextSDK$ = getInContextSDK(finalSchema, rawSources, logger); } return inContextSDK$; }), enableIf(!!finalSchema.getDirective('oneOf'), () => useExtendedValidation({ rules: [OneOfInputObjectsRule], })), { onParse({ setParseFn }) { setParseFn(parseWithCache); }, }, ...additionalEnvelopPlugins, ]; const EMPTY_ROOT_VALUE = {}; const EMPTY_CONTEXT_VALUE = {}; const EMPTY_VARIABLES_VALUE = {}; async function meshExecute(documentOrSDL, variableValues = EMPTY_VARIABLES_VALUE, contextValue = EMPTY_CONTEXT_VALUE, rootValue = EMPTY_ROOT_VALUE, operationName) { const getEnveloped = memoizedGetEnvelopedFactory(plugins); const { execute, contextFactory, parse } = getEnveloped(contextValue); return execute({ document: typeof documentOrSDL === 'string' ? parse(documentOrSDL) : documentOrSDL, contextValue: await contextFactory(), rootValue, variableValues: variableValues, schema: unifiedSubschema.schema, operationName, }); } async function meshSubscribe(documentOrSDL, variableValues = EMPTY_VARIABLES_VALUE, contextValue = EMPTY_CONTEXT_VALUE, rootValue = EMPTY_ROOT_VALUE, operationName) { const getEnveloped = memoizedGetEnvelopedFactory(plugins); const { subscribe, contextFactory, parse } = getEnveloped(contextValue); return subscribe({ document: typeof documentOrSDL === 'string' ? parse(documentOrSDL) : documentOrSDL, contextValue: await contextFactory(), rootValue, variableValues: variableValues, schema: unifiedSubschema.schema, operationName, }); } function sdkRequesterFactory(globalContext) { return async function meshSdkRequester(document, variables, contextValue) { const executeFn = memoizedGetOperationType(document) === 'subscription' ? meshSubscribe : meshExecute; const result = await executeFn(document, variables, { ...globalContext, ...contextValue, }); if (isAsyncIterable(result)) { return mapAsyncIterator$1(result, extractDataOrThrowErrors); } return extractDataOrThrowErrors(result); }; } return { execute: meshExecute, subscribe: meshSubscribe, schema: finalSchema, rawSources, cache, pubsub, destroy() { return pubsub.publish('destroy', undefined); }, logger, plugins, get getEnveloped() { return memoizedGetEnvelopedFactory(plugins); }, sdkRequesterFactory, }; } function extractDataOrThrowErrors(result) { if (result.errors) { if (result.errors.length === 1) { throw result.errors[0]; } throw new AggregateError(result.errors); } return result.data; } export { getMesh, useSubschema };