diff --git a/.changeset/fifty-carrots-pretend.md b/.changeset/fifty-carrots-pretend.md new file mode 100644 index 00000000000..5001357f28e --- /dev/null +++ b/.changeset/fifty-carrots-pretend.md @@ -0,0 +1,11 @@ +--- +'@graphql-tools/url-loader': major +--- + +BREAKING CHANGE +- Remove `handleSDLAsync` and `handleSDLSync`; use `handleSDL` instead +- Remove `useSSEForSubscription` and `useWebSocketLegacyProtocol`; use `subscriptionProtocol` instead +- If introspection source is different than endpoint, use `endpoint` for remote execution source +- Default HTTP Executor is renamed to `buildHTTPExecutor` with a new signature +- `build*Subscriber` methods are renamed to `buildWSLegacyExecutor`, `buildWSExecutor` and `buildSSEExecutor` with new signatures +- `getFetch` no longer takes `async` flag diff --git a/packages/loaders/url/package.json b/packages/loaders/url/package.json index f45850cc44c..20b3bbd6fb2 100644 --- a/packages/loaders/url/package.json +++ b/packages/loaders/url/package.json @@ -46,6 +46,7 @@ "@graphql-tools/utils": "^7.9.0", "@graphql-tools/wrap": "^7.0.4", "@microsoft/fetch-event-source": "2.0.1", + "@n1ru4l/graphql-live-query": "0.7.1", "@types/websocket": "1.0.2", "abort-controller": "3.0.0", "cross-fetch": "3.1.4", @@ -60,7 +61,8 @@ "sync-fetch": "0.3.0", "tslib": "~2.3.0", "valid-url": "1.0.9", - "ws": "7.5.1" + "ws": "7.5.1", + "value-or-promise": "1.0.10" }, "publishConfig": { "access": "public", diff --git a/packages/loaders/url/src/index.ts b/packages/loaders/url/src/index.ts index 6424b0bd23c..ef3cc4dd3ab 100644 --- a/packages/loaders/url/src/index.ts +++ b/packages/loaders/url/src/index.ts @@ -1,6 +1,6 @@ /* eslint-disable no-case-declarations */ /// -import { print, IntrospectionOptions, Kind, GraphQLError } from 'graphql'; +import { print, IntrospectionOptions, GraphQLError, buildASTSchema, buildSchema, getOperationAST } from 'graphql'; import { AsyncExecutor, @@ -16,11 +16,9 @@ import { mapAsyncIterator, withCancel, parseGraphQLSDL, - Maybe, } from '@graphql-tools/utils'; import { isWebUri } from 'valid-url'; import { fetch as crossFetch } from 'cross-fetch'; -import { SubschemaConfig } from '@graphql-tools/delegate'; import { introspectSchema, wrapSchema } from '@graphql-tools/wrap'; import { ClientOptions, createClient } from 'graphql-ws'; import WebSocket from 'isomorphic-ws'; @@ -33,6 +31,8 @@ import { ConnectionParamsOptions, SubscriptionClient as LegacySubscriptionClient import AbortController from 'abort-controller'; import { meros } from 'meros'; import _ from 'lodash'; +import { ValueOrPromise } from 'value-or-promise'; +import { isLiveQueryOperationDefinitionNode } from '@n1ru4l/graphql-live-query'; export type AsyncFetchFn = typeof import('cross-fetch').fetch; export type SyncFetchFn = (input: RequestInfo, init?: RequestInit) => SyncResponse; @@ -42,15 +42,6 @@ export type SyncResponse = Omit & { }; export type FetchFn = AsyncFetchFn | SyncFetchFn; -type BuildExecutorOptions = { - pointer: string; - fetch: TFetchFn; - extraHeaders?: HeadersConfig; - defaultMethod: 'GET' | 'POST'; - useGETForQueries?: Maybe; - multipart?: Maybe; -}; - // TODO: Should the types here be changed to T extends Record ? export type AsyncImportFn = (moduleName: string) => PromiseLike; // TODO: Should the types here be changed to T extends Record ? @@ -80,6 +71,18 @@ interface ExecutionExtensions { headers?: HeadersConfig; } +export enum SubscriptionProtocol { + WS = 'WS', + /** + * Use legacy web socket protocol `graphql-ws` instead of the more current standard `graphql-transport-ws` + */ + LEGACY_WS = 'LEGACY_WS', + /** + * Use SSE for subscription instead of WebSocket + */ + SSE = 'SSE', +} + /** * Additional options for loading from a URL */ @@ -110,14 +113,6 @@ export interface LoadFromUrlOptions extends SingleFileOptions, Partial { return !!isWebUri(pointer); } - async createFormDataFromVariables({ + createFormDataFromVariables({ query, variables, operationName, @@ -178,7 +181,7 @@ export class UrlLoader implements DocumentLoader { prev[currIndex] = curr; return prev; }, {}); - const uploads: any = new Map(Array.from(files.keys()).map((u, i) => [i, u])); + const uploads: Map = new Map(Array.from(files.keys()).map((u, i) => [i, u])); const form = new FormData(); form.append( 'operations', @@ -190,32 +193,35 @@ export class UrlLoader implements DocumentLoader { }) ); form.append('map', JSON.stringify(map)); - await Promise.all( - Array.from(uploads.entries()).map(async (params: unknown) => { - let [i, u] = params as any; - if (isPromise(u)) { - u = await u; - } - if (u?.promise) { - const upload = await u.promise; - const stream = upload.createReadStream(); - form.append(i.toString(), stream, { - filename: upload.filename, - contentType: upload.mimetype, - } as any); - } else { - form.append( - i.toString(), - u as any, - { - filename: 'name' in u ? u['name'] : i, - contentType: u.type, - } as any - ); - } - }) - ); - return form; + return ValueOrPromise.all( + Array.from(uploads.entries()).map(params => + new ValueOrPromise(() => { + const [i, u$] = params as any; + return new ValueOrPromise(() => u$).then(u => [i, u]).resolve(); + }).then(([i, u]) => { + if (u?.promise) { + return u.promise.then((upload: any) => { + const stream = upload.createReadStream(); + form.append(i.toString(), stream, { + filename: upload.filename, + contentType: upload.mimetype, + } as any); + }); + } else { + form.append( + i.toString(), + u as any, + { + filename: 'name' in u ? u['name'] : i, + contentType: u.type, + } as any + ); + } + }) + ) + ) + .then(() => form) + .resolve(); } prepareGETUrl({ @@ -256,17 +262,25 @@ export class UrlLoader implements DocumentLoader { return finalUrl; } - buildExecutor(options: BuildExecutorOptions): SyncExecutor; - buildExecutor(options: BuildExecutorOptions): AsyncExecutor; - buildExecutor({ - pointer, - fetch, - extraHeaders, - defaultMethod, - useGETForQueries, - multipart, - }: BuildExecutorOptions): Executor { - const HTTP_URL = switchProtocols(pointer, { + buildHTTPExecutor( + endpoint: string, + fetch: SyncFetchFn, + options?: LoadFromUrlOptions + ): SyncExecutor; + + buildHTTPExecutor( + endpoint: string, + fetch: AsyncFetchFn, + options?: LoadFromUrlOptions + ): AsyncExecutor; + + buildHTTPExecutor( + endpoint: string, + fetch: FetchFn, + options?: LoadFromUrlOptions + ): Executor { + const defaultMethod = this.getDefaultMethodFromOptions(options?.method, 'POST'); + const HTTP_URL = switchProtocols(endpoint, { wss: 'https', ws: 'http', }); @@ -278,104 +292,112 @@ export class UrlLoader implements DocumentLoader { }: ExecutionParams) => { const controller = new AbortController(); let method = defaultMethod; - if (useGETForQueries) { - method = 'GET'; - for (const definition of document.definitions) { - if (definition.kind === Kind.OPERATION_DEFINITION) { - if (definition.operation !== 'query') { - method = defaultMethod; - } - } + if (options?.useGETForQueries) { + const operationAst = getOperationAST(document, operationName); + if (operationAst?.operation === 'query') { + method = 'GET'; + } else { + method = defaultMethod; } } - const headers = Object.assign({}, extraHeaders, extensions?.headers || {}); + const headers = Object.assign({}, options?.headers, extensions?.headers || {}); - let fetchResult: SyncResponse | Promise; - const query = print(document); - switch (method) { - case 'GET': - const finalUrl = this.prepareGETUrl({ baseUrl: pointer, query, variables, operationName, extensions }); - fetchResult = fetch(finalUrl, { - method: 'GET', - credentials: 'include', - headers: { - accept: 'application/json', - ...headers, - }, - }); - break; - case 'POST': - if (multipart) { - fetchResult = this.createFormDataFromVariables({ query, variables, operationName, extensions }).then(form => - (fetch as AsyncFetchFn)(HTTP_URL, { + return new ValueOrPromise(() => { + const query = print(document); + switch (method) { + case 'GET': + const finalUrl = this.prepareGETUrl({ baseUrl: endpoint, query, variables, operationName, extensions }); + return fetch(finalUrl, { + method: 'GET', + credentials: 'include', + headers: { + accept: 'application/json', + ...headers, + }, + }); + case 'POST': + if (options?.multipart) { + return new ValueOrPromise(() => + this.createFormDataFromVariables({ query, variables, operationName, extensions }) + ) + .then( + form => + fetch(HTTP_URL, { + method: 'POST', + credentials: 'include', + body: form as any, + headers: { + accept: 'application/json', + ...headers, + }, + signal: controller.signal, + }) as any + ) + .resolve(); + } else { + return fetch(HTTP_URL, { method: 'POST', credentials: 'include', - body: form as any, + body: JSON.stringify({ + query, + variables, + operationName, + extensions, + }), headers: { - accept: 'application/json', + accept: 'application/json, multipart/mixed', + 'content-type': 'application/json', ...headers, }, signal: controller.signal, - }) - ); - } else { - fetchResult = fetch(HTTP_URL, { - method: 'POST', - credentials: 'include', - body: JSON.stringify({ - query, - variables, - operationName, - extensions, - }), - headers: { - accept: 'application/json, multipart/mixed', - 'content-type': 'application/json', - ...headers, - }, - signal: controller.signal, - }); - } - break; - } - if (isPromise(fetchResult)) { - return fetchResult.then(async res => { + }); + } + } + }) + .then((fetchResult: Response) => { const response: ExecutionResult = {}; - const maybeStream = await meros(res); - if (isAsyncIterable(maybeStream)) { - return withCancel( - mapAsyncIterator(maybeStream, part => { - if (part.json) { - const chunk = part.body; - if (chunk.path) { - if (chunk.data) { - const path: Array = ['data']; - _.merge(response, _.set({}, path.concat(chunk.path), chunk.data)); - } - - if (chunk.errors) { - response.errors = (response.errors || []).concat(chunk.errors); - } - } else { - if (chunk.data) { - response.data = chunk.data; + const contentType = fetchResult.headers.get + ? fetchResult.headers.get('content-type') + : fetchResult['content-type']; + if (contentType?.includes('multipart/mixed')) { + return meros(fetchResult).then(maybeStream => { + if (isAsyncIterable(maybeStream)) { + return withCancel( + mapAsyncIterator(maybeStream, part => { + if (part.json) { + const chunk = part.body; + if (chunk.path) { + if (chunk.data) { + const path: Array = ['data']; + _.merge(response, _.set({}, path.concat(chunk.path), chunk.data)); + } + + if (chunk.errors) { + response.errors = (response.errors || []).concat(chunk.errors); + } + } else { + if (chunk.data) { + response.data = chunk.data; + } + if (chunk.errors) { + response.errors = chunk.errors; + } + } + return response; } - if (chunk.errors) { - response.errors = chunk.errors; - } - } - return response; - } - }), - () => controller.abort() - ); - } else { - return maybeStream.json(); + }), + () => controller.abort() + ); + } else { + return maybeStream.json(); + } + }); } - }); - } - return fetchResult.json(); + + return fetchResult.json(); + }) + .resolve(); }; return executor; @@ -447,18 +469,17 @@ export class UrlLoader implements DocumentLoader { } buildSSEExecutor( - pointer: string, - extraHeaders: HeadersConfig | undefined, + endpoint: string, fetch: AsyncFetchFn, - options: Maybe + options?: Omit ): AsyncExecutor { return async ({ document, variables, extensions }) => { const controller = new AbortController(); const query = print(document); - const finalUrl = this.prepareGETUrl({ baseUrl: pointer, query, variables }); + const finalUrl = this.prepareGETUrl({ baseUrl: endpoint, query, variables }); return observableToAsyncIterable({ subscribe: observer => { - const headers = Object.assign({}, extraHeaders || {}, extensions?.headers || {}); + const headers = Object.assign({}, options?.headers || {}, extensions?.headers || {}); fetchEventSource(finalUrl, { credentials: 'include', headers, @@ -489,7 +510,7 @@ export class UrlLoader implements DocumentLoader { }, fetch, signal: controller.signal, - ...options, + ...(options?.eventSourceOptions || {}), }); return { unsubscribe: () => controller.abort(), @@ -499,18 +520,13 @@ export class UrlLoader implements DocumentLoader { }; } - getFetch( - customFetch: LoadFromUrlOptions['customFetch'], - importFn: AsyncImportFn, - async: true - ): PromiseLike; + getFetch(customFetch: LoadFromUrlOptions['customFetch'], importFn: AsyncImportFn): PromiseLike; - getFetch(customFetch: LoadFromUrlOptions['customFetch'], importFn: SyncImportFn, async: false): SyncFetchFn; + getFetch(customFetch: LoadFromUrlOptions['customFetch'], importFn: SyncImportFn): SyncFetchFn; getFetch( customFetch: LoadFromUrlOptions['customFetch'], - importFn: SyncImportFn | AsyncImportFn, - async: boolean + importFn: SyncImportFn | AsyncImportFn ): SyncFetchFn | PromiseLike { if (customFetch) { if (typeof customFetch === 'string') { @@ -525,7 +541,7 @@ export class UrlLoader implements DocumentLoader { return customFetch as any; } } - return async ? (typeof fetch === 'undefined' ? crossFetch : fetch) : syncFetch; + return importFn === asyncImport ? (typeof fetch === 'undefined' ? crossFetch : fetch) : syncFetch; } private getDefaultMethodFromOptions(method: LoadFromUrlOptions['method'], defaultMethod: 'GET' | 'POST') { @@ -535,13 +551,13 @@ export class UrlLoader implements DocumentLoader { return defaultMethod; } - getWebSocketImpl(options: LoadFromUrlOptions, importFn: AsyncImportFn): PromiseLike; + getWebSocketImpl(importFn: AsyncImportFn, options?: LoadFromUrlOptions): PromiseLike; - getWebSocketImpl(options: LoadFromUrlOptions, importFn: SyncImportFn): typeof WebSocket; + getWebSocketImpl(importFn: SyncImportFn, options?: LoadFromUrlOptions): typeof WebSocket; getWebSocketImpl( - options: LoadFromUrlOptions, - importFn: SyncImportFn | AsyncImportFn + importFn: SyncImportFn | AsyncImportFn, + options?: LoadFromUrlOptions ): typeof WebSocket | PromiseLike { if (typeof options?.webSocketImpl === 'string') { const [moduleName, webSocketImplName] = options.webSocketImpl.split('#'); @@ -552,133 +568,146 @@ export class UrlLoader implements DocumentLoader { return webSocketImplName ? (importedModule as Record)[webSocketImplName] : importedModule; } } else { - const websocketImpl = options.webSocketImpl || WebSocket; + const websocketImpl = options?.webSocketImpl || WebSocket; return websocketImpl; } } - async getExecutorAsync(pointer: SchemaPointerSingle, options: LoadFromUrlOptions = {}): Promise { - const fetch = await this.getFetch(options.customFetch, asyncImport, true); - const defaultMethod = this.getDefaultMethodFromOptions(options.method, 'POST'); - - const httpExecutor = this.buildExecutor({ - pointer, - fetch, - extraHeaders: options.headers, - defaultMethod, - useGETForQueries: options.useGETForQueries, - multipart: options.multipart, - }); - - let subscriptionExecutor: AsyncExecutor; - - const subscriptionsEndpoint = options.subscriptionsEndpoint || pointer; - if (options.useSSEForSubscription) { - subscriptionExecutor = this.buildSSEExecutor( - subscriptionsEndpoint, - options.headers, - fetch, - options.eventSourceOptions - ); + async buildSubscriptionExecutor( + subscriptionsEndpoint: string, + fetch: AsyncFetchFn, + options?: Omit + ): Promise { + if (options?.subscriptionsProtocol === SubscriptionProtocol.SSE) { + return this.buildSSEExecutor(subscriptionsEndpoint, fetch, options); } else { - const webSocketImpl = await this.getWebSocketImpl(options, asyncImport); - const connectionParams = () => ({ headers: options.headers }); - if (options.useWebSocketLegacyProtocol) { - subscriptionExecutor = this.buildWSLegacyExecutor(subscriptionsEndpoint, webSocketImpl, connectionParams); + const webSocketImpl = await this.getWebSocketImpl(asyncImport, options); + const connectionParams = () => ({ headers: options?.headers }); + if (options?.subscriptionsProtocol === SubscriptionProtocol.LEGACY_WS) { + return this.buildWSLegacyExecutor(subscriptionsEndpoint, webSocketImpl, connectionParams); } else { - subscriptionExecutor = this.buildWSExecutor(subscriptionsEndpoint, webSocketImpl, connectionParams); + return this.buildWSExecutor(subscriptionsEndpoint, webSocketImpl, connectionParams); } } + } + + async getExecutorAsync(endpoint: string, options?: Omit): Promise { + const fetch = await this.getFetch(options?.customFetch, asyncImport); + const httpExecutor = this.buildHTTPExecutor(endpoint, fetch, options); + const subscriptionsEndpoint = options?.subscriptionsEndpoint || endpoint; + const subscriptionExecutor = await this.buildSubscriptionExecutor(subscriptionsEndpoint, fetch, options); return params => { - if (params.info?.operation.operation === 'subscription') { + const operationAst = getOperationAST(params.document, params.operationName); + if (!operationAst) { + throw new Error(`No valid operations found: ${params.operationName || ''}`); + } + if ( + operationAst.operation === 'subscription' || + isLiveQueryOperationDefinitionNode(operationAst, params.variables as Record) + ) { return subscriptionExecutor(params); } return httpExecutor(params); }; } - getExecutorSync(pointer: SchemaPointerSingle, options: LoadFromUrlOptions): SyncExecutor { - const fetch = this.getFetch(options?.customFetch, syncImport, false); - const defaultMethod = this.getDefaultMethodFromOptions(options?.method, 'POST'); - - const executor = this.buildExecutor({ - pointer, - fetch, - extraHeaders: options.headers, - defaultMethod, - useGETForQueries: options.useGETForQueries, - }); + getExecutorSync(endpoint: string, options: Omit): SyncExecutor { + const fetch = this.getFetch(options?.customFetch, syncImport); + const executor = this.buildHTTPExecutor(endpoint, fetch, options); return executor; } - async getSubschemaConfigAsync(pointer: SchemaPointerSingle, options: LoadFromUrlOptions): Promise { - const executor = await this.getExecutorAsync(pointer, options); - return { - schema: await introspectSchema(executor, undefined, options as IntrospectionOptions), - executor, - }; - } - - getSubschemaConfigSync(pointer: SchemaPointerSingle, options: LoadFromUrlOptions): SubschemaConfig { - const executor = this.getExecutorSync(pointer, options); - return { - schema: introspectSchema(executor, undefined, options as IntrospectionOptions), - executor, - }; - } - - async handleSDLAsync(pointer: SchemaPointerSingle, options: LoadFromUrlOptions): Promise { - const fetch = await this.getFetch(options?.customFetch, asyncImport, true); - const defaultMethod = this.getDefaultMethodFromOptions(options?.method, 'GET'); - const response = await fetch(pointer, { - method: defaultMethod, - headers: options.headers, - }); - const schemaString = await response.text(); - return parseGraphQLSDL(pointer, schemaString, options); - } - - handleSDLSync(pointer: SchemaPointerSingle, options: LoadFromUrlOptions): Source { - const fetch = this.getFetch(options?.customFetch, syncImport, false); + handleSDL(pointer: SchemaPointerSingle, fetch: SyncFetchFn, options: LoadFromUrlOptions): Source; + handleSDL(pointer: SchemaPointerSingle, fetch: AsyncFetchFn, options: LoadFromUrlOptions): Promise; + handleSDL(pointer: SchemaPointerSingle, fetch: FetchFn, options: LoadFromUrlOptions): Source | Promise { const defaultMethod = this.getDefaultMethodFromOptions(options?.method, 'GET'); - const response = fetch(pointer, { - method: defaultMethod, - headers: options.headers, - }); - const schemaString = response.text(); - return parseGraphQLSDL(pointer, schemaString, options); + return new ValueOrPromise(() => + fetch(pointer, { + method: defaultMethod, + headers: options.headers, + }) + ) + .then(response => response.text()) + .then(schemaString => parseGraphQLSDL(pointer, schemaString, options)) + .resolve(); } async load(pointer: SchemaPointerSingle, options: LoadFromUrlOptions): Promise { + let source: Source = { + location: pointer, + }; + const fetch = await this.getFetch(options?.customFetch, asyncImport); + let executor = await this.getExecutorAsync(pointer, options); if (options?.handleAsSDL || pointer.endsWith('.graphql')) { - return this.handleSDLAsync(pointer, options); + source = await this.handleSDL(pointer, fetch, options); + if (!source.schema && !source.document && !source.rawSDL) { + throw new Error(`Invalid SDL response`); + } + source.schema = + source.schema || + (source.document + ? buildASTSchema(source.document, options) + : source.rawSDL + ? buildSchema(source.rawSDL, options) + : undefined); + } else { + source.schema = await introspectSchema(executor, {}, options); } - const subschemaConfig = await this.getSubschemaConfigAsync(pointer, options); + if (!source.schema) { + throw new Error(`Invalid introspected schema`); + } - const remoteExecutableSchema = wrapSchema(subschemaConfig); + if (options?.endpoint) { + executor = await this.getExecutorAsync(options.endpoint, options); + } - return { - location: pointer, - schema: remoteExecutableSchema, - }; + source.schema = wrapSchema({ + schema: source.schema, + executor, + }); + + return source; } loadSync(pointer: SchemaPointerSingle, options: LoadFromUrlOptions): Source { + let source: Source = { + location: pointer, + }; + const fetch = this.getFetch(options?.customFetch, syncImport); + let executor = this.getExecutorSync(pointer, options); if (options?.handleAsSDL || pointer.endsWith('.graphql')) { - return this.handleSDLSync(pointer, options); + source = this.handleSDL(pointer, fetch, options); + if (!source.schema && !source.document && !source.rawSDL) { + throw new Error(`Invalid SDL response`); + } + source.schema = + source.schema || + (source.document + ? buildASTSchema(source.document, options) + : source.rawSDL + ? buildSchema(source.rawSDL, options) + : undefined); + } else { + source.schema = introspectSchema(executor, {}, options); } - const subschemaConfig = this.getSubschemaConfigSync(pointer, options); + if (!source.schema) { + throw new Error(`Invalid introspected schema`); + } - const remoteExecutableSchema = wrapSchema(subschemaConfig); + if (options?.endpoint) { + executor = this.getExecutorSync(options.endpoint, options); + } - return { - location: pointer, - schema: remoteExecutableSchema, - }; + source.schema = wrapSchema({ + schema: source.schema, + executor, + }); + + return source; } } diff --git a/packages/loaders/url/tests/url-loader.spec.ts b/packages/loaders/url/tests/url-loader.spec.ts index ff8d599dfc6..eb99a2e963e 100644 --- a/packages/loaders/url/tests/url-loader.spec.ts +++ b/packages/loaders/url/tests/url-loader.spec.ts @@ -1,6 +1,6 @@ import '../../../testing/to-be-similar-gql-doc'; import { makeExecutableSchema } from '@graphql-tools/schema'; -import { UrlLoader } from '../src'; +import { SubscriptionProtocol, UrlLoader } from '../src'; import { printSchemaWithDirectives } from '@graphql-tools/utils'; import nock from 'nock'; import { mockGraphQLServer } from '../../../testing/utils'; @@ -305,6 +305,7 @@ input TestInput { data: introspectionFromSchema(testSchema), }) }) as any, + subscriptionsProtocol: SubscriptionProtocol.WS }); const httpServer = http.createServer(function weServeSocketsOnly(_, res) { @@ -374,7 +375,7 @@ input TestInput { data: introspectionFromSchema(testSchema), }) }) as any, - useWebSocketLegacyProtocol: true, + subscriptionsProtocol: SubscriptionProtocol.LEGACY_WS }); const httpServer = http.createServer(function weServeSocketsOnly(_, res) { diff --git a/packages/testing/utils.ts b/packages/testing/utils.ts index f761e79ddff..3f4172e4b17 100644 --- a/packages/testing/utils.ts +++ b/packages/testing/utils.ts @@ -142,9 +142,9 @@ export function mockGraphQLServer({ // 2) MULTIPART RESPONSE: a multipart response (when @stream or @defer directives are used) // 3) PUSH: a stream of events to push back down the client for a subscription if (result.type === 'RESPONSE') { - const headers = {}; + const headers = new Map(); // We set the provided status and headers and just the send the payload back to the client - result.headers.forEach(({ name, value }) => (headers[name] = value)); + result.headers.forEach(({ name, value }) => headers.set(name, value)); return [result.status, result.payload, headers]; } else { return [500, 'Not implemented']; diff --git a/packages/wrap/src/introspect.ts b/packages/wrap/src/introspect.ts index 7f0a4576513..6a11a74e545 100644 --- a/packages/wrap/src/introspect.ts +++ b/packages/wrap/src/introspect.ts @@ -1,20 +1,30 @@ import { GraphQLSchema, - DocumentNode, getIntrospectionQuery, buildClientSchema, parse, IntrospectionOptions, IntrospectionQuery, + ParseOptions, } from 'graphql'; import { ValueOrPromise } from 'value-or-promise'; -import { AsyncExecutor, Executor, ExecutionResult, AggregateError, isAsyncIterable } from '@graphql-tools/utils'; +import { + AsyncExecutor, + Executor, + ExecutionResult, + AggregateError, + isAsyncIterable, + SyncExecutor, +} from '@graphql-tools/utils'; -function getSchemaFromIntrospection(introspectionResult: ExecutionResult): GraphQLSchema { +function getSchemaFromIntrospection( + introspectionResult: ExecutionResult, + options?: Parameters[1] +): GraphQLSchema { if (introspectionResult?.data?.__schema) { - return buildClientSchema(introspectionResult.data); + return buildClientSchema(introspectionResult.data, options); } else if (introspectionResult?.errors?.length) { if (introspectionResult.errors.length > 1) { const combinedError = new AggregateError(introspectionResult.errors, 'Could not obtain introspection result'); @@ -27,14 +37,24 @@ function getSchemaFromIntrospection(introspectionResult: ExecutionResult( - executor: TExecutor, +export function introspectSchema( + executor: SyncExecutor, + context?: Record, + options?: Partial & Parameters[1] & ParseOptions +): GraphQLSchema; +export function introspectSchema( + executor: AsyncExecutor, + context?: Record, + options?: Partial & Parameters[1] & ParseOptions +): Promise; +export function introspectSchema( + executor: Executor, context?: Record, - options?: IntrospectionOptions -): TExecutor extends AsyncExecutor ? Promise : GraphQLSchema { - const parsedIntrospectionQuery: DocumentNode = parse(getIntrospectionQuery(options)); + options?: Partial & Parameters[1] & ParseOptions +): Promise | GraphQLSchema { + const parsedIntrospectionQuery = parse(getIntrospectionQuery(options as any), options); return new ValueOrPromise(() => - (executor as Executor)({ + executor({ document: parsedIntrospectionQuery, context, }) @@ -45,6 +65,6 @@ export function introspectSchema( } return introspection; }) - .then(introspection => getSchemaFromIntrospection(introspection)) - .resolve() as any; + .then(introspection => getSchemaFromIntrospection(introspection, options)) + .resolve(); } diff --git a/yarn.lock b/yarn.lock index b2f0387af8a..53e6767dc5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2175,6 +2175,11 @@ resolved "https://registry.yarnpkg.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d" integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA== +"@n1ru4l/graphql-live-query@0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@n1ru4l/graphql-live-query/-/graphql-live-query-0.7.1.tgz#c020d017c3ed6bcfdde49a7106ba035e4d0774f5" + integrity sha512-5kJPe2FkPNsCGu9tocKIzUSNO986qAqdnbk8hIFqWlpVPBAmEAOYb1mr6PA18FYAlu7ojWm9Hm13k29aj2GGlQ== + "@nodelib/fs.scandir@2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69"