Skip to content

Commit

Permalink
feat: simplify typings by using full RequestInit type (#107)
Browse files Browse the repository at this point in the history
Co-authored-by: Jason Kuhrt <jason.kuhrt@dialogue.co>
  • Loading branch information
brikou and Jason Kuhrt committed May 28, 2020
1 parent b0abe80 commit 9d5e344
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 113 deletions.
6 changes: 6 additions & 0 deletions .prettierrc
@@ -0,0 +1,6 @@
{
"arrowParens": "always",
"semi": false,
"singleQuote": true,
"trailingComma": "all"
}
7 changes: 4 additions & 3 deletions package.json
Expand Up @@ -46,8 +46,9 @@
"bundlesize": "0.17.0",
"fetch-cookie": "0.7.2",
"fetch-mock": "5.13.1",
"tslint": "5.9.1",
"tslint-config-standard": "7.0.0",
"typescript": "2.7.2"
"prettier": "1.14.2",
"tslint": "5.11.0",
"tslint-config-standard": "8.0.1",
"typescript": "3.0.3"
}
}
43 changes: 27 additions & 16 deletions src/index.ts
@@ -1,20 +1,28 @@
import { ClientError, GraphQLError, Headers as HttpHeaders, Options, Variables } from './types'
export { ClientError } from './types'
import 'cross-fetch/polyfill'

import { ClientError, GraphQLError, Variables } from './types'

export { ClientError } from './types'

export class GraphQLClient {
private url: string
private options: Options
private options: RequestInit

constructor(url: string, options?: Options) {
constructor(url: string, options?: RequestInit) {
this.url = url
this.options = options || {}
}

async rawRequest<T extends any>(
async rawRequest<T = any>(
query: string,
variables?: Variables,
): Promise<{ data?: T, extensions?: any, headers: Headers, status: number, errors?: GraphQLError[] }> {
): Promise<{
data?: T
extensions?: any
headers: Request['headers']
status: number
errors?: GraphQLError[]
}> {
const { headers, ...others } = this.options

const body = JSON.stringify({
Expand All @@ -24,7 +32,7 @@ export class GraphQLClient {

const response = await fetch(this.url, {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, headers),
headers: { 'Content-Type': 'application/json', ...headers },
body,
...others,
})
Expand All @@ -44,10 +52,7 @@ export class GraphQLClient {
}
}

async request<T extends any>(
query: string,
variables?: Variables,
): Promise<T> {
async request<T = any>(query: string, variables?: Variables): Promise<T> {
const { headers, ...others } = this.options

const body = JSON.stringify({
Expand All @@ -57,7 +62,7 @@ export class GraphQLClient {

const response = await fetch(this.url, {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, headers),
headers: { 'Content-Type': 'application/json', ...headers },
body,
...others,
})
Expand All @@ -76,7 +81,7 @@ export class GraphQLClient {
}
}

setHeaders(headers: HttpHeaders): GraphQLClient {
setHeaders(headers: Response['headers']): GraphQLClient {
this.options.headers = headers

return this
Expand All @@ -94,17 +99,23 @@ export class GraphQLClient {
}
}

export async function rawRequest<T extends any>(
export async function rawRequest<T = any>(
url: string,
query: string,
variables?: Variables,
): Promise<{ data?: T, extensions?: any, headers: Headers, status: number, errors?: GraphQLError[] }> {
): Promise<{
data?: T
extensions?: any
headers: Request['headers']
status: number
errors?: GraphQLError[]
}> {
const client = new GraphQLClient(url)

return client.rawRequest<T>(query, variables)
}

export async function request<T extends any>(
export async function request<T = any>(
url: string,
query: string,
variables?: Variables,
Expand Down
28 changes: 7 additions & 21 deletions src/types.ts
@@ -1,24 +1,8 @@
export type Variables = { [key: string]: any }

export interface Headers {
[key: string]: string
}

export interface Options {
method?: RequestInit['method']
headers?: Headers
mode?: RequestInit['mode']
credentials?: RequestInit['credentials']
cache?: RequestInit['cache']
redirect?: RequestInit['redirect']
referrer?: RequestInit['referrer']
referrerPolicy?: RequestInit['referrerPolicy']
integrity?: RequestInit['integrity']
}

export interface GraphQLError {
message: string
locations: { line: number, column: number }[]
locations: { line: number; column: number }[]
path: string[]
}

Expand All @@ -36,12 +20,14 @@ export interface GraphQLRequestContext {
}

export class ClientError extends Error {

response: GraphQLResponse
request: GraphQLRequestContext

constructor (response: GraphQLResponse, request: GraphQLRequestContext) {
const message = `${ClientError.extractMessage(response)}: ${JSON.stringify({ response, request })}`
constructor(response: GraphQLResponse, request: GraphQLRequestContext) {
const message = `${ClientError.extractMessage(response)}: ${JSON.stringify({
response,
request,
})}`

super(message)

Expand All @@ -55,7 +41,7 @@ export class ClientError extends Error {
}
}

private static extractMessage (response: GraphQLResponse): string {
private static extractMessage(response: GraphQLResponse): string {
try {
return response.errors![0].message
} catch (e) {
Expand Down
100 changes: 62 additions & 38 deletions tests/index.test.ts
@@ -1,7 +1,7 @@
import test from 'ava'
import * as fetchMock from 'fetch-mock'

import { ClientError, rawRequest, request, GraphQLClient } from '../src/index'
import { Options } from '../src/types'

test('minimal query', async (t) => {
const data = {
Expand All @@ -10,8 +10,11 @@ test('minimal query', async (t) => {
},
}

await mock({body: {data}}, async () => {
t.deepEqual(await request('https://mock-api.com/graphql', `{ viewer { id } }`), data)
await mock({ body: { data } }, async () => {
t.deepEqual(
await request('https://mock-api.com/graphql', `{ viewer { id } }`),
data,
)
})
})

Expand All @@ -26,9 +29,12 @@ test('minimal raw query', async (t) => {
version: '1',
}

await mock({body: {data, extensions}}, async () => {
const { headers, ...result } = await rawRequest('https://mock-api.com/graphql', `{ viewer { id } }`)
t.deepEqual(result, {data, extensions, status: 200})
await mock({ body: { data, extensions } }, async () => {
const { headers, ...result } = await rawRequest(
'https://mock-api.com/graphql',
`{ viewer { id } }`,
)
t.deepEqual(result, { data, extensions, status: 200 })
})
})

Expand All @@ -48,43 +54,53 @@ test('minimal raw query with response headers', async (t) => {
'X-Custom-Header': 'test-custom-header',
}

await mock({headers: reqHeaders, body: {data, extensions}}, async () => {
const { headers, ...result } = await rawRequest('https://mock-api.com/graphql', `{ viewer { id } }`)
t.deepEqual(result, {data, extensions, status: 200})
await mock({ headers: reqHeaders, body: { data, extensions } }, async () => {
const { headers, ...result } = await rawRequest(
'https://mock-api.com/graphql',
`{ viewer { id } }`,
)

t.deepEqual(result, { data, extensions, status: 200 })
t.deepEqual(headers.get('X-Custom-Header'), reqHeaders['X-Custom-Header'])
})
})

test('basic error', async (t) => {
const errors = {
message: "Syntax Error GraphQL request (1:1) Unexpected Name \"x\"\n\n1: x\n ^\n",
message:
'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n',
locations: [
{
line: 1,
column: 1
}
]
column: 1,
},
],
}

await mock({body: {errors}}, async () => {
const err: ClientError = await t.throws(request('https://mock-api.com/graphql', `x`))
await mock({ body: { errors } }, async () => {
const err: ClientError = await t.throws(
request('https://mock-api.com/graphql', `x`),
)
t.deepEqual<any>(err.response.errors, errors)
})
})

test('raw request error', async (t) => {
const errors = {
message: "Syntax Error GraphQL request (1:1) Unexpected Name \"x\"\n\n1: x\n ^\n",
message:
'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n',
locations: [
{
line: 1,
column: 1
}
]
column: 1,
},
],
}

await mock({body: {errors}}, async () => {
const err: ClientError = await t.throws(rawRequest('https://mock-api.com/graphql', `x`))
await mock({ body: { errors } }, async () => {
const err: ClientError = await t.throws(
rawRequest('https://mock-api.com/graphql', `x`),
)
t.deepEqual<any>(err.response.errors, errors)
})
})
Expand All @@ -96,32 +112,40 @@ test('content-type with charset', async (t) => {
},
}

await mock({
headers: {'Content-Type': 'application/json; charset=utf-8'},
body: {data}
}, async () => {
t.deepEqual(await request('https://mock-api.com/graphql', `{ viewer { id } }`), data)
})
await mock(
{
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: { data },
},
async () => {
t.deepEqual(
await request('https://mock-api.com/graphql', `{ viewer { id } }`),
data,
)
},
)
})


test('extra fetch options', async (t) => {
const options: Options = {
const options: RequestInit = {
credentials: 'include',
mode: 'cors',
cache: 'reload',
}

const client = new GraphQLClient('https://mock-api.com/graphql', options)
await mock({
body: { data: {test: 'test'} }
}, async () => {
await client.request('{ test }')
const actualOptions = fetchMock.lastCall()[1]
for (let name in options) {
t.deepEqual(actualOptions[name], options[name])
}
})
await mock(
{
body: { data: { test: 'test' } },
},
async () => {
await client.request('{ test }')
const actualOptions = fetchMock.lastCall()[1]
for (let name in options) {
t.deepEqual(actualOptions[name], options[name])
}
},
)
})

async function mock(response: any, testFn: () => Promise<void>) {
Expand Down
12 changes: 5 additions & 7 deletions tsconfig.json
@@ -1,16 +1,14 @@
{
"compilerOptions": {
"lib": ["es2015", "es2016", "dom", "esnext.asynciterable"],
"module": "commonjs",
"moduleResolution": "node",
"noUnusedLocals": true,
"outDir": "dist",
"rootDir": ".",
"target": "es5",
"sourceMap": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"outDir": "dist",
"lib": ["es2015", "es2016", "dom", "esnext.asynciterable"]
"target": "es5"
},
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}

0 comments on commit 9d5e344

Please sign in to comment.