Skip to content

Commit

Permalink
feat: exclude operation name via a field in RequestConfig (#645)
Browse files Browse the repository at this point in the history
Co-authored-by: Roberto Badalamenti <r.badalamenti@replay.it>
  • Loading branch information
robertobadalamenti and Roberto Badalamenti authored Feb 1, 2024
1 parent 8e06b6e commit 0f1b7b5
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 13 deletions.
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 `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?
Expand Down
10 changes: 6 additions & 4 deletions src/classes/GraphQLClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@ export class GraphQLClient {
method = `POST`,
requestMiddleware,
responseMiddleware,
excludeOperationName,
...fetchOptions
} = this.requestConfig
const { url } = this
if (rawRequestOptions.signal !== undefined) {
fetchOptions.signal = rawRequestOptions.signal
}

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

return makeRequest<T, V>({
url,
Expand Down Expand Up @@ -108,14 +109,15 @@ export class GraphQLClient {
method = `POST`,
requestMiddleware,
responseMiddleware,
excludeOperationName,
...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, excludeOperationName)

return makeRequest<T>({
url,
Expand Down Expand Up @@ -155,14 +157,14 @@ export 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, 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)

Expand Down
9 changes: 8 additions & 1 deletion src/helpers/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,
excludeOperationName?: boolean,
): { query: string; operationName?: string } => {
if (typeof document === `string`) {
if (excludeOperationName) {
return { query: document }
}

let operationName = undefined

try {
Expand All @@ -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 }
Expand Down
9 changes: 5 additions & 4 deletions src/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export interface RequestConfig extends Omit<RequestInit, 'headers' | 'method'>,
requestMiddleware?: RequestMiddleware
responseMiddleware?: ResponseMiddleware
jsonSerializer?: JsonSerializer
excludeOperationName?: boolean
}

export type RawRequestOptions<V extends Variables = Variables> = {
Expand All @@ -104,8 +105,8 @@ export type RawRequestOptions<V extends Variables = Variables> = {
} & (V extends Record<any, never>
? { variables?: V }
: keyof RemoveIndex<V> extends never
? { variables?: V }
: { variables: V })
? { variables?: V }
: { variables: V })

export type RequestOptions<V extends Variables = Variables, T = unknown> = {
document: RequestDocument | TypedDocumentNode<T, V>
Expand All @@ -114,8 +115,8 @@ export type RequestOptions<V extends Variables = Variables, T = unknown> = {
} & (V extends Record<any, never>
? { variables?: V }
: keyof RemoveIndex<V> extends never
? { variables?: V }
: { variables: V })
? { variables?: V }
: { variables: V })

export type ResponseMiddleware = (response: GraphQLClientResponse<unknown> | ClientError | Error) => void

Expand Down
15 changes: 13 additions & 2 deletions src/lib/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 = {
excludeOperationName?: 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 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 = {}
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.excludeOperationName)
return this.makeSubscribe(query, operationName, subscriber, variables)
}

Expand Down
64 changes: 64 additions & 0 deletions tests/general.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@

// Other
"skipLibCheck": true,
"esModuleInterop": true,
"esModuleInterop": true
},
"include": ["src", "tests", "examples"],
"exclude": ["build"],
"exclude": ["build"]
}

0 comments on commit 0f1b7b5

Please sign in to comment.