@graphql-mesh/runtime
Version:
323 lines (322 loc) • 12.9 kB
JavaScript
import { execute, getOperationAST, specifiedRules, validate, } from 'graphql';
import { envelop, useEngine, useExtendContext, useSchema } from '@envelop/core';
import { OneOfInputObjectsRule, useExtendedValidation } from '@envelop/extended-validation';
import { useGraphQlJit } from '@envelop/graphql-jit';
import { process } from '@graphql-mesh/cross-helpers';
import { applySchemaTransforms, DefaultLogger, getHeadersObj, groupTransforms, parseWithCache, PubSub, } from '@graphql-mesh/utils';
import { Subschema } from '@graphql-tools/delegate';
import { getRootTypeMap, inspect, isAsyncIterable, isPromise, mapAsyncIterator, memoize1, } from '@graphql-tools/utils';
import { fetch as defaultFetchFn } from '@whatwg-node/fetch';
import { MESH_CONTEXT_SYMBOL } from './constants.js';
import { getInContextSDK } from './in-context-sdk.js';
import { useSubschema } from './useSubschema.js';
import { isStreamOperation, iterateAsync } from './utils.js';
const memoizedGetEnvelopedFactory = memoize1(function getEnvelopedFactory(plugins) {
return envelop({
plugins,
});
});
const memoizedGetOperationType = memoize1((document) => {
const operationAST = getOperationAST(document, undefined);
if (!operationAST) {
throw new Error('Must provide document with a valid operation');
}
return operationAST.operation;
});
export function wrapFetchWithPlugins(plugins) {
const onFetchHooks = [];
for (const plugin of plugins) {
if (plugin?.onFetch != null) {
onFetchHooks.push(plugin.onFetch);
}
}
return function wrappedFetchFn(url, options, context, info) {
if (url != null && typeof url !== 'string') {
throw new TypeError(`First parameter(url) of 'fetch' must be a string, got ${inspect(url)}`);
}
if (options != null && typeof options !== 'object') {
throw new TypeError(`Second parameter(options) of 'fetch' must be an object, got ${inspect(options)}`);
}
if (context != null && typeof context !== 'object') {
throw new TypeError(`Third parameter(context) of 'fetch' must be an object, got ${inspect(context)}`);
}
if (info != null && typeof info !== 'object') {
throw new TypeError(`Fourth parameter(info) of 'fetch' must be an object, got ${inspect(info)}`);
}
let fetchFn;
const doneHooks = [];
function setFetchFn(newFetchFn) {
fetchFn = newFetchFn;
}
const result$ = iterateAsync(onFetchHooks, onFetch => onFetch({
fetchFn,
setFetchFn,
url,
options,
context,
info,
}), doneHooks);
function handleIterationResult() {
const response$ = fetchFn(url, options, context, info);
if (doneHooks.length === 0) {
return response$;
}
if (isPromise(response$)) {
return response$.then(response => handleOnFetchDone(response, doneHooks));
}
return handleOnFetchDone(response$, doneHooks);
}
if (isPromise(result$)) {
return result$.then(handleIterationResult);
}
return handleIterationResult();
};
}
// 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('🕸️ Mesh'), 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,
[MESH_CONTEXT_SYMBOL]: true,
})),
{
onFetch({ setFetchFn }) {
setFetchFn(fetchFn);
},
},
...additionalEnvelopPlugins,
];
const wrappedFetchFn = wrapFetchWithPlugins(initialPluginList);
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({
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.push({
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.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${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 subschema;
if (unifiedSubschema.executor != null || unifiedSubschema.transforms?.length) {
subschema = new Subschema(unifiedSubschema);
}
const plugins = [
useEngine({
execute,
validate,
parse: parseWithCache,
specifiedRules,
}),
...(subschema
? [useSubschema(new Subschema(unifiedSubschema))]
: [
useSchema(unifiedSubschema.schema),
useGraphQlJit({
customJSONSerializer: !unifiedSubschema.schema.getType('BigInt'),
disableLeafSerialization: true,
}, {
enableIf(args) {
return !isStreamOperation(args.document);
},
}),
]),
useExtendContext(() => {
if (!inContextSDK) {
const onDelegateHooks = [];
for (const plugin of initialPluginList) {
if (plugin?.onDelegate != null) {
onDelegateHooks.push(plugin.onDelegate);
}
}
inContextSDK = getInContextSDK(subschema ? subschema.transformedSchema : unifiedSubschema.schema, rawSources, logger, onDelegateHooks);
}
return inContextSDK;
}),
useExtendedValidation({
rules: [OneOfInputObjectsRule],
}),
...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 executeFn = memoizedGetOperationType(document) === 'subscription' ? subscribe : execute;
const contextValue$ = contextFactory(contextValue);
if (isPromise(contextValue$)) {
return contextValue$.then(contextValue => executeFn({
schema,
document,
contextValue,
rootValue,
variableValues: variableValues,
operationName,
}));
}
return executeFn({
schema,
document,
contextValue: contextValue$,
rootValue,
variableValues: variableValues,
operationName,
});
};
}
function sdkRequesterFactory(globalContext) {
const executor = createExecutor(globalContext);
return function sdkRequester(...args) {
const result$ = executor(...args);
if (isPromise(result$)) {
return result$.then(handleExecutorResultForSdk);
}
return handleExecutorResultForSdk(result$);
};
}
function meshDestroy() {
return pubsub.publish('destroy', undefined);
}
return {
get schema() {
return subschema ? subschema.transformedSchema : unifiedSubschema.schema;
},
rawSources,
cache,
pubsub,
destroy: meshDestroy,
logger,
plugins,
get getEnveloped() {
return memoizedGetEnvelopedFactory(plugins);
},
createExecutor,
get execute() {
return createExecutor();
},
get subscribe() {
return createExecutor();
},
sdkRequesterFactory,
};
}
function handleExecutorResultForSdk(result) {
if (isAsyncIterable(result)) {
return mapAsyncIterator(result, extractDataOrThrowErrors);
}
return extractDataOrThrowErrors(result);
}
function extractDataOrThrowErrors(result) {
if (result.errors) {
if (result.errors.length === 1) {
throw result.errors[0];
}
throw new AggregateError(result.errors);
}
return result.data;
}
function handleOnFetchDone(response, onFetchDoneHooks) {
function setResponse(newResponse) {
response = newResponse;
}
const result$ = iterateAsync(onFetchDoneHooks, onFetchDone => onFetchDone({
response,
setResponse,
}));
if (isPromise(result$)) {
return result$.then(() => response);
}
return response;
}