Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
26 changes: 26 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 @@ -539,6 +540,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
1 change: 1 addition & 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 Down
126 changes: 112 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,69 @@ 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 {
BatchRequestDocument,
BatchRequestOptions,
ClientError,
RawRequestOptions,
RequestDocument,
RequestOptions,
Variables,
} from './types'
import * as Dom from './types.dom'

export { BatchRequestDocument, ClientError, RequestDocument, Variables }
export {
BatchRequestDocument,
BatchRequestOptions,
ClientError,
RequestDocument,
Variables,
RawRequestOptions,
RequestOptions,
}

function parseRequestArgs<V = Variables>(
arg1: RequestDocument | RequestOptions,
arg2?: V,
arg3?: Dom.RequestInit['headers']
) {
return (arg1 as RequestOptions).document
? (arg1 as RequestOptions<V>)
: {
document: arg1 as RequestDocument,
variables: arg2,
requestHeaders: arg3,
signal: undefined,
}
}

function parseRawRequestArgs<V = Variables>(
arg1: RequestDocument | RawRequestOptions,
arg2?: V,
arg3?: Dom.RequestInit['headers']
) {
return (arg1 as RawRequestOptions).query
? (arg1 as RawRequestOptions<V>)
: {
query: arg1 as string,
variables: arg2,
requestHeaders: arg3,
signal: undefined,
}
}

function parseBatchRequestArgs<V = Variables>(
arg1: BatchRequestDocument<V>[] | BatchRequestOptions,
arg2?: Dom.RequestInit['headers']
) {
return (arg1 as BatchRequestOptions).documents
? (arg1 as BatchRequestOptions<V>)
: {
documents: arg1 as BatchRequestDocument<V>[],
requestHeaders: arg2,
signal: undefined,
}
}

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

/**
* todo
* GraphQL Client.
*/
export class GraphQLClient {
private url: string
Expand All @@ -163,21 +222,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
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }>
async rawRequest<T = any, V = Variables>(
arg1: RequestDocument | RawRequestOptions,
arg2?: V,
arg3?: Dom.RequestInit['headers']
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }> {
const rawRequestOptions = parseRawRequestArgs(arg1, arg2, arg3)

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 +268,30 @@ export class GraphQLClient {
document: RequestDocument,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<T>
async request<T = any, V = Variables>(options: RequestOptions): Promise<T>
async request<T = any, V = Variables>(
arg1: RequestDocument | RequestOptions,
arg2?: V,
arg3?: Dom.RequestInit['headers']
): Promise<T> {
const requestOptions = parseRequestArgs(arg1, arg2, arg3)

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 +303,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: BatchRequestOptions): Promise<T>
async batchRequests<T = any, V = Variables>(
arg1: BatchRequestDocument<V>[] | BatchRequestOptions,
arg2?: Dom.RequestInit['headers']
): Promise<T> {
const batchRequestOptions = parseBatchRequestArgs(arg1, arg2)

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
21 changes: 21 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DocumentNode } from 'graphql/language/ast'
import * as Dom from './types.dom'

export type Variables = { [key: string]: any }

Expand Down Expand Up @@ -60,3 +61,23 @@ export type BatchRequestDocument<V = Variables> = {
document: RequestDocument
variables?: V
}

export type RawRequestOptions<V = Variables> = {
query: string
variables?: V
requestHeaders?: Dom.RequestInit['headers']
signal?: Dom.RequestInit['signal']
}

export type RequestOptions<V = Variables> = {
document: RequestDocument
variables?: V
requestHeaders?: Dom.RequestInit['headers']
signal?: Dom.RequestInit['signal']
}

export type BatchRequestOptions<V = Variables> = {
documents: BatchRequestDocument<V>[]
requestHeaders?: Dom.RequestInit['headers']
signal?: Dom.RequestInit['signal']
}
14 changes: 12 additions & 2 deletions tests/__helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type MockResult<Spec extends MockSpec | MockSpecBatch = MockSpec> = {
}[]
}

export function setupTestServer<T extends MockSpec | MockSpecBatch = MockSpec>(): Context<T> {
export function setupTestServer<T extends MockSpec | MockSpecBatch = MockSpec>(delay?: number): Context<T> {
const ctx = {} as Context<T>
beforeAll(async () => {
const port = await getPort()
Expand All @@ -58,7 +58,11 @@ export function setupTestServer<T extends MockSpec | MockSpecBatch = MockSpec>()
ctx.url = `http://localhost:${port}`
ctx.res = (spec?: T): MockResult<T> => {
const requests: CapturedRequest[] = []
ctx.server.use('*', function mock(req, res) {
ctx.server.use('*', async function mock(req, res) {
if (delay) {
await sleep(delay)
}

req.headers.host = 'DYNAMIC'
requests.push({
method: req.method,
Expand Down Expand Up @@ -130,3 +134,9 @@ export function createApolloServerContext({ typeDefs, resolvers }: ApolloServerC

return ctx
}

export function sleep(timeout: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, timeout)
})
}
Loading