Skip to content

Commit

Permalink
feat: add error policy (#352)
Browse files Browse the repository at this point in the history
  • Loading branch information
wesley079 committed May 26, 2022
1 parent deae89c commit 10a5896
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 8 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,24 @@ For Node.js v12 you can use [abort-controller](https://github.com/mysticatea/abo
const abortController = new AbortController()
````

### ErrorPolicy

By default GraphQLClient will throw when an error is received. However, sometimes you still want to resolve the (partial) data you received.
You can define `errorPolicy` in the `GraphQLClient` constructor.

```ts
const client = new GraphQLClient(endpoint, {errorPolicy: "all"});
```

#### None (default)
Allow no errors at all. If you receive a GraphQL error the client will throw.

#### Ignore
Ignore incoming errors and resolve like no errors occurred

#### All
Return both the errors and data.

## FAQ

#### Why do I have to install `graphql`?
Expand Down
20 changes: 14 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
Variables,
PatchedRequestInit,
MaybeFunction,
GraphQLError,
} from './types'
import * as Dom from './types.dom'

Expand Down Expand Up @@ -206,15 +207,15 @@ export class GraphQLClient {
query: string,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }>
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; errors?: GraphQLError[]; status: number }>
async rawRequest<T = any, V = Variables>(
options: RawRequestOptions<V>
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }>
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; errors?: GraphQLError[]; 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 }> {
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; errors?: GraphQLError[]; status: number }> {
const rawRequestOptions = parseRawRequestArgs<V>(queryOrOptions, variables, requestHeaders)

let { headers, fetch = crossFetch, method = 'POST', ...fetchOptions } = this.options
Expand Down Expand Up @@ -372,7 +373,7 @@ async function makeRequest<T = any, V = Variables>({
fetch: any
method: string
fetchOptions: Dom.RequestInit
}): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }> {
}): Promise<{ data: T; extensions?: any; headers: Dom.Headers; errors?: GraphQLError[]; status: number }> {
const fetcher = method.toUpperCase() === 'POST' ? post : get
const isBathchingQuery = Array.isArray(query)

Expand All @@ -390,10 +391,17 @@ async function makeRequest<T = any, V = Variables>({
const successfullyReceivedData =
isBathchingQuery && Array.isArray(result) ? !result.some(({ data }) => !data) : !!result.data

if (response.ok && !result.errors && successfullyReceivedData) {
const successfullyPassedErrorPolicy =
!result.errors || fetchOptions.errorPolicy === 'all' || fetchOptions.errorPolicy === 'ignore'

if (response.ok && successfullyPassedErrorPolicy && successfullyReceivedData) {
const { headers, status } = response

const { errors, ...rest } = result
const data = fetchOptions.errorPolicy === 'ignore' ? rest : result

return {
...(isBathchingQuery ? { data: result } : result),
...(isBathchingQuery ? { data } : data),
headers,
status,
}
Expand Down
17 changes: 15 additions & 2 deletions src/types.dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ type RequestCredentials = 'omit' | 'same-origin' | 'include'

type HeadersInit = Headers | string[][] | Record<string, string>

/**
* 'None' will throw whenever the response contains errors
*
* 'Ignore' will ignore incoming errors and resolve like no errors occurred
*
* 'All' will return both the errors and data
*/
export type ErrorPolicy = 'none' | 'ignore' | 'all'

type RequestMode = 'navigate' | 'same-origin' | 'no-cors' | 'cors'

type RequestRedirect = 'follow' | 'error' | 'manual'
Expand Down Expand Up @@ -279,8 +288,8 @@ interface AbortSignal extends EventTarget {
}

export interface JsonSerializer {
stringify(obj: any): string;
parse(obj: string): unknown;
stringify(obj: any): string
parse(obj: string): unknown
}

export interface RequestInit {
Expand All @@ -300,6 +309,10 @@ export interface RequestInit {
window?: any
fetch?: any
jsonSerializer?: JsonSerializer
/**
* Decide how to handle GraphQLErrors in response
*/
errorPolicy?: ErrorPolicy
}

interface Body {
Expand Down
62 changes: 62 additions & 0 deletions tests/errorPolicy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { GraphQLClient } from '../src'
import { setupTestServer } from './__helpers'

const ctx = setupTestServer()
const errors = {
message: 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n',
locations: [
{
line: 1,
column: 1,
},
],
}

test('should throw error when error policy not set', async () => {
ctx.res({
body: {
data: {},
errors,
},
})

expect(async () => await new GraphQLClient(ctx.url).rawRequest(`x`)).rejects.toThrow('GraphQL Error')
})

test('should throw error when error policy set to "none"', async () => {
ctx.res({
body: {
data: {},
errors,
},
})

expect(async () => await new GraphQLClient(ctx.url).rawRequest(`x`)).rejects.toThrow('GraphQL Error')
})

test('should not throw error when error policy set to "ignore" and return only data', async () => {
ctx.res({
body: {
data: { test: {} },
errors,
},
})

const res = await new GraphQLClient(ctx.url, { errorPolicy: 'ignore' }).rawRequest(`x`)

expect(res).toEqual(expect.objectContaining({ data: { test: {} } }))
expect(res).toEqual(expect.not.objectContaining({ errors }))
})

test('should not throw error when error policy set to "all" and return both data and error', async () => {
ctx.res({
body: {
data: { test: {} },
errors,
},
})

const res = await new GraphQLClient(ctx.url, { errorPolicy: 'all' }).rawRequest(`x`)

expect(res).toEqual(expect.objectContaining({ data: { test: {} }, errors }))
})

0 comments on commit 10a5896

Please sign in to comment.