diff --git a/README.md b/README.md index 73b1e3088..4e4da066d 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Minimal GraphQL client supporting Node and browsers for scripts or simple apps - [None (default)](#none-default) - [Ignore](#ignore) - [All](#all) + - [IgnoreOperationName](#ignoreoperationname) - [Knowledge Base](#knowledge-base) - [Why was the file upload feature taken away? Will it return?](#why-was-the-file-upload-feature-taken-away-will-it-return) - [Why do I have to install `graphql`?](#why-do-i-have-to-install-graphql) @@ -152,6 +153,26 @@ Ignore incoming errors and resolve like no errors occurred Return both the errors and data, only works with `rawRequest`. +### IgnoreOperationName + +OperationName has been introduced to address issues reported here [Support operation name](https://github.com/jasonkuhrt/graphql-request/issues/64), +However, on certain occasions this information may not be needed in requests. In such cases, you might consider ignoring operationName to avoid the extraction steps currently performed by a parsing operation when the document is provided in string format. + +By default the GraphQLClient tries to extract the operationName from the document. +You can define `excludeOperationName` in the constructor of GraphQLClient to avoid the extraction process if it is not needed. This can be useful if you don't use operationName and want to optimise queries by reducing the amount of computation as much as possible, especially if we are in a context where we are using documents in string format to reduce bundle size. + +```ts +// example where the operation name is not ignored +const client = new GraphQLClient(endpoint, { + method: 'POST', +}) +// example in which the operation name is ignored +const client = new GraphQLClient(endpoint, { + method: 'POST', + excludeOperationName: true, +}) +``` + ## Knowledge Base #### Why was the file upload feature taken away? Will it return? diff --git a/src/classes/GraphQLClient.ts b/src/classes/GraphQLClient.ts index 00d8db2d1..1089299f9 100644 --- a/src/classes/GraphQLClient.ts +++ b/src/classes/GraphQLClient.ts @@ -50,6 +50,7 @@ export class GraphQLClient { method = `POST`, requestMiddleware, responseMiddleware, + excludeOperationName, ...fetchOptions } = this.requestConfig const { url } = this @@ -57,7 +58,7 @@ export class GraphQLClient { fetchOptions.signal = rawRequestOptions.signal } - const { operationName } = resolveRequestDocument(rawRequestOptions.query) + const { operationName } = resolveRequestDocument(rawRequestOptions.query, excludeOperationName) return makeRequest({ url, @@ -108,6 +109,7 @@ export class GraphQLClient { method = `POST`, requestMiddleware, responseMiddleware, + excludeOperationName, ...fetchOptions } = this.requestConfig const { url } = this @@ -115,7 +117,7 @@ export class GraphQLClient { fetchOptions.signal = requestOptions.signal } - const { query, operationName } = resolveRequestDocument(requestOptions.document) + const { query, operationName } = resolveRequestDocument(requestOptions.document, excludeOperationName) return makeRequest({ url, @@ -155,14 +157,14 @@ export class GraphQLClient { // prettier-ignore batchRequests(documentsOrOptions: BatchRequestDocument[] | BatchRequestsOptions, requestHeaders?: HeadersInit): Promise { const batchRequestOptions = parseBatchRequestArgs(documentsOrOptions, requestHeaders) - const { headers, ...fetchOptions } = this.requestConfig + const { headers, excludeOperationName, ...fetchOptions } = this.requestConfig if (batchRequestOptions.signal !== undefined) { fetchOptions.signal = batchRequestOptions.signal } const queries = batchRequestOptions.documents.map( - ({ document }) => resolveRequestDocument(document).query + ({ document }) => resolveRequestDocument(document, excludeOperationName).query ) const variables = batchRequestOptions.documents.map(({ variables }) => variables) diff --git a/src/helpers/resolveRequestDocument.ts b/src/helpers/resolveRequestDocument.ts index e6369e6ff..666069b9d 100644 --- a/src/helpers/resolveRequestDocument.ts +++ b/src/helpers/resolveRequestDocument.ts @@ -29,8 +29,13 @@ const extractOperationName = (document: DocumentNode): string | undefined => { export const resolveRequestDocument = ( document: RequestDocument, + excludeOperationName?: boolean, ): { query: string; operationName?: string } => { if (typeof document === `string`) { + if (excludeOperationName) { + return { query: document } + } + let operationName = undefined try { @@ -42,7 +47,9 @@ export const resolveRequestDocument = ( return { query: document, operationName } } - + if (excludeOperationName) { + return { query: print(document) } + } const operationName = extractOperationName(document) return { query: print(document), operationName } diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 331002b1b..0358523f7 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -95,6 +95,7 @@ export interface RequestConfig extends Omit, requestMiddleware?: RequestMiddleware responseMiddleware?: ResponseMiddleware jsonSerializer?: JsonSerializer + excludeOperationName?: boolean } export type RawRequestOptions = { @@ -104,8 +105,8 @@ export type RawRequestOptions = { } & (V extends Record ? { variables?: V } : keyof RemoveIndex extends never - ? { variables?: V } - : { variables: V }) + ? { variables?: V } + : { variables: V }) export type RequestOptions = { document: RequestDocument | TypedDocumentNode @@ -114,8 +115,8 @@ export type RequestOptions = { } & (V extends Record ? { variables?: V } : keyof RemoveIndex extends never - ? { variables?: V } - : { variables: V }) + ? { variables?: V } + : { variables: V }) export type ResponseMiddleware = (response: GraphQLClientResponse | ClientError | Error) => void diff --git a/src/lib/graphql-ws.ts b/src/lib/graphql-ws.ts index 12aeac63c..c0d6112d3 100644 --- a/src/lib/graphql-ws.ts +++ b/src/lib/graphql-ws.ts @@ -65,6 +65,10 @@ export type SocketHandler = { onClose?: () => any } +export type SocketClientConfig = { + excludeOperationName?: boolean +} + export type UnsubscribeCallback = () => void export interface GraphQLSubscriber { @@ -89,11 +93,18 @@ export class GraphQLWebSocketClient { static PROTOCOL = `graphql-transport-ws` private socket: WebSocket + private excludeOperationName: boolean | undefined private socketState: SocketState = { acknowledged: false, lastRequestId: 0, subscriptions: {} } - constructor(socket: WebSocket, { onInit, onAcknowledged, onPing, onPong }: SocketHandler) { + constructor( + socket: WebSocket, + { onInit, onAcknowledged, onPing, onPong }: SocketHandler, + socketClientConfg?: SocketClientConfig, + ) { this.socket = socket + this.excludeOperationName = socketClientConfg?.excludeOperationName + socket.addEventListener(`open`, async (e) => { this.socketState.acknowledged = false this.socketState.subscriptions = {} @@ -236,7 +247,7 @@ export class GraphQLWebSocketClient { subscriber: GraphQLSubscriber, variables?: V, ): UnsubscribeCallback { - const { query, operationName } = resolveRequestDocument(document) + const { query, operationName } = resolveRequestDocument(document, this.excludeOperationName) return this.makeSubscribe(query, operationName, subscriber, variables) } diff --git a/tests/general.test.ts b/tests/general.test.ts index 5bfc88f81..30d25273b 100644 --- a/tests/general.test.ts +++ b/tests/general.test.ts @@ -307,6 +307,70 @@ describe(`operationName parsing`, () => { expect(requestBody?.[`operationName`]).toEqual(`myStringOperation`) }) }) +describe(`excludeOperationName`, () => { + it(`it should not ignore operation name by default`, async () => { + ctx.res({ + body: { + data: { + result: `ok`, + }, + }, + }) + const requestMiddleware: Mock = vitest.fn((req: { body: string; operationName: string }) => { + expect(req.body).toContain(`"operationName":"myStringOperation"`) + expect(req.operationName).toBe(`myStringOperation`) + return { ...req } + }) + const client: GraphQLClient = new GraphQLClient(ctx.url, { + requestMiddleware, + }) + await client.request<{ result: number }>(`query myStringOperation { + users + }`) + }) + it(`it should not ignore operation name`, async () => { + ctx.res({ + body: { + data: { + result: `ok`, + }, + }, + }) + const requestMiddleware: Mock = vitest.fn((req: { body: string; operationName: string }) => { + expect(req.body).toContain(`"operationName":"myStringOperation"`) + expect(req.operationName).toBe(`myStringOperation`) + return { ...req } + }) + const client: GraphQLClient = new GraphQLClient(ctx.url, { + requestMiddleware, + excludeOperationName: false, + }) + await client.request<{ result: number }>(`query myStringOperation { + users + }`) + }) + it(`it should ignore operation name`, async () => { + ctx.res({ + body: { + data: { + result: `ok`, + }, + }, + }) + const requestMiddleware: Mock = vitest.fn((req: { body: string; operationName: string }) => { + expect(req.body).not.toContain(`operationName`) + expect(req.operationName).toBe(undefined) + return { ...req } + }) + const client: GraphQLClient = new GraphQLClient(ctx.url, { + requestMiddleware, + excludeOperationName: true, + }) + await client.request<{ result: number }>(`query myStringOperation { + users + }`) + }) +}) test(`should not throw error when errors property is an empty array (occurred when using UltraGraphQL)`, async () => { ctx.res({ diff --git a/tsconfig.json b/tsconfig.json index f4887aca8..673dc5feb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,8 +23,8 @@ // Other "skipLibCheck": true, - "esModuleInterop": true, + "esModuleInterop": true }, "include": ["src", "tests", "examples"], - "exclude": ["build"], + "exclude": ["build"] }