Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: exclude operation name via a field in RequestConfig #645

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 `ignoreOperationName` 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',
ignoreOperationName: true,
})
```

## Knowledge Base

#### Why was the file upload feature taken away? Will it return?
Expand Down
15 changes: 13 additions & 2 deletions src/graphql-ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export type SocketHandler = {
onClose?: () => any
}

export type SocketClientConfig = {
ignoreOperationName?: boolean
}

export type UnsubscribeCallback = () => void

export interface GraphQLSubscriber<T, E = unknown> {
Expand All @@ -89,11 +93,18 @@ export class GraphQLWebSocketClient {
static PROTOCOL = `graphql-transport-ws`

private socket: WebSocket
private ignoreOperationName: 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.ignoreOperationName = socketClientConfg?.ignoreOperationName

socket.addEventListener(`open`, async (e) => {
this.socketState.acknowledged = false
this.socketState.subscriptions = {}
Expand Down Expand Up @@ -236,7 +247,7 @@ export class GraphQLWebSocketClient {
subscriber: GraphQLSubscriber<T, E>,
variables?: V,
): UnsubscribeCallback {
const { query, operationName } = resolveRequestDocument(document)
const { query, operationName } = resolveRequestDocument(document, this.ignoreOperationName)
return this.makeSubscribe(query, operationName, subscriber, variables)
}

Expand Down
10 changes: 6 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,14 +203,15 @@ class GraphQLClient {
method = `POST`,
requestMiddleware,
responseMiddleware,
ignoreOperationName,
...fetchOptions
} = this.requestConfig
const { url } = this
if (rawRequestOptions.signal !== undefined) {
fetchOptions.signal = rawRequestOptions.signal
}

const { operationName } = resolveRequestDocument(rawRequestOptions.query)
const { operationName } = resolveRequestDocument(rawRequestOptions.query, ignoreOperationName)

return makeRequest<T, V>({
url,
Expand Down Expand Up @@ -261,14 +262,15 @@ class GraphQLClient {
method = `POST`,
requestMiddleware,
responseMiddleware,
ignoreOperationName,
...fetchOptions
} = this.requestConfig
const { url } = this
if (requestOptions.signal !== undefined) {
fetchOptions.signal = requestOptions.signal
}

const { query, operationName } = resolveRequestDocument(requestOptions.document)
const { query, operationName } = resolveRequestDocument(requestOptions.document, ignoreOperationName)

return makeRequest<T>({
url,
Expand Down Expand Up @@ -308,14 +310,14 @@ class GraphQLClient {
// prettier-ignore
batchRequests<T extends BatchResult, V extends Variables = Variables>(documentsOrOptions: BatchRequestDocument<V>[] | BatchRequestsOptions<V>, requestHeaders?: HeadersInit): Promise<T> {
const batchRequestOptions = parseBatchRequestArgs<V>(documentsOrOptions, requestHeaders)
const { headers, ...fetchOptions } = this.requestConfig
const { headers, ignoreOperationName, ...fetchOptions } = this.requestConfig

if (batchRequestOptions.signal !== undefined) {
fetchOptions.signal = batchRequestOptions.signal
}

const queries = batchRequestOptions.documents.map(
({ document }) => resolveRequestDocument(document).query
({ document }) => resolveRequestDocument(document, ignoreOperationName).query
)
const variables = batchRequestOptions.documents.map(({ variables }) => variables)

Expand Down
9 changes: 8 additions & 1 deletion src/resolveRequestDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ const extractOperationName = (document: DocumentNode): string | undefined => {

export const resolveRequestDocument = (
document: RequestDocument,
ignoreOperationName?: boolean,
): { query: string; operationName?: string } => {
if (typeof document === `string`) {
if (ignoreOperationName) {
return { query: document }
}

let operationName = undefined

try {
Expand All @@ -42,7 +47,9 @@ export const resolveRequestDocument = (

return { query: document, operationName }
}

if (ignoreOperationName) {
return { query: print(document) }
}
const operationName = extractOperationName(document)

return { query: print(document), operationName }
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export interface RequestConfig extends Omit<RequestInit, 'headers' | 'method'>,
requestMiddleware?: RequestMiddleware
responseMiddleware?: ResponseMiddleware
jsonSerializer?: JsonSerializer
ignoreOperationName?: boolean
}

export type BatchRequestDocument<V extends Variables = Variables> = {
Expand Down
66 changes: 65 additions & 1 deletion tests/general.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { GraphQLClient, rawRequest, request } from '../src/index.js'
import { setupMockServer } from './__helpers.js'
import { gql } from 'graphql-tag'
import type { Mock } from 'vitest'
import { beforeEach, describe, expect, it, test, vitest } from 'vitest'
import { afterAll, beforeEach, describe, expect, it, test, vitest } from 'vitest'

const ctx = setupMockServer()

Expand Down Expand Up @@ -307,6 +307,70 @@ describe(`operationName parsing`, () => {
expect(requestBody?.[`operationName`]).toEqual(`myStringOperation`)
})
})
describe(`ignoreOperationName`, () => {
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,
ignoreOperationName: 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,
ignoreOperationName: 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({
Expand Down