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!: add signal support #303

Merged
merged 12 commits into from
Jan 8, 2022
Merged
Show file tree
Hide file tree
Changes from 11 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
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
37 changes: 37 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,31 @@ 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()
```

## 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.x || 15.x"
},
"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