From c0ca31901572473785c0eccbdfdadbbfa145d14a Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Tue, 29 Jun 2021 10:54:17 +0300 Subject: [PATCH] Remove Subscriber and use only Executor (#3117) * Remove Subscriber and use only Executor * Fix introspectSchema * Fix tests * Cleanup --- .changeset/nine-ducks-burn.md | 6 ++ .changeset/sharp-wombats-perform.md | 15 ++++ .../src/createBatchingExecutor.ts | 7 +- packages/delegate/src/Subschema.ts | 4 +- packages/delegate/src/delegateToSchema.ts | 89 +++++++------------ packages/delegate/src/index.ts | 2 +- packages/delegate/src/types.ts | 3 +- packages/links/src/index.ts | 1 - packages/links/src/linkToExecutor.ts | 36 ++++---- packages/links/src/linkToSubscriber.ts | 25 ------ packages/loaders/url/src/index.ts | 78 ++++++---------- packages/stitch/tests/fixtures/schemas.ts | 39 ++++---- packages/utils/src/executor.ts | 18 ++-- packages/wrap/src/introspect.ts | 19 ++-- packages/wrap/tests/fixtures/schemas.ts | 70 +++++++-------- website/docs/remote-schemas.md | 33 ++----- website/docs/stitch-combining-schemas.md | 6 +- 17 files changed, 178 insertions(+), 273 deletions(-) create mode 100644 .changeset/nine-ducks-burn.md create mode 100644 .changeset/sharp-wombats-perform.md delete mode 100644 packages/links/src/linkToSubscriber.ts diff --git a/.changeset/nine-ducks-burn.md b/.changeset/nine-ducks-burn.md new file mode 100644 index 00000000000..0f2eaa561fd --- /dev/null +++ b/.changeset/nine-ducks-burn.md @@ -0,0 +1,6 @@ +--- +'@graphql-tools/wrap': major +--- + +BREAKING CHANGE +- Remove unnecessary `introspectSchemaSync`, `introspectSchema` already handles sync execution diff --git a/.changeset/sharp-wombats-perform.md b/.changeset/sharp-wombats-perform.md new file mode 100644 index 00000000000..f347c5699df --- /dev/null +++ b/.changeset/sharp-wombats-perform.md @@ -0,0 +1,15 @@ +--- +'@graphql-tools/batch-execute': major +'@graphql-tools/delegate': major +'@graphql-tools/links': major +'@graphql-tools/url-loader': major +'@graphql-tools/stitch': major +'@graphql-tools/utils': major +'@graphql-tools/wrap': major +--- + +BREAKING CHANGE +- Remove Subscriber and use only Executor +- - Now `Executor` can receive `AsyncIterable` and subscriptions will also be handled by `Executor`. This is a future-proof change for defer, stream and live queries + + diff --git a/packages/batch-execute/src/createBatchingExecutor.ts b/packages/batch-execute/src/createBatchingExecutor.ts index 7bb8a4201f0..5f1bd306872 100644 --- a/packages/batch-execute/src/createBatchingExecutor.ts +++ b/packages/batch-execute/src/createBatchingExecutor.ts @@ -18,7 +18,10 @@ export function createBatchingExecutor( ) => Record = defaultExtensionsReducer ): Executor { const loader = new DataLoader(createLoadFn(executor, extensionsReducer), dataLoaderOptions); - return (executionParams: ExecutionParams) => loader.load(executionParams); + return (executionParams: ExecutionParams) => + executionParams.info?.operation.operation === 'subscription' + ? executor(executionParams) + : loader.load(executionParams); } function createLoadFn( @@ -53,7 +56,7 @@ function createLoadFn( const executionResults: Array> = execBatches.map(execBatch => { const mergedExecutionParams = mergeExecutionParams(execBatch, extensionsReducer); - return new ValueOrPromise(() => executor(mergedExecutionParams)); + return new ValueOrPromise(() => executor(mergedExecutionParams) as ExecutionResult); }); return ValueOrPromise.all(executionResults) diff --git a/packages/delegate/src/Subschema.ts b/packages/delegate/src/Subschema.ts index b16d8fa611c..668d68f82e6 100644 --- a/packages/delegate/src/Subschema.ts +++ b/packages/delegate/src/Subschema.ts @@ -3,7 +3,7 @@ import { GraphQLSchema } from 'graphql'; import { SubschemaConfig, Transform, MergedTypeConfig, CreateProxyingResolverFn, BatchingOptions } from './types'; import { applySchemaTransforms } from './applySchemaTransforms'; -import { Executor, Subscriber } from '@graphql-tools/utils'; +import { Executor } from '@graphql-tools/utils'; export function isSubschema(value: any): value is Subschema { return Boolean(value.transformedSchema); @@ -20,7 +20,6 @@ export class Subschema> public schema: GraphQLSchema; public executor?: Executor; - public subscriber?: Subscriber; public batch?: boolean; public batchingOptions?: BatchingOptions; @@ -34,7 +33,6 @@ export class Subschema> this.schema = config.schema; this.executor = config.executor; - this.subscriber = config.subscriber; this.batch = config.batch; this.batchingOptions = config.batchingOptions; diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 2632fdae574..440bcc7fcfe 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -18,13 +18,12 @@ import { getBatchingExecutor } from '@graphql-tools/batch-execute'; import { mapAsyncIterator, - ExecutionResult, Executor, ExecutionParams, - Subscriber, Maybe, assertSome, AggregateError, + isAsyncIterable, } from '@graphql-tools/utils'; import { @@ -102,41 +101,27 @@ export function delegateRequest, TArgs = any>( validateRequest(delegationContext, processedRequest.document); } - const { operation, context, info } = delegationContext; + const { context, info } = delegationContext; - if (operation === 'query' || operation === 'mutation') { - const executor = getExecutor(delegationContext); + const executor = getExecutor(delegationContext); - return new ValueOrPromise(() => - executor({ - ...processedRequest, - context, - info, - }) - ) - .then(originalResult => transformer.transformResult(originalResult)) - .resolve(); - } - - const subscriber = getSubscriber(delegationContext); - - return subscriber({ - ...processedRequest, - context, - info, - }).then(subscriptionResult => { - if (Symbol.asyncIterator in subscriptionResult) { - // "subscribe" to the subscription result and map the result through the transforms - return mapAsyncIterator( - subscriptionResult as AsyncIterableIterator, - originalResult => ({ + return new ValueOrPromise(() => + executor({ + ...processedRequest, + context, + info, + }) + ) + .then(originalResult => { + if (isAsyncIterable(originalResult)) { + // "subscribe" to the subscription result and map the result through the transforms + return mapAsyncIterator(originalResult, originalResult => ({ [delegationContext.fieldName]: transformer.transformResult(originalResult), - }) - ); - } - - return transformer.transformResult(subscriptionResult as ExecutionResult); - }); + })); + } + return transformer.transformResult(originalResult); + }) + .resolve(); } const emptyObject = {}; @@ -250,14 +235,14 @@ function validateRequest(delegationContext: DelegationContext, document: Do } function getExecutor(delegationContext: DelegationContext): Executor { - const { subschemaConfig, targetSchema, context } = delegationContext; + const { subschemaConfig, targetSchema, context, operation } = delegationContext; - let executor: Executor = subschemaConfig?.executor || createDefaultExecutor(targetSchema); + let executor: Executor = subschemaConfig?.executor || createDefaultExecutor(targetSchema, operation); if (subschemaConfig?.batch) { const batchingOptions = subschemaConfig?.batchingOptions; executor = getBatchingExecutor( - context ?? self ?? window ?? global, + context ?? globalThis ?? window ?? global, executor, batchingOptions?.dataLoaderOptions, batchingOptions?.extensionsReducer @@ -267,29 +252,17 @@ function getExecutor(delegationContext: DelegationContext): return executor; } -function getSubscriber(delegationContext: DelegationContext): Subscriber { - const { subschemaConfig, targetSchema } = delegationContext; - return subschemaConfig?.subscriber || createDefaultSubscriber(targetSchema); -} - -function createDefaultExecutor(schema: GraphQLSchema): Executor { - return (({ document, context, variables, rootValue }: ExecutionParams) => - execute({ +const createDefaultExecutor = (schema: GraphQLSchema, operation: OperationTypeNode) => + (({ document, context, variables, rootValue }: ExecutionParams) => { + const executionParams = { schema, document, contextValue: context, variableValues: variables, rootValue, - })) as Executor; -} - -function createDefaultSubscriber(schema: GraphQLSchema) { - return ({ document, context, variables, rootValue }: ExecutionParams) => - subscribe({ - schema, - document, - contextValue: context, - variableValues: variables, - rootValue, - }) as any; -} + }; + if (operation === 'subscription') { + return subscribe(executionParams); + } + return execute(executionParams); + }) as Executor; diff --git a/packages/delegate/src/index.ts b/packages/delegate/src/index.ts index 93b3a21b408..030fed028b0 100644 --- a/packages/delegate/src/index.ts +++ b/packages/delegate/src/index.ts @@ -9,4 +9,4 @@ export * from './resolveExternalValue'; export * from './subschemaConfig'; export * from './transforms'; export * from './types'; -export { Executor, Subscriber, AsyncExecutor, SyncExecutor, ExecutionParams } from '@graphql-tools/utils'; +export { Executor, AsyncExecutor, SyncExecutor, ExecutionParams } from '@graphql-tools/utils'; diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index 7f1a77a2fa3..f793ea6c19b 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -14,7 +14,7 @@ import { import DataLoader from 'dataloader'; -import { ExecutionParams, ExecutionResult, Executor, Request, Subscriber, TypeMap } from '@graphql-tools/utils'; +import { ExecutionParams, ExecutionResult, Executor, Request, TypeMap } from '@graphql-tools/utils'; import { Subschema } from './Subschema'; import { OBJECT_SUBSCHEMA_SYMBOL, FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SYMBOL } from './symbols'; @@ -145,7 +145,6 @@ export interface SubschemaConfig>; merge?: Record>; executor?: Executor; - subscriber?: Subscriber; batch?: boolean; batchingOptions?: BatchingOptions; } diff --git a/packages/links/src/index.ts b/packages/links/src/index.ts index 17fdfb74273..2b2a329a4d9 100644 --- a/packages/links/src/index.ts +++ b/packages/links/src/index.ts @@ -1,5 +1,4 @@ export { createServerHttpLink } from './createServerHttpLink'; export { AwaitVariablesLink } from './AwaitVariablesLink'; export { linkToExecutor } from './linkToExecutor'; -export { linkToSubscriber } from './linkToSubscriber'; export { GraphQLUpload } from './GraphQLUpload'; diff --git a/packages/links/src/linkToExecutor.ts b/packages/links/src/linkToExecutor.ts index 4b6c105027a..1f8e579da6b 100644 --- a/packages/links/src/linkToExecutor.ts +++ b/packages/links/src/linkToExecutor.ts @@ -1,24 +1,26 @@ +import { toPromise } from '@apollo/client/core'; import { ApolloLink, execute } from '@apollo/client/link/core'; import { Observable } from '@apollo/client/utilities'; -import { toPromise } from '@apollo/client/link/utils'; -import { AsyncExecutor, ExecutionParams, ExecutionResult } from '@graphql-tools/utils'; +import { Executor, ExecutionParams, ExecutionResult, observableToAsyncIterable } from '@graphql-tools/utils'; export const linkToExecutor = - (link: ApolloLink): AsyncExecutor => - (params: ExecutionParams): Promise> => { + (link: ApolloLink): Executor => + async (params: ExecutionParams) => { const { document, variables, extensions, context, info, operationName } = params; - return toPromise( - execute(link, { - query: document, - variables: variables, - context: { - graphqlContext: context, - graphqlResolveInfo: info, - clientAwareness: {}, - }, - extensions, - operationName, - }) as Observable> - ); + const observable = execute(link, { + query: document, + variables, + context: { + graphqlContext: context, + graphqlResolveInfo: info, + clientAwareness: {}, + }, + extensions, + operationName, + }) as Observable>; + if (info?.operation.operation === 'subscription') { + return observableToAsyncIterable>(observable)[Symbol.asyncIterator](); + } + return toPromise(observable); }; diff --git a/packages/links/src/linkToSubscriber.ts b/packages/links/src/linkToSubscriber.ts deleted file mode 100644 index 3da42805e7f..00000000000 --- a/packages/links/src/linkToSubscriber.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ApolloLink, execute } from '@apollo/client/link/core'; -import { Observable } from '@apollo/client/utilities'; - -import { Subscriber, ExecutionParams, ExecutionResult, observableToAsyncIterable } from '@graphql-tools/utils'; - -export const linkToSubscriber = - (link: ApolloLink): Subscriber => - async ( - params: ExecutionParams - ): Promise | AsyncIterableIterator>> => { - const { document, variables, extensions, context, info, operationName } = params; - return observableToAsyncIterable>( - execute(link, { - query: document, - variables, - context: { - graphqlContext: context, - graphqlResolveInfo: info, - clientAwareness: {}, - }, - extensions, - operationName, - }) as Observable> - )[Symbol.asyncIterator](); - }; diff --git a/packages/loaders/url/src/index.ts b/packages/loaders/url/src/index.ts index 2b13a5ee38f..6424b0bd23c 100644 --- a/packages/loaders/url/src/index.ts +++ b/packages/loaders/url/src/index.ts @@ -5,7 +5,6 @@ import { print, IntrospectionOptions, Kind, GraphQLError } from 'graphql'; import { AsyncExecutor, Executor, - Subscriber, SyncExecutor, SchemaPointerSingle, Source, @@ -382,11 +381,11 @@ export class UrlLoader implements DocumentLoader { return executor; } - buildWSSubscriber( + buildWSExecutor( subscriptionsEndpoint: string, webSocketImpl: typeof WebSocket, connectionParams?: ClientOptions['connectionParams'] - ): Subscriber { + ): AsyncExecutor { const WS_URL = switchProtocols(subscriptionsEndpoint, { https: 'wss', http: 'ws', @@ -418,11 +417,11 @@ export class UrlLoader implements DocumentLoader { }; } - buildWSLegacySubscriber( + buildWSLegacyExecutor( subscriptionsEndpoint: string, webSocketImpl: typeof WebSocket, connectionParams?: ConnectionParamsOptions - ): Subscriber { + ): AsyncExecutor { const WS_URL = switchProtocols(subscriptionsEndpoint, { https: 'wss', http: 'ws', @@ -447,12 +446,12 @@ export class UrlLoader implements DocumentLoader { }; } - buildSSESubscriber( + buildSSEExecutor( pointer: string, extraHeaders: HeadersConfig | undefined, fetch: AsyncFetchFn, options: Maybe - ): Subscriber { + ): AsyncExecutor { return async ({ document, variables, extensions }) => { const controller = new AbortController(); const query = print(document); @@ -558,14 +557,11 @@ export class UrlLoader implements DocumentLoader { } } - async getExecutorAndSubscriberAsync( - pointer: SchemaPointerSingle, - options: LoadFromUrlOptions = {} - ): Promise<{ executor: AsyncExecutor; subscriber: Subscriber }> { + async getExecutorAsync(pointer: SchemaPointerSingle, options: LoadFromUrlOptions = {}): Promise { const fetch = await this.getFetch(options.customFetch, asyncImport, true); const defaultMethod = this.getDefaultMethodFromOptions(options.method, 'POST'); - const executor = this.buildExecutor({ + const httpExecutor = this.buildExecutor({ pointer, fetch, extraHeaders: options.headers, @@ -574,31 +570,35 @@ export class UrlLoader implements DocumentLoader { multipart: options.multipart, }); - let subscriber: Subscriber; + let subscriptionExecutor: AsyncExecutor; const subscriptionsEndpoint = options.subscriptionsEndpoint || pointer; if (options.useSSEForSubscription) { - subscriber = this.buildSSESubscriber(subscriptionsEndpoint, options.headers, fetch, options.eventSourceOptions); + subscriptionExecutor = this.buildSSEExecutor( + subscriptionsEndpoint, + options.headers, + fetch, + options.eventSourceOptions + ); } else { const webSocketImpl = await this.getWebSocketImpl(options, asyncImport); const connectionParams = () => ({ headers: options.headers }); if (options.useWebSocketLegacyProtocol) { - subscriber = this.buildWSLegacySubscriber(subscriptionsEndpoint, webSocketImpl, connectionParams); + subscriptionExecutor = this.buildWSLegacyExecutor(subscriptionsEndpoint, webSocketImpl, connectionParams); } else { - subscriber = this.buildWSSubscriber(subscriptionsEndpoint, webSocketImpl, connectionParams); + subscriptionExecutor = this.buildWSExecutor(subscriptionsEndpoint, webSocketImpl, connectionParams); } } - return { - executor, - subscriber, + return params => { + if (params.info?.operation.operation === 'subscription') { + return subscriptionExecutor(params); + } + return httpExecutor(params); }; } - getExecutorAndSubscriberSync( - pointer: SchemaPointerSingle, - options: LoadFromUrlOptions - ): { executor: SyncExecutor; subscriber: Subscriber } { + getExecutorSync(pointer: SchemaPointerSingle, options: LoadFromUrlOptions): SyncExecutor { const fetch = this.getFetch(options?.customFetch, syncImport, false); const defaultMethod = this.getDefaultMethodFromOptions(options?.method, 'POST'); @@ -610,48 +610,22 @@ export class UrlLoader implements DocumentLoader { useGETForQueries: options.useGETForQueries, }); - const subscriptionsEndpoint = options.subscriptionsEndpoint || pointer; - let subscriber: Subscriber; - if (options.useSSEForSubscription) { - const asyncFetchFn: any = (...args: any[]) => - this.getFetch(options?.customFetch, asyncImport, true).then((asyncFetch: any) => asyncFetch(...args)); - subscriber = this.buildSSESubscriber( - subscriptionsEndpoint, - options.headers, - asyncFetchFn, - options.eventSourceOptions - ); - } else { - const webSocketImpl = this.getWebSocketImpl(options, syncImport); - const connectionParams = () => ({ headers: options.headers }); - if (options.useWebSocketLegacyProtocol) { - subscriber = this.buildWSLegacySubscriber(subscriptionsEndpoint, webSocketImpl, connectionParams); - } else { - subscriber = this.buildWSSubscriber(subscriptionsEndpoint, webSocketImpl, connectionParams); - } - } - - return { - executor, - subscriber, - }; + return executor; } async getSubschemaConfigAsync(pointer: SchemaPointerSingle, options: LoadFromUrlOptions): Promise { - const { executor, subscriber } = await this.getExecutorAndSubscriberAsync(pointer, options); + const executor = await this.getExecutorAsync(pointer, options); return { schema: await introspectSchema(executor, undefined, options as IntrospectionOptions), executor, - subscriber, }; } getSubschemaConfigSync(pointer: SchemaPointerSingle, options: LoadFromUrlOptions): SubschemaConfig { - const { executor, subscriber } = this.getExecutorAndSubscriberSync(pointer, options); + const executor = this.getExecutorSync(pointer, options); return { schema: introspectSchema(executor, undefined, options as IntrospectionOptions), executor, - subscriber, }; } diff --git a/packages/stitch/tests/fixtures/schemas.ts b/packages/stitch/tests/fixtures/schemas.ts index a6840a8b890..bab2fb0b1b5 100644 --- a/packages/stitch/tests/fixtures/schemas.ts +++ b/packages/stitch/tests/fixtures/schemas.ts @@ -682,7 +682,23 @@ export const subscriptionSchema: GraphQLSchema = makeExecutableSchema({ }); function makeExecutorFromSchema(schema: GraphQLSchema) { - return async ({ document, variables, context }: ExecutionParams) => { + return async ({ document, variables, context, info }: ExecutionParams) => { + if (info?.operation.operation === 'subscription') { + const result = subscribe( + schema, + document, + null, + context, + variables, + ) as Promise> | ExecutionResult>; + if (isPromise(result)) { + return result.then(asyncIterator => { + assertAsyncIterable(asyncIterator) + return mapAsyncIterator(asyncIterator, (originalResult: ExecutionResult) => JSON.parse(JSON.stringify(originalResult))) + }); + } + return JSON.parse(JSON.stringify(result)); + } return (new ValueOrPromise(() => graphql( schema, print(document), @@ -701,35 +717,14 @@ function assertAsyncIterable(input: unknown): asserts input is AsyncIterableIter } } -function makeSubscriberFromSchema(schema: GraphQLSchema) { - return async ({ document, variables, context }: ExecutionParams) => { - const result = subscribe( - schema, - document, - null, - context, - variables, - ) as Promise> | ExecutionResult>; - if (isPromise(result)) { - return result.then(asyncIterator => { - assertAsyncIterable(asyncIterator) - return mapAsyncIterator(asyncIterator, (originalResult: ExecutionResult) => JSON.parse(JSON.stringify(originalResult))) - }); - } - return JSON.parse(JSON.stringify(result)); - }; -} - export async function makeSchemaRemote( schema: GraphQLSchema, ): Promise { const executor = makeExecutorFromSchema(schema); - const subscriber = makeSubscriberFromSchema(schema); const clientSchema = await introspectSchema(executor); return { schema: clientSchema, executor, - subscriber, batch: true, }; } diff --git a/packages/utils/src/executor.ts b/packages/utils/src/executor.ts index eb984db581d..ac89baeb322 100644 --- a/packages/utils/src/executor.ts +++ b/packages/utils/src/executor.ts @@ -1,6 +1,9 @@ import { DocumentNode, GraphQLResolveInfo } from 'graphql'; import { ExecutionResult } from './Interfaces'; +type MaybePromise = Promise | T; +type MaybeAsyncIterableIterator = AsyncIterableIterator | T; + export interface ExecutionParams< TArgs extends Record = Record, TContext = any, @@ -24,7 +27,8 @@ export type AsyncExecutor, TBaseExtensions = TExtensions extends TBaseExtensions = TBaseExtensions >( params: ExecutionParams -) => Promise>; +) => Promise>>; + export type SyncExecutor, TBaseExtensions = Record> = < TReturn = any, TArgs = Record, @@ -34,6 +38,7 @@ export type SyncExecutor, TBaseExtensions = R >( params: ExecutionParams ) => ExecutionResult; + export type Executor, TBaseExtensions = Record> = < TReturn = any, TArgs = Record, @@ -42,13 +47,4 @@ export type Executor, TBaseExtensions = Recor TExtensions extends TBaseExtensions = TBaseExtensions >( params: ExecutionParams -) => ExecutionResult | Promise>; -export type Subscriber, TBaseExtensions = Record> = < - TReturn = any, - TArgs = Record, - TContext extends TBaseContext = TBaseContext, - TRoot = any, - TExtensions extends TBaseExtensions = TBaseExtensions ->( - params: ExecutionParams -) => Promise> | ExecutionResult>; +) => MaybePromise>>; diff --git a/packages/wrap/src/introspect.ts b/packages/wrap/src/introspect.ts index 34e91c3f8cf..7f0a4576513 100644 --- a/packages/wrap/src/introspect.ts +++ b/packages/wrap/src/introspect.ts @@ -10,7 +10,7 @@ import { import { ValueOrPromise } from 'value-or-promise'; -import { AsyncExecutor, Executor, SyncExecutor, ExecutionResult, AggregateError } from '@graphql-tools/utils'; +import { AsyncExecutor, Executor, ExecutionResult, AggregateError, isAsyncIterable } from '@graphql-tools/utils'; function getSchemaFromIntrospection(introspectionResult: ExecutionResult): GraphQLSchema { if (introspectionResult?.data?.__schema) { @@ -27,7 +27,7 @@ function getSchemaFromIntrospection(introspectionResult: ExecutionResult( +export function introspectSchema( executor: TExecutor, context?: Record, options?: IntrospectionOptions @@ -39,15 +39,12 @@ export function introspectSchema context, }) ) + .then(introspection => { + if (isAsyncIterable(introspection)) { + return introspection.next().then(({ value }) => value); + } + return introspection; + }) .then(introspection => getSchemaFromIntrospection(introspection)) .resolve() as any; } - -// Keep for backwards compatibility. Will be removed on next release. -export function introspectSchemaSync( - executor: SyncExecutor, - context?: Record, - options?: IntrospectionOptions -) { - return introspectSchema(executor, context, options); -} diff --git a/packages/wrap/tests/fixtures/schemas.ts b/packages/wrap/tests/fixtures/schemas.ts index b29511136b2..22edcc4f7f4 100644 --- a/packages/wrap/tests/fixtures/schemas.ts +++ b/packages/wrap/tests/fixtures/schemas.ts @@ -284,8 +284,7 @@ const propertyRootTypeDefs = ` foo: String } - ${ - 'getInterfaces' in GraphQLInterfaceType.prototype + ${'getInterfaces' in GraphQLInterfaceType.prototype ? `interface TestNestedInterface implements TestInterface { kind: TestInterfaceKind testString: String @@ -296,7 +295,7 @@ const propertyRootTypeDefs = ` testString: String bar: String }` - : `type TestImpl2 implements TestInterface { + : `type TestImpl2 implements TestInterface { kind: TestInterfaceKind testString: String bar: String @@ -363,27 +362,27 @@ const propertyResolvers: IResolvers = { interfaceTest(_root, { kind }) { return kind === 'ONE' ? { - kind: 'ONE', - testString: 'test', - foo: 'foo', - } + kind: 'ONE', + testString: 'test', + foo: 'foo', + } : { - kind: 'TWO', - testString: 'test', - bar: 'bar', - }; + kind: 'TWO', + testString: 'test', + bar: 'bar', + }; }, unionTest(_root, { output }) { return output === 'Interface' ? { - kind: 'ONE', - testString: 'test', - foo: 'foo', - } + kind: 'ONE', + testString: 'test', + foo: 'foo', + } : { - someField: 'Bar', - }; + someField: 'Bar', + }; }, errorTest() { @@ -679,7 +678,20 @@ export const subscriptionSchema: GraphQLSchema = makeExecutableSchema({ }); function makeExecutorFromSchema(schema: GraphQLSchema) { - return async ({ document, variables, context }: ExecutionParams) => { + return async ({ document, variables, context, info }: ExecutionParams) => { + if (info?.operation.operation === 'subscription') { + const result = await subscribe( + schema, + document, + null, + context, + variables, + ); + if (isAsyncIterable>(result)) { + return mapAsyncIterator, TReturn>(result, (originalResult: ExecutionResult) => JSON.parse(JSON.stringify(originalResult))); + } + return JSON.parse(JSON.stringify(result)); + } return (new ValueOrPromise(() => graphql( schema, print(document), @@ -687,24 +699,8 @@ function makeExecutorFromSchema(schema: GraphQLSchema) { context, variables, )) - .then(originalResult => JSON.parse(JSON.stringify(originalResult))) - .resolve()) as Promise> | ExecutionResult; - }; -} - -function makeSubscriberFromSchema(schema: GraphQLSchema) { - return async ({ document, variables, context }: ExecutionParams) => { - const result = await subscribe( - schema, - document, - null, - context, - variables, - ); - if (isAsyncIterable>(result)) { - return mapAsyncIterator, TReturn>(result, (originalResult: ExecutionResult) => JSON.parse(JSON.stringify(originalResult))); - } - return JSON.parse(JSON.stringify(result)); + .then(originalResult => JSON.parse(JSON.stringify(originalResult))) + .resolve()) as Promise> | ExecutionResult; }; } @@ -712,12 +708,10 @@ export async function makeSchemaRemote( schema: GraphQLSchema, ): Promise { const executor = makeExecutorFromSchema(schema); - const subscriber = makeSubscriberFromSchema(schema); const clientSchema = await introspectSchema(executor); return { schema: clientSchema, executor, - subscriber, }; } diff --git a/website/docs/remote-schemas.md b/website/docs/remote-schemas.md index a07a70c7edb..a9fbbad9687 100644 --- a/website/docs/remote-schemas.md +++ b/website/docs/remote-schemas.md @@ -10,9 +10,9 @@ There two ways to create remote schemas; ## Use Loaders to load schemas easily -Check out [Schema Loading](/docs/schema-loading) to load schemas from an URL and/or different sources easily without implementing an executor or subscriber. +Check out [Schema Loading](/docs/schema-loading) to load schemas from an URL and/or different sources easily without implementing an executor. -## Create a remote executable schema with custom executor and subscriber methods +## Create a remote executable schema with custom executor methods Generally, to create a remote schema, you generally need just three steps: @@ -20,8 +20,6 @@ Generally, to create a remote schema, you generally need just three steps: 2. Use [`introspectSchema`](#introspectschemaexecutor-context) to get the non-executable schema of the remote server 3. Use [`wrapSchema`](#wrapschemaschemaconfig) to create a schema that uses the executor to delegate requests to the underlying service -You can optionally also include a [subscriber](#creating-a-subscriber) that can retrieve real time subscription results from the remote schema (only if you are using GraphQL Subscriptions) - ### Creating an executor You can use an executor with an HTTP Client implementation (like cross-fetch). An executor is a function capable of retrieving GraphQL results. It is the same way that a GraphQL Client handles fetching data and is used by several `graphql-tools` features to do introspection or fetch results during execution. @@ -99,13 +97,9 @@ export default async () => { }; ``` -### Creating a subscriber - -For subscriptions, we need to define a subscriber that returns `AsyncIterator`. Other than that, it has the similar API with `executor`. +### Extending the executor for subscriptions -```ts -type Subscriber = (executionParams: ExecutionParams) => Promise>; -``` +For subscriptions, we need to extend our executor by returning `AsyncIterator`. #### Using `graphql-ws` @@ -113,7 +107,7 @@ For the following example to work, the server must implement the [library's tran ```ts import { wrapSchema, introspectSchema } from '@graphql-tools/wrap'; -import { Executor, Subscriber } from '@graphql-tools/delegate'; +import { Executor } from '@graphql-tools/delegate'; import { fetch } from 'cross-fetch'; import { print } from 'graphql'; import { observableToAsyncIterable } from '@graphql-tools/utils'; @@ -122,23 +116,11 @@ import { createClient } from 'graphql-ws'; const HTTP_GRAPHQL_ENDPOINT = 'http://localhost:3000/graphql'; const WS_GRAPHQL_ENDPOINT = 'ws://localhost:3000/graphql'; -const executor: Executor = async ({ document, variables }) => { - const query = print(document); - const fetchResult = await fetch(HTTP_GRAPHQL_ENDPOINT, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ query, variables }), - }); - return fetchResult.json(); -}; - const subscriptionClient = createClient({ url: WS_GRAPHQL_ENDPOINT, }); -const subscriber: Subscriber = ({ document, variables }) => +const executor: Executor = ({ document, variables }) => observableToAsyncIterable({ subscribe: observer => ({ unsubscribe: subscriptionClient.subscribe( @@ -169,7 +151,6 @@ export default async () => { const schema = wrapSchema({ schema: await introspectSchema(executor), executor, - subscriber, }); return schema; @@ -260,4 +241,4 @@ const schema = wrapSchema({ }); ``` -Note that within the `defaultCreateProxyingResolver` function, `delegateToSchema` receives `executor` and `subscriber` functions stored on the subschema config object originally passed to `wrapSchema`. As above, use of the the `createProxyingResolver` option is helpful when you want to customize additional functionality at resolver creation time. If you just want to customize how things are proxied at the time that they are proxied, you can make do just with custom executors and subscribers. +Note that within the `defaultCreateProxyingResolver` function, `delegateToSchema` receives `executor` function stored on the subschema config object originally passed to `wrapSchema`. As above, use of the the `createProxyingResolver` option is helpful when you want to customize additional functionality at resolver creation time. If you just want to customize how things are proxied at the time that they are proxied, you can make do just with custom executors. diff --git a/website/docs/stitch-combining-schemas.md b/website/docs/stitch-combining-schemas.md index 344026fc13a..648fb96bfc9 100644 --- a/website/docs/stitch-combining-schemas.md +++ b/website/docs/stitch-combining-schemas.md @@ -88,7 +88,6 @@ export interface SubschemaConfig { schema: GraphQLSchema; rootValue?: Record; executor?: Executor; - subscriber?: Subscriber; createProxyingResolver?: CreateProxyingResolverFn; transforms?: Array; merge?: Record; @@ -109,7 +108,7 @@ Also note that these subschema config objects may need to be referenced again in ## Stitching remote schemas -To include a remote schema in the combined gateway, you must provide at least the `schema` and `executor` subschema config options, and an optional `subscriber` for subscriptions: +To include a remote schema in the combined gateway, you must provide at least the `schema` and `executor` subschema config options. ```js import { introspectSchema } from '@graphql-tools/wrap'; @@ -129,13 +128,12 @@ async function remoteExecutor({ document, variables }) { export const postsSubschema = { schema: await introspectSchema(remoteExecutor), executor: remoteExecutor, - // subscriber: remoteSubscriber }; ``` - `schema`: this is a non-executable schema representing the remote API. The remote schema may be obtained using [introspection](/docs/remote-schemas/#introspectschemaexecutor-context), or fetched as a flat SDL string (from a server or repo) and built into a schema using [`buildSchema`](https://graphql.org/graphql-js/utilities/#buildschema). Note that not all GraphQL servers enable introspection, and those that do will not include custom directives. - `executor`: is a generic method that performs requests to a remote schema. It's quite simple to [write your own](/docs/remote-schemas#creating-an-executor). Subschema config uses the executor for query and mutation operations. See [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/combining-local-and-remote-schemas). -- `subscriber`: to enable subscription operations, include a [subscriber function](/docs/remote-schemas#creating-a-subscriber) that returns an AsyncIterator. See [handbook example](https://github.com/gmac/schema-stitching-handbook/tree/master/mutations-and-subscriptions). + ## Duplicate types