@graphql-mesh/runtime
Version:
430 lines (424 loc) • 19.7 kB
JavaScript
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 };