diff --git a/.changeset/light-badgers-tan.md b/.changeset/light-badgers-tan.md new file mode 100644 index 000000000..b23c5383a --- /dev/null +++ b/.changeset/light-badgers-tan.md @@ -0,0 +1,5 @@ +--- +'@graphql-mesh/fusion-runtime': patch +--- + +Refactor to make it easier to replace the supergraph execution diff --git a/packages/fusion-runtime/src/executor.ts b/packages/fusion-runtime/src/executor.ts index 672e12f76..924e26144 100644 --- a/packages/fusion-runtime/src/executor.ts +++ b/packages/fusion-runtime/src/executor.ts @@ -1,5 +1,6 @@ import { createDefaultExecutor } from '@graphql-mesh/transport-common'; import { + Executor, isAsyncIterable, mapAsyncIterator, mapMaybePromise, @@ -30,17 +31,33 @@ export function getExecutorForUnifiedGraph( return mapMaybePromise( unifiedGraphManager.getContext(execReq.context), (context) => { + function handleExecutor(executor: Executor) { + opts?.transportContext?.logger?.debug( + 'Executing request on unified graph', + print(execReq.document), + ); + return executor({ + ...execReq, + context, + }); + } return mapMaybePromise( - unifiedGraphManager.getUnifiedGraph(), - (unifiedGraph) => { - opts?.transportContext?.logger?.debug( - 'Executing request on unified graph', - print(execReq.document), - ); - return createDefaultExecutor(unifiedGraph)({ - ...execReq, - context, - }); + unifiedGraphManager.getExecutor(), + (executor) => { + if (!executor) { + return mapMaybePromise( + unifiedGraphManager.getUnifiedGraph(), + (unifiedGraph) => { + opts?.transportContext?.logger?.debug( + 'Executing request on unified graph', + print(execReq.document), + ); + executor = createDefaultExecutor(unifiedGraph); + return handleExecutor(executor); + }, + ); + } + return handleExecutor(executor); }, ); }, diff --git a/packages/fusion-runtime/src/federation/subgraph.ts b/packages/fusion-runtime/src/federation/subgraph.ts index d922acdf7..e48cedd43 100644 --- a/packages/fusion-runtime/src/federation/subgraph.ts +++ b/packages/fusion-runtime/src/federation/subgraph.ts @@ -1,4 +1,3 @@ -import type { TransportEntry } from '@graphql-mesh/transport-common'; import { YamlConfig } from '@graphql-mesh/types'; import type { MergedTypeConfig, @@ -40,13 +39,16 @@ import { typeFromAST, visit, } from 'graphql'; -import { compareSubgraphNames, type getOnSubgraphExecute } from '../utils'; +import { + compareSubgraphNames, + TransportEntry, + type getOnSubgraphExecute, +} from '../utils'; export interface HandleFederationSubschemaOpts { subschemaConfig: SubschemaConfig & { endpoint?: string }; + unifiedGraphDirectives?: Record; realSubgraphNameMap?: Map; - schemaDirectives?: Record; - transportEntryMap: Record; additionalTypeDefs: TypeSource[]; stitchingDirectivesTransformer: ( subschemaConfig: SubschemaConfig, @@ -56,9 +58,8 @@ export interface HandleFederationSubschemaOpts { export function handleFederationSubschema({ subschemaConfig, + unifiedGraphDirectives, realSubgraphNameMap, - schemaDirectives, - transportEntryMap, additionalTypeDefs, stitchingDirectivesTransformer, onSubgraphExecute, @@ -72,13 +73,16 @@ export function handleFederationSubschema({ transport: TransportEntry; [key: string]: any; }>(subschemaConfig.schema); - const directivesToLook = schemaDirectives || subgraphDirectives; + + // We need to add subgraph specific directives from supergraph to the subgraph schema + // So the executor can use it + const directivesToLook = unifiedGraphDirectives || subgraphDirectives; for (const directiveName in directivesToLook) { if ( !subgraphDirectives[directiveName]?.length && - schemaDirectives?.[directiveName]?.length + unifiedGraphDirectives?.[directiveName]?.length ) { - const directives = schemaDirectives[directiveName]; + const directives = unifiedGraphDirectives[directiveName]; for (const directive of directives) { if (directive.subgraph && directive.subgraph !== subgraphName) { continue; @@ -91,17 +95,7 @@ export function handleFederationSubschema({ const subgraphExtensions: Record = (subschemaConfig.schema.extensions ||= {}); subgraphExtensions['directives'] = subgraphDirectives; - const transportDirectives = (subgraphDirectives.transport ||= []); - const transportDirective = transportDirectives[0]; - if (transportDirective) { - transportEntryMap[subgraphName] = transportDirective; - } else { - transportEntryMap[subgraphName] = { - kind: 'http', - subgraph: subgraphName, - location: subschemaConfig.endpoint, - }; - } + interface TypeDirectives { source: SourceDirective; [key: string]: any; diff --git a/packages/fusion-runtime/src/federation/supergraph.ts b/packages/fusion-runtime/src/federation/supergraph.ts index 9d37f3291..571ecc3a8 100644 --- a/packages/fusion-runtime/src/federation/supergraph.ts +++ b/packages/fusion-runtime/src/federation/supergraph.ts @@ -1,7 +1,14 @@ -import type { TransportEntry } from '@graphql-mesh/transport-common'; import type { YamlConfig } from '@graphql-mesh/types'; -import { resolveAdditionalResolversWithoutImport } from '@graphql-mesh/utils'; -import type { SubschemaConfig } from '@graphql-tools/delegate'; +import { + getInContextSDK, + requestIdByRequest, + resolveAdditionalResolversWithoutImport, +} from '@graphql-mesh/utils'; +import type { + DelegationPlanBuilder, + StitchingInfo, + SubschemaConfig, +} from '@graphql-tools/delegate'; import { getStitchedSchemaFromSupergraphSdl } from '@graphql-tools/federation'; import { mergeTypeDefs } from '@graphql-tools/merge'; import { createMergedTypeResolver } from '@graphql-tools/stitch'; @@ -14,7 +21,6 @@ import { MapperKind, mapSchema, memoize1, - mergeDeep, TypeSource, } from '@graphql-tools/utils'; import { @@ -24,8 +30,16 @@ import { type GraphQLSchema, type ObjectTypeDefinitionNode, } from 'graphql'; -import type { UnifiedGraphHandler } from '../unifiedGraphManager'; -import { wrapMergedTypeResolver } from '../utils'; +import type { + UnifiedGraphHandler, + UnifiedGraphHandlerOpts, + UnifiedGraphHandlerResult, +} from '../unifiedGraphManager'; +import { + compareSubgraphNames, + OnDelegationPlanDoneHook, + wrapMergedTypeResolver, +} from '../utils'; import { handleFederationSubschema } from './subgraph'; // Memoize to avoid re-parsing the same schema AST @@ -139,22 +153,19 @@ export function handleResolveToDirectives( export const handleFederationSupergraph: UnifiedGraphHandler = function ({ unifiedGraph, onSubgraphExecute, + onDelegationPlanHooks, onDelegationStageExecuteHooks, + onDelegateHooks, additionalTypeDefs: additionalTypeDefsFromConfig = [], additionalResolvers: additionalResolversFromConfig = [], - transportEntryAdditions, batch = true, logger, -}) { +}: UnifiedGraphHandlerOpts): UnifiedGraphHandlerResult { const additionalTypeDefs = [...asArray(additionalTypeDefsFromConfig)]; const additionalResolvers = [...asArray(additionalResolversFromConfig)]; - const transportEntryMap: Record = {}; let subschemas: SubschemaConfig[] = []; const stitchingDirectivesTransformer = getStitchingDirectivesTransformerForSubschema(); - unifiedGraph = restoreExtraDirectives(unifiedGraph); - // Get Transport Information from Schema Directives - const schemaDirectives = getDirectiveExtensions(unifiedGraph); // Workaround to get the real name of the subschema const realSubgraphNameMap = new Map(); const joinGraphType = unifiedGraph.getType('join__Graph'); @@ -173,6 +184,8 @@ export const handleFederationSupergraph: UnifiedGraphHandler = function ({ } } + const unifiedGraphDirectives = getDirectiveExtensions(unifiedGraph); + let executableUnifiedGraph = getStitchedSchemaFromSupergraphSdl({ supergraphSdl: getDocumentNodeFromSchema(unifiedGraph), /** @@ -185,9 +198,8 @@ export const handleFederationSupergraph: UnifiedGraphHandler = function ({ onSubschemaConfig: (subschemaConfig) => handleFederationSubschema({ subschemaConfig, + unifiedGraphDirectives, realSubgraphNameMap, - schemaDirectives, - transportEntryMap, additionalTypeDefs, stitchingDirectivesTransformer, onSubgraphExecute, @@ -264,34 +276,104 @@ export const handleFederationSupergraph: UnifiedGraphHandler = function ({ }); }, }); - - if (transportEntryAdditions) { - const wildcardTransportOptions = transportEntryAdditions['*']; - for (const subgraphName in transportEntryMap) { - const toBeMerged: Partial[] = []; - const transportEntry = transportEntryMap[subgraphName]; - if (transportEntry) { - toBeMerged.push(transportEntry); - } - const transportOptionBySubgraph = transportEntryAdditions[subgraphName]; - if (transportOptionBySubgraph) { - toBeMerged.push(transportOptionBySubgraph); - } - const transportOptionByKind = - transportEntryAdditions['*.' + transportEntry?.kind]; - if (transportOptionByKind) { - toBeMerged.push(transportOptionByKind); - } - if (wildcardTransportOptions) { - toBeMerged.push(wildcardTransportOptions); + const inContextSDK = getInContextSDK( + executableUnifiedGraph, + // @ts-expect-error Legacy Mesh RawSource is not compatible with new Mesh + subschemas, + logger, + onDelegateHooks || [], + ); + const stitchingInfo = executableUnifiedGraph.extensions?.[ + 'stitchingInfo' + ] as StitchingInfo; + if (stitchingInfo && onDelegationPlanHooks?.length) { + for (const typeName in stitchingInfo.mergedTypes) { + const mergedTypeInfo = stitchingInfo.mergedTypes[typeName]; + if (mergedTypeInfo) { + const originalDelegationPlanBuilder = + mergedTypeInfo.nonMemoizedDelegationPlanBuilder; + mergedTypeInfo.nonMemoizedDelegationPlanBuilder = ( + supergraph, + sourceSubschema, + variables, + fragments, + fieldNodes, + context, + info, + ) => { + let delegationPlanBuilder = originalDelegationPlanBuilder; + function setDelegationPlanBuilder( + newDelegationPlanBuilder: DelegationPlanBuilder, + ) { + delegationPlanBuilder = newDelegationPlanBuilder; + } + const onDelegationPlanDoneHooks: OnDelegationPlanDoneHook[] = []; + let currentLogger = logger; + let requestId: string | undefined; + if (context?.request) { + requestId = requestIdByRequest.get(context.request); + if (requestId) { + currentLogger = currentLogger?.child(requestId); + } + } + if (sourceSubschema.name) { + currentLogger = currentLogger?.child(sourceSubschema.name); + } + for (const onDelegationPlan of onDelegationPlanHooks) { + const onDelegationPlanDone = onDelegationPlan({ + supergraph, + subgraph: sourceSubschema.name!, + sourceSubschema, + typeName: mergedTypeInfo.typeName, + variables, + fragments, + fieldNodes, + logger: currentLogger, + context, + info, + delegationPlanBuilder, + setDelegationPlanBuilder, + }); + if (onDelegationPlanDone) { + onDelegationPlanDoneHooks.push(onDelegationPlanDone); + } + } + let delegationPlan = delegationPlanBuilder( + supergraph, + sourceSubschema, + variables, + fragments, + fieldNodes, + context, + info, + ); + function setDelegationPlan( + newDelegationPlan: ReturnType, + ) { + delegationPlan = newDelegationPlan; + } + for (const onDelegationPlanDone of onDelegationPlanDoneHooks) { + onDelegationPlanDone({ + delegationPlan, + setDelegationPlan, + }); + } + return delegationPlan; + }; } - transportEntryMap[subgraphName] = mergeDeep(toBeMerged); } } return { unifiedGraph: executableUnifiedGraph, - subschemas, - transportEntryMap, - additionalResolvers, + inContextSDK, + getSubgraphSchema(subgraphName) { + const subgraph = subschemas.find( + (s) => s.name && compareSubgraphNames(s.name, subgraphName), + ); + if (!subgraph) { + throw new Error(`Subgraph ${subgraphName} not found`); + } + return subgraph.schema; + }, }; }; diff --git a/packages/fusion-runtime/src/unifiedGraphManager.ts b/packages/fusion-runtime/src/unifiedGraphManager.ts index edfa0d9cb..14e47d96d 100644 --- a/packages/fusion-runtime/src/unifiedGraphManager.ts +++ b/packages/fusion-runtime/src/unifiedGraphManager.ts @@ -3,13 +3,9 @@ import type { TransportEntry, } from '@graphql-mesh/transport-common'; import type { Logger, OnDelegateHook } from '@graphql-mesh/types'; -import { getInContextSDK, requestIdByRequest } from '@graphql-mesh/utils'; -import type { - DelegationPlanBuilder, - StitchingInfo, - SubschemaConfig, -} from '@graphql-tools/delegate'; +import { dispose, isDisposable } from '@graphql-mesh/utils'; import type { + Executor, IResolvers, MaybePromise, TypeSource, @@ -30,10 +26,9 @@ import { buildASTSchema, buildSchema, isSchema, print } from 'graphql'; import { handleFederationSupergraph } from './federation/supergraph'; import { compareSchemas, - compareSubgraphNames, getOnSubgraphExecute, + getTransportEntryMapUsingFusionAndFederationDirectives, millisecondsToStr, - OnDelegationPlanDoneHook, OnDelegationPlanHook, OnDelegationStageExecuteHook, type OnSubgraphExecuteHook, @@ -66,8 +61,9 @@ export interface UnifiedGraphHandlerOpts { additionalTypeDefs?: TypeSource; additionalResolvers?: IResolvers | IResolvers[]; onSubgraphExecute: ReturnType; + onDelegationPlanHooks?: OnDelegationPlanHook[]; onDelegationStageExecuteHooks?: OnDelegationStageExecuteHook[]; - transportEntryAdditions?: TransportEntryAdditions; + onDelegateHooks?: OnDelegateHook[]; /** * Whether to batch the subgraph executions. * @default true @@ -79,9 +75,9 @@ export interface UnifiedGraphHandlerOpts { export interface UnifiedGraphHandlerResult { unifiedGraph: GraphQLSchema; - transportEntryMap: Record; - subschemas: SubschemaConfig[]; - additionalResolvers: IResolvers[]; + executor?: Executor; + getSubgraphSchema(subgraphName: string): GraphQLSchema; + inContextSDK: any; } export interface UnifiedGraphManagerOptions { @@ -127,6 +123,7 @@ export class UnifiedGraphManager implements AsyncDisposable { private _transportEntryMap?: Record; private _transportExecutorStack?: AsyncDisposableStack; private lastLoadTime?: number; + private executor?: Executor; constructor(private opts: UnifiedGraphManagerOptions) { this.batch = opts.batch ?? true; this.handleUnifiedGraph = @@ -149,6 +146,7 @@ export class UnifiedGraphManager implements AsyncDisposable { this.initialUnifiedGraph$ = undefined; this.lastLoadTime = undefined; this.polling$ = undefined; + this.executor = undefined; } private ensureUnifiedGraph(): MaybePromise { @@ -289,8 +287,18 @@ export class UnifiedGraphManager implements AsyncDisposable { } } } + const transportExecutorStackDisposal = + this._transportExecutorStack?.disposeAsync?.(); + const unifiedgraphExecutorDisposal = isDisposable(this.executor) + ? dispose(this.executor) + : undefined; + + const disposalJobs = [ + transportExecutorStackDisposal, + unifiedgraphExecutorDisposal, + ].filter(isPromise); return mapMaybePromise( - this._transportExecutorStack?.disposeAsync?.(), + disposalJobs.length > 0 ? Promise.all(disposalJobs) : disposalJobs, () => { this.disposeReason = undefined; this._transportExecutorStack = new AsyncDisposableStack(); @@ -299,11 +307,16 @@ export class UnifiedGraphManager implements AsyncDisposable { }); this.lastLoadedUnifiedGraph = loadedUnifiedGraph; this.unifiedGraph = ensureSchema(loadedUnifiedGraph); + const transportEntryMap = + getTransportEntryMapUsingFusionAndFederationDirectives( + this.unifiedGraph, + this.opts.transportEntryAdditions, + ); const { unifiedGraph: newUnifiedGraph, - transportEntryMap, - subschemas, - additionalResolvers, + inContextSDK, + getSubgraphSchema, + executor, } = this.handleUnifiedGraph({ unifiedGraph: this.unifiedGraph, additionalTypeDefs: this.opts.additionalTypeDefs, @@ -311,121 +324,26 @@ export class UnifiedGraphManager implements AsyncDisposable { onSubgraphExecute(subgraphName, execReq) { return onSubgraphExecute(subgraphName, execReq); }, + onDelegationPlanHooks: this.onDelegationPlanHooks, onDelegationStageExecuteHooks: this.onDelegationStageExecuteHooks, - transportEntryAdditions: this.opts.transportEntryAdditions, + onDelegateHooks: this.opts.onDelegateHooks, batch: this.batch, logger: this.opts.transportContext?.logger, }); this.unifiedGraph = newUnifiedGraph; + this.executor = executor; const onSubgraphExecute = getOnSubgraphExecute({ onSubgraphExecuteHooks: this.onSubgraphExecuteHooks, transports: this.opts.transports, transportContext: this.opts.transportContext, transportEntryMap, - getSubgraphSchema(subgraphName) { - const subgraph = subschemas.find( - (s) => s.name && compareSubgraphNames(s.name, subgraphName), - ); - if (!subgraph) { - throw new Error(`Subgraph ${subgraphName} not found`); - } - return subgraph.schema; - }, + getSubgraphSchema, transportExecutorStack: this._transportExecutorStack, getDisposeReason: () => this.disposeReason, }); - if (this.opts.additionalResolvers || additionalResolvers.length) { - this.inContextSDK = getInContextSDK( - this.unifiedGraph, - // @ts-expect-error Legacy Mesh RawSource is not compatible with new Mesh - subschemas, - this.opts.transportContext?.logger, - this.opts.onDelegateHooks || [], - ); - } + this.inContextSDK = inContextSDK; this.lastLoadTime = Date.now(); this._transportEntryMap = transportEntryMap; - const stitchingInfo = this.unifiedGraph?.extensions?.[ - 'stitchingInfo' - ] as StitchingInfo; - if (stitchingInfo && this.onDelegationPlanHooks?.length) { - for (const typeName in stitchingInfo.mergedTypes) { - const mergedTypeInfo = stitchingInfo.mergedTypes[typeName]; - if (mergedTypeInfo) { - const originalDelegationPlanBuilder = - mergedTypeInfo.nonMemoizedDelegationPlanBuilder; - mergedTypeInfo.nonMemoizedDelegationPlanBuilder = ( - supergraph, - sourceSubschema, - variables, - fragments, - fieldNodes, - context, - info, - ) => { - let delegationPlanBuilder = originalDelegationPlanBuilder; - function setDelegationPlanBuilder( - newDelegationPlanBuilder: DelegationPlanBuilder, - ) { - delegationPlanBuilder = newDelegationPlanBuilder; - } - const onDelegationPlanDoneHooks: OnDelegationPlanDoneHook[] = - []; - let logger = this.opts.transportContext?.logger; - let requestId: string | undefined; - if (context?.request) { - requestId = requestIdByRequest.get(context.request); - if (requestId) { - logger = logger?.child(requestId); - } - } - if (sourceSubschema.name) { - logger = logger?.child(sourceSubschema.name); - } - for (const onDelegationPlan of this.onDelegationPlanHooks) { - const onDelegationPlanDone = onDelegationPlan({ - supergraph, - subgraph: sourceSubschema.name!, - sourceSubschema, - typeName: mergedTypeInfo.typeName, - variables, - fragments, - fieldNodes, - logger, - context, - info, - delegationPlanBuilder, - setDelegationPlanBuilder, - }); - if (onDelegationPlanDone) { - onDelegationPlanDoneHooks.push(onDelegationPlanDone); - } - } - let delegationPlan = delegationPlanBuilder( - supergraph, - sourceSubschema, - variables, - fragments, - fieldNodes, - context, - info, - ); - function setDelegationPlan( - newDelegationPlan: ReturnType, - ) { - delegationPlan = newDelegationPlan; - } - for (const onDelegationPlanDone of onDelegationPlanDoneHooks) { - onDelegationPlanDone({ - delegationPlan, - setDelegationPlan, - }); - } - return delegationPlan; - }; - } - } - } return this.unifiedGraph; }, ); @@ -468,6 +386,10 @@ export class UnifiedGraphManager implements AsyncDisposable { }); } + public getExecutor(): MaybePromise { + return mapMaybePromise(this.ensureUnifiedGraph(), () => this.executor); + } + public getContext(base: T = {} as T) { return mapMaybePromise(this.ensureUnifiedGraph(), () => { if (this.inContextSDK) { diff --git a/packages/fusion-runtime/src/utils.ts b/packages/fusion-runtime/src/utils.ts index 3704366fc..6cbeb5179 100644 --- a/packages/fusion-runtime/src/utils.ts +++ b/packages/fusion-runtime/src/utils.ts @@ -19,10 +19,12 @@ import { Subschema, } from '@graphql-tools/delegate'; import { + getDirectiveExtensions, isAsyncIterable, isDocumentNode, mapAsyncIterator, mapMaybePromise, + mergeDeep, printSchemaWithDirectives, type ExecutionRequest, type Executor, @@ -33,6 +35,7 @@ import { constantCase } from 'constant-case'; import { FragmentDefinitionNode, GraphQLError, + isEnumType, SelectionNode, SelectionSetNode, type DocumentNode, @@ -40,6 +43,8 @@ import { type GraphQLSchema, } from 'graphql'; import type { GraphQLOutputType, GraphQLResolveInfo } from 'graphql/type'; +import { restoreExtraDirectives } from './federation/supergraph'; +import { TransportEntryAdditions } from './unifiedGraphManager'; export type { TransportEntry, @@ -620,3 +625,79 @@ export function millisecondsToStr(milliseconds: number): string { } return 'less than a second'; //'just now' //or other string you like; } + +export function getTransportEntryMapUsingFusionAndFederationDirectives( + unifiedGraph: GraphQLSchema, + transportEntryAdditions?: TransportEntryAdditions, +) { + unifiedGraph = restoreExtraDirectives(unifiedGraph); + const transportEntryMap: Record = {}; + const joinGraph = unifiedGraph.getType('join__Graph'); + const schemaDirectives = getDirectiveExtensions<{ + transport: TransportEntry; + }>(unifiedGraph); + if (isEnumType(joinGraph)) { + for (const enumValue of joinGraph.getValues()) { + const enumValueDirectives = getDirectiveExtensions<{ + join__graph: { + name: string; + url?: string; + }; + }>(enumValue); + if (enumValueDirectives?.join__graph?.length) { + for (const joinGraphDirective of enumValueDirectives.join__graph) { + if (joinGraphDirective.url) { + transportEntryMap[joinGraphDirective.name] = { + subgraph: joinGraphDirective.name, + kind: 'http', + location: joinGraphDirective.url, + }; + } + } + } + } + } + if (schemaDirectives?.transport?.length) { + for (const transportDirective of schemaDirectives.transport) { + transportEntryMap[transportDirective.subgraph] = transportDirective; + } + } + if (transportEntryAdditions) { + const wildcardTransportOptions = transportEntryAdditions['*']; + for (const subgraphName in transportEntryMap) { + const toBeMerged: Partial[] = []; + const transportEntry = transportEntryMap[subgraphName]; + if (transportEntry) { + toBeMerged.push(transportEntry); + } + const transportOptionBySubgraph = transportEntryAdditions[subgraphName]; + if (transportOptionBySubgraph) { + toBeMerged.push(transportOptionBySubgraph); + } + const transportOptionByKind = + transportEntryAdditions['*.' + transportEntry?.kind]; + if (transportOptionByKind) { + toBeMerged.push(transportOptionByKind); + } + if (wildcardTransportOptions) { + toBeMerged.push(wildcardTransportOptions); + } + transportEntryMap[subgraphName] = mergeDeep(toBeMerged); + } + } + const schemaExtensions: { + directives?: { + transport?: TransportEntry[]; + }; + } = (unifiedGraph.extensions ||= {}); + const directivesInExtensions = (schemaExtensions.directives ||= {}); + const transportEntriesInExtensions: TransportEntry[] = + (directivesInExtensions.transport = []); + for (const subgraphName in transportEntryMap) { + const transportEntry = transportEntryMap[subgraphName]; + if (transportEntry) { + transportEntriesInExtensions.push(transportEntry); + } + } + return transportEntryMap; +} diff --git a/packages/runtime/src/createGatewayRuntime.ts b/packages/runtime/src/createGatewayRuntime.ts index 84fc5fe0b..718625f34 100644 --- a/packages/runtime/src/createGatewayRuntime.ts +++ b/packages/runtime/src/createGatewayRuntime.ts @@ -1,3 +1,4 @@ +import { OnExecuteEventPayload, OnSubscribeEventPayload } from '@envelop/core'; import { useDisableIntrospection } from '@envelop/disable-introspection'; import { useGenericAuth } from '@envelop/generic-auth'; import { @@ -13,6 +14,7 @@ import type { import { getOnSubgraphExecute, getStitchingDirectivesTransformerForSubschema, + getTransportEntryMapUsingFusionAndFederationDirectives, handleFederationSubschema, handleResolveToDirectives, restoreExtraDirectives, @@ -65,7 +67,6 @@ import { GraphQLSchema, isSchema, parse, - type ExecutionArgs, } from 'graphql'; import { createYoga, @@ -105,7 +106,11 @@ import type { GatewayPlugin, UnifiedGraphConfig, } from './types'; -import { checkIfDataSatisfiesSelectionSet, defaultQueryText } from './utils'; +import { + checkIfDataSatisfiesSelectionSet, + defaultQueryText, + getExecuteFnFromExecutor, +} from './utils'; // TODO: this type export is not properly accessible from graphql-yoga // "graphql-yoga/typings/plugins/use-graphiql.js" is an illegal path @@ -170,6 +175,7 @@ export function createGatewayRuntime< let getSchema: () => MaybePromise = () => unifiedGraph; let contextBuilder: (context: T) => MaybePromise; let readinessChecker: () => MaybePromise; + let getExecutor: (() => MaybePromise) | undefined; const { name: reportingTarget, plugin: registryPlugin } = getReportingPlugin( config, configContext, @@ -215,18 +221,8 @@ export function createGatewayRuntime< onSubgraphExecuteHooks, transportExecutorStack, }); - function createExecuteFnFromExecutor(executor: Executor) { - return function executeFn(args: ExecutionArgs) { - return executor({ - rootValue: args.rootValue, - document: args.document, - ...(args.operationName ? { operationName: args.operationName } : {}), - ...(args.variableValues ? { variables: args.variableValues } : {}), - ...(args.contextValue ? { context: args.contextValue } : {}), - }); - }; - } - const executeFn = createExecuteFnFromExecutor(proxyExecutor); + + getExecutor = () => proxyExecutor; let currentTimeout: ReturnType; const pollingInterval = config.pollingInterval; @@ -342,12 +338,6 @@ export function createGatewayRuntime< const shouldSkipValidation = 'skipValidation' in config ? config.skipValidation : false; const executorPlugin: GatewayPlugin = { - onExecute({ setExecuteFn }) { - setExecuteFn(executeFn); - }, - onSubscribe({ setSubscribeFn }) { - setSubscribeFn(executeFn); - }, onValidate({ params, setResult }) { if (shouldSkipValidation || !params.schema) { setResult([]); @@ -432,7 +422,11 @@ export function createGatewayRuntime< ], schema: unifiedGraph, }; - const transportEntryMap: Record = {}; + const transportEntryMap: Record = + getTransportEntryMapUsingFusionAndFederationDirectives( + unifiedGraph, + config.transportEntries, + ); const additionalTypeDefs: TypeSource[] = []; const stitchingDirectivesTransformer = @@ -449,7 +443,6 @@ export function createGatewayRuntime< }); subschemaConfig = handleFederationSubschema({ subschemaConfig, - transportEntryMap, additionalTypeDefs, stitchingDirectivesTransformer, onSubgraphExecute, @@ -701,6 +694,7 @@ export function createGatewayRuntime< ); schemaInvalidator = () => unifiedGraphManager.invalidateUnifiedGraph(); contextBuilder = (base) => unifiedGraphManager.getContext(base as any); + getExecutor = () => unifiedGraphManager.getExecutor(); unifiedGraphPlugin = { [DisposableSymbols.asyncDispose]() { return unifiedGraphManager[DisposableSymbols.asyncDispose](); @@ -814,6 +808,31 @@ export function createGatewayRuntime< }, }; + if (getExecutor) { + const onExecute = ({ + setExecuteFn, + }: OnExecuteEventPayload) => + mapMaybePromise(getExecutor?.(), (executor) => { + if (executor) { + const executeFn = getExecuteFnFromExecutor(executor); + setExecuteFn(executeFn); + } + }); + const onSubscribe = ({ + setSubscribeFn, + }: OnSubscribeEventPayload) => + mapMaybePromise(getExecutor?.(), (executor) => { + if (executor) { + const subscribeFn = getExecuteFnFromExecutor(executor); + setSubscribeFn(subscribeFn); + } + }); + //@ts-expect-error - MaybePromise is not compatible with PromiseOrValue + defaultGatewayPlugin.onExecute = onExecute; + //@ts-expect-error - MaybePromise is not compatible with PromiseOrValue + defaultGatewayPlugin.onSubscribe = onSubscribe; + } + const productName = config.productName || 'Hive Gateway'; const productDescription = config.productDescription || 'Federated GraphQL Gateway'; diff --git a/packages/runtime/src/utils.ts b/packages/runtime/src/utils.ts index 2c5ae5845..8b78a6cdd 100644 --- a/packages/runtime/src/utils.ts +++ b/packages/runtime/src/utils.ts @@ -1,3 +1,5 @@ +import type { ExecutionArgs } from '@graphql-tools/executor'; +import { Executor, memoize1 } from '@graphql-tools/utils'; import type { SelectionSetNode } from 'graphql'; export function checkIfDataSatisfiesSelectionSet( @@ -75,3 +77,18 @@ export function delayInMs(ms: number, signal?: AbortSignal) { ); }); } + +export const getExecuteFnFromExecutor = memoize1( + function getExecuteFnFromExecutor(executor: Executor) { + return function executeFn(args: ExecutionArgs) { + return executor({ + document: args.document, + variables: args.variableValues, + operationName: args.operationName ?? undefined, + rootValue: args.rootValue, + context: args.contextValue, + signal: args.signal, + }); + }; + }, +);