Skip to content

Commit

Permalink
feat!: add signal support (#303)
Browse files Browse the repository at this point in the history
  • Loading branch information
arnaudbzn authored Jan 8, 2022
1 parent b693d27 commit 12c2e8d
Show file tree
Hide file tree
Showing 9 changed files with 575 additions and 25 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules
dist
.DS_Store
*.log
coverage
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Minimal GraphQL client supporting Node and browsers for scripts or simple apps
- [Browser](#browser)
- [Node](#node)
- [Batching](#batching)
- [Cancellation](#cancellation)
- [FAQ](#faq)
- [Why do I have to install `graphql`?](#why-do-i-have-to-install-graphql)
- [Do I need to wrap my GraphQL documents inside the `gql` template exported by `graphql-request`?](#do-i-need-to-wrap-my-graphql-documents-inside-the-gql-template-exported-by-graphql-request)
Expand Down Expand Up @@ -85,6 +86,17 @@ const client = new GraphQLClient(endpoint, { headers: {} })
client.request(query, variables).then((data) => console.log(data))
```

You can also use the single argument function variant:

```js
request({
url: endpoint,
document: query,
variables: variables,
requestHeaders: headers,
}).then((data) => console.log(data))
```

## Node Version Support

We only officially support [LTS Node versions](https://github.com/nodejs/Release#release-schedule). We also make an effort to support two additional versions:
Expand Down Expand Up @@ -539,6 +551,40 @@ import { batchRequests } from 'graphql-request';
})().catch((error) => console.error(error))
```

### Cancellation

It is possible to cancel a request using an `AbortController` signal.

You can define the `signal` in the `GraphQLClient` constructor:

```ts
const abortController = new AbortController()

const client = new GraphQLClient(endpoint, { signal: abortController.signal })
client.request(query)

abortController.abort()
```

You can also set the signal per request (this will override an existing GraphQLClient signal):

```ts
const abortController = new AbortController()

const client = new GraphQLClient(endpoint)
client.request({ document: query, signal: abortController.signal })

abortController.abort()
```

In Node environment, `AbortController` is supported since version v14.17.0.
For Node.js v12 you can use [abort-controller](https://github.com/mysticatea/abort-controller) polyfill.

````
import 'abort-controller/polyfill'
const abortController = new AbortController()
````

## FAQ

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"test:node": "jest --testEnvironment node",
"test:dom": "jest --testEnvironment jsdom",
"test": "yarn test:node && yarn test:dom",
"test:coverage": "yarn test --coverage",
"release:stable": "dripip stable",
"release:preview": "dripip preview",
"release:pr": "dripip pr"
Expand All @@ -51,6 +52,7 @@
"graphql": "14 - 16"
},
"devDependencies": {
"abort-controller": "^3.0.0",
"@prisma-labs/prettier-config": "^0.1.0",
"@types/body-parser": "^1.19.1",
"@types/express": "^4.17.13",
Expand Down
146 changes: 123 additions & 23 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,40 @@ import crossFetch, * as CrossFetch from 'cross-fetch'
import { OperationDefinitionNode } from 'graphql/language/ast'
import { print } from 'graphql/language/printer'
import createRequestBody from './createRequestBody'
import { BatchRequestDocument, ClientError, RequestDocument, Variables } from './types'
import {
parseBatchRequestArgs,
parseRawRequestArgs,
parseRequestArgs,
parseBatchRequestsExtendedArgs,
parseRawRequestExtendedArgs,
parseRequestExtendedArgs,
} from './parseArgs'
import {
BatchRequestDocument,
BatchRequestsOptions,
ClientError,
RawRequestOptions,
RequestDocument,
RequestOptions,
BatchRequestsExtendedOptions,
RawRequestExtendedOptions,
RequestExtendedOptions,
Variables,
} from './types'
import * as Dom from './types.dom'

export { BatchRequestDocument, ClientError, RequestDocument, Variables }
export {
BatchRequestDocument,
BatchRequestsOptions,
BatchRequestsExtendedOptions,
ClientError,
RawRequestOptions,
RawRequestExtendedOptions,
RequestDocument,
RequestOptions,
RequestExtendedOptions,
Variables,
}

/**
* Convert the given headers configuration into a plain object.
Expand Down Expand Up @@ -152,7 +182,7 @@ const get = async <V = Variables>({
}

/**
* todo
* GraphQL Client.
*/
export class GraphQLClient {
private url: string
Expand All @@ -163,21 +193,37 @@ export class GraphQLClient {
this.options = options || {}
}

rawRequest<T = any, V = Variables>(
/**
* Send a GraphQL query to the server.
*/
async rawRequest<T = any, V = Variables>(
query: string,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }>
async rawRequest<T = any, V = Variables>(
options: RawRequestOptions<V>
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }>
async rawRequest<T = any, V = Variables>(
queryOrOptions: string | RawRequestOptions<V>,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }> {
const rawRequestOptions = parseRawRequestArgs<V>(queryOrOptions, variables, requestHeaders)

let { headers, fetch = crossFetch, method = 'POST', ...fetchOptions } = this.options
let { url } = this
if (rawRequestOptions.signal !== undefined) {
fetchOptions.signal = rawRequestOptions.signal
}

return makeRequest<T, V>({
url,
query,
variables,
query: rawRequestOptions.query,
variables: rawRequestOptions.variables,
headers: {
...resolveHeaders(headers),
...resolveHeaders(requestHeaders),
...resolveHeaders(rawRequestOptions.requestHeaders),
},
operationName: undefined,
fetch,
Expand All @@ -193,19 +239,30 @@ export class GraphQLClient {
document: RequestDocument,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<T>
async request<T = any, V = Variables>(options: RequestOptions<V>): Promise<T>
async request<T = any, V = Variables>(
documentOrOptions: RequestDocument | RequestOptions<V>,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<T> {
const requestOptions = parseRequestArgs<V>(documentOrOptions, variables, requestHeaders)

let { headers, fetch = crossFetch, method = 'POST', ...fetchOptions } = this.options
let { url } = this
if (requestOptions.signal !== undefined) {
fetchOptions.signal = requestOptions.signal
}

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

const { data } = await makeRequest<T, V>({
url,
query,
variables,
variables: requestOptions.variables,
headers: {
...resolveHeaders(headers),
...resolveHeaders(requestHeaders),
...resolveHeaders(requestOptions.requestHeaders),
},
operationName,
fetch,
Expand All @@ -217,25 +274,37 @@ export class GraphQLClient {
}

/**
* Send a GraphQL document to the server.
* Send GraphQL documents in batch to the server.
*/
async batchRequests<T extends any = any, V = Variables>(
documents: BatchRequestDocument<V>[],
requestHeaders?: Dom.RequestInit['headers']
): Promise<T>
async batchRequests<T = any, V = Variables>(options: BatchRequestsOptions<V>): Promise<T>
async batchRequests<T = any, V = Variables>(
documentsOrOptions: BatchRequestDocument<V>[] | BatchRequestsOptions<V>,
requestHeaders?: Dom.RequestInit['headers']
): Promise<T> {
const batchRequestOptions = parseBatchRequestArgs<V>(documentsOrOptions, requestHeaders)

let { headers, fetch = crossFetch, method = 'POST', ...fetchOptions } = this.options
let { url } = this
if (batchRequestOptions.signal !== undefined) {
fetchOptions.signal = batchRequestOptions.signal
}

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

const { data } = await makeRequest<T, (V | undefined)[]>({
url,
query: queries,
variables,
headers: {
...resolveHeaders(headers),
...resolveHeaders(requestHeaders),
...resolveHeaders(batchRequestOptions.requestHeaders),
},
operationName: undefined,
fetch,
Expand Down Expand Up @@ -330,20 +399,32 @@ async function makeRequest<T = any, V = Variables>({
}

/**
* todo
* Send a GraphQL Query to the GraphQL server for execution.
*/
export async function rawRequest<T = any, V = Variables>(
url: string,
query: string,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }>
export async function rawRequest<T = any, V = Variables>(
options: RawRequestExtendedOptions<V>
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }>
export async function rawRequest<T = any, V = Variables>(
urlOrOptions: string | RawRequestExtendedOptions<V>,
query?: string,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }> {
const client = new GraphQLClient(url)
return client.rawRequest<T, V>(query, variables, requestHeaders)
const requestOptions = parseRawRequestExtendedArgs<V>(urlOrOptions, query, variables, requestHeaders)
const client = new GraphQLClient(requestOptions.url)
return client.rawRequest<T, V>({
...requestOptions,
})
}

/**
* Send a GraphQL Document to the GraphQL server for exectuion.
* Send a GraphQL Document to the GraphQL server for execution.
*
* @example
*
Expand Down Expand Up @@ -381,9 +462,19 @@ export async function request<T = any, V = Variables>(
document: RequestDocument,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<T>
export async function request<T = any, V = Variables>(options: RequestExtendedOptions<V>): Promise<T>
export async function request<T = any, V = Variables>(
urlOrOptions: string | RequestExtendedOptions<V>,
document?: RequestDocument,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<T> {
const client = new GraphQLClient(url)
return client.request<T, V>(document, variables, requestHeaders)
const requestOptions = parseRequestExtendedArgs<V>(urlOrOptions, document, variables, requestHeaders)
const client = new GraphQLClient(requestOptions.url)
return client.request<T, V>({
...requestOptions,
})
}

/**
Expand Down Expand Up @@ -420,13 +511,22 @@ export async function request<T = any, V = Variables>(
* await batchRequests('https://foo.bar/graphql', [{ query: gql`...` }])
* ```
*/
export async function batchRequests<T extends any = any, V = Variables>(
export async function batchRequests<T = any, V = Variables>(
url: string,
documents: BatchRequestDocument<V>[],
requestHeaders?: Dom.RequestInit['headers']
): Promise<T>
export async function batchRequests<T = any, V = Variables>(
options: BatchRequestsExtendedOptions<V>
): Promise<T>
export async function batchRequests<T = any, V = Variables>(
urlOrOptions: string | BatchRequestsExtendedOptions<V>,
documents?: BatchRequestDocument<V>[],
requestHeaders?: Dom.RequestInit['headers']
): Promise<T> {
const client = new GraphQLClient(url)
return client.batchRequests<T, V>(documents, requestHeaders)
const requestOptions = parseBatchRequestsExtendedArgs<V>(urlOrOptions, documents, requestHeaders)
const client = new GraphQLClient(requestOptions.url)
return client.batchRequests<T, V>({ ...requestOptions })
}

export default request
Expand Down
Loading

0 comments on commit 12c2e8d

Please sign in to comment.