Skip to content

Commit

Permalink
feat: added middleware support
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander.sokolov committed Jun 23, 2022
1 parent dec0296 commit 76abc28
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 36 deletions.
91 changes: 57 additions & 34 deletions src/index.ts
Expand Up @@ -26,7 +26,7 @@ import {
Variables,
PatchedRequestInit,
MaybeFunction,
GraphQLError,
Response,
} from './types'
import * as Dom from './types.dom'

Expand Down Expand Up @@ -132,6 +132,7 @@ const post = async <V = Variables>({
headers,
fetch,
fetchOptions,
middleware,
}: {
url: string
query: string | string[]
Expand All @@ -140,18 +141,23 @@ const post = async <V = Variables>({
variables?: V
headers?: Dom.RequestInit['headers']
operationName?: string
middleware?: (request: Dom.RequestInit) => Dom.RequestInit
}) => {
const body = createRequestBody(query, variables, operationName, fetchOptions.jsonSerializer)

return await fetch(url, {
let options: Dom.RequestInit = {
method: 'POST',
headers: {
...(typeof body === 'string' ? { 'Content-Type': 'application/json' } : {}),
...headers,
},
body,
...fetchOptions,
})
};
if (middleware) {
options = middleware(options)
}
return await fetch(url, options)
}

/**
Expand All @@ -165,6 +171,7 @@ const get = async <V = Variables>({
headers,
fetch,
fetchOptions,
middleware,
}: {
url: string
query: string | string[]
Expand All @@ -173,6 +180,7 @@ const get = async <V = Variables>({
variables?: V
headers?: HeadersInit
operationName?: string
middleware?: (request: Dom.RequestInit) => Dom.RequestInit
}) => {
const queryParams = buildGetQueryParams<V>({
query,
Expand All @@ -181,24 +189,22 @@ const get = async <V = Variables>({
jsonSerializer: fetchOptions.jsonSerializer
} as TBuildGetQueryParams<V>)

return await fetch(`${url}?${queryParams}`, {
let options: Dom.RequestInit = {
method: 'GET',
headers,
...fetchOptions,
})
};
if (middleware) {
options = middleware(options)
}
return await fetch(`${url}?${queryParams}`, options)
}

/**
* GraphQL Client.
*/
export class GraphQLClient {
private url: string
private options: PatchedRequestInit

constructor(url: string, options?: PatchedRequestInit) {
this.url = url
this.options = options || {}
}
constructor(private url: string, private readonly options: PatchedRequestInit = {}) {}

/**
* Send a GraphQL query to the server.
Expand All @@ -207,18 +213,18 @@ export class GraphQLClient {
query: string,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; errors?: GraphQLError[]; status: number }>
): Promise<Response<T>>
async rawRequest<T = any, V = Variables>(
options: RawRequestOptions<V>
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; errors?: GraphQLError[]; status: number }>
): Promise<Response<T>>
async rawRequest<T = any, V = Variables>(
queryOrOptions: string | RawRequestOptions<V>,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; errors?: GraphQLError[]; status: number }> {
): Promise<Response<T>> {
const rawRequestOptions = parseRawRequestArgs<V>(queryOrOptions, variables, requestHeaders)

let { headers, fetch = crossFetch, method = 'POST', ...fetchOptions } = this.options
let { headers, fetch = crossFetch, method = 'POST', requestMiddleware, responseMiddleware , ...fetchOptions } = this.options
let { url } = this
if (rawRequestOptions.signal !== undefined) {
fetchOptions.signal = rawRequestOptions.signal
Expand All @@ -238,34 +244,40 @@ export class GraphQLClient {
fetch,
method,
fetchOptions,
middleware: requestMiddleware,
}).then(response => {
if (responseMiddleware) {
responseMiddleware(response)
}
return response
})
}

/**
* Send a GraphQL document to the server.
*/
async request<T = any, V = Variables>(
request<T = any, V = Variables>(
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>(
request<T = any, V = Variables>(options: RequestOptions<V>): Promise<T>
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 { headers, fetch = crossFetch, method = 'POST', requestMiddleware, responseMiddleware, ...fetchOptions } = this.options
let { url } = this
if (requestOptions.signal !== undefined) {
fetchOptions.signal = requestOptions.signal
}

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

const { data } = await makeRequest<T, V>({
return makeRequest<T, V>({
url,
query,
variables: requestOptions.variables,
Expand All @@ -277,26 +289,30 @@ export class GraphQLClient {
fetch,
method,
fetchOptions,
middleware: requestMiddleware,
}).then(response => {
if (responseMiddleware) {
responseMiddleware(response)
}
return response.data
})

return data
}

/**
* Send GraphQL documents in batch to the server.
*/
async batchRequests<T extends any = any, V = Variables>(
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>(
batchRequests<T = any, V = Variables>(options: BatchRequestsOptions<V>): Promise<T>
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 { headers, fetch = crossFetch, method = 'POST', requestMiddleware, responseMiddleware, ...fetchOptions } = this.options
let { url } = this
if (batchRequestOptions.signal !== undefined) {
fetchOptions.signal = batchRequestOptions.signal
Expand All @@ -307,7 +323,7 @@ export class GraphQLClient {
)
const variables = batchRequestOptions.documents.map(({ variables }) => variables)

const { data } = await makeRequest<T, (V | undefined)[]>({
return makeRequest<T, (V | undefined)[]>({
url,
query: queries,
variables,
Expand All @@ -319,9 +335,13 @@ export class GraphQLClient {
fetch,
method,
fetchOptions,
middleware: requestMiddleware,
}).then(response => {
if (responseMiddleware) {
responseMiddleware(response)
}
return response.data
})

return data
}

setHeaders(headers: Dom.RequestInit['headers']): GraphQLClient {
Expand Down Expand Up @@ -364,6 +384,7 @@ async function makeRequest<T = any, V = Variables>({
fetch,
method = 'POST',
fetchOptions,
middleware,
}: {
url: string
query: string | string[]
Expand All @@ -373,7 +394,8 @@ async function makeRequest<T = any, V = Variables>({
fetch: any
method: string
fetchOptions: Dom.RequestInit
}): Promise<{ data: T; extensions?: any; headers: Dom.Headers; errors?: GraphQLError[]; status: number }> {
middleware?: (request: Dom.RequestInit) => Dom.RequestInit
}): Promise<Response<T>> {
const fetcher = method.toUpperCase() === 'POST' ? post : get
const isBathchingQuery = Array.isArray(query)

Expand All @@ -385,6 +407,7 @@ async function makeRequest<T = any, V = Variables>({
headers,
fetch,
fetchOptions,
middleware,
})
const result = await getResult(response, fetchOptions.jsonSerializer)

Expand Down Expand Up @@ -422,16 +445,16 @@ export async function rawRequest<T = any, V = Variables>(
query: string,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }>
): Promise<Response<T>>
export async function rawRequest<T = any, V = Variables>(
options: RawRequestExtendedOptions<V>
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }>
): Promise<Response<T>>
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 }> {
): Promise<Response<T>> {
const requestOptions = parseRawRequestExtendedArgs<V>(urlOrOptions, query, variables, requestHeaders)
const client = new GraphQLClient(requestOptions.url)
return client.rawRequest<T, V>({
Expand Down
15 changes: 13 additions & 2 deletions src/types.ts
Expand Up @@ -59,8 +59,19 @@ export type MaybeFunction<T> = T | (() => T);

export type RequestDocument = string | DocumentNode

export type PatchedRequestInit = Omit<Dom.RequestInit, "headers">
& {headers?: MaybeFunction<Dom.RequestInit['headers']>};
export interface Response<T> {
data: T
extensions?: any
headers: Dom.Headers
errors?: GraphQLError[]
status: number
}

export type PatchedRequestInit = Omit<Dom.RequestInit, "headers"> & {
headers?: MaybeFunction<Dom.RequestInit['headers']>
requestMiddleware?: (request: Dom.RequestInit) => Dom.RequestInit
responseMiddleware?: (response: Response<unknown>) => void
};

export type BatchRequestDocument<V = Variables> = {
document: RequestDocument
Expand Down
42 changes: 42 additions & 0 deletions tests/general.test.ts
Expand Up @@ -117,6 +117,48 @@ test('basic error with raw request', async () => {
)
})

describe('middleware', () => {
let client: GraphQLClient
let requestMiddleware: jest.Mock
let responseMiddleware: jest.Mock

beforeEach(() => {
ctx.res({
body: {
data: {
result: 123,
},
},
})

requestMiddleware = jest.fn(req => ({ ...req }))
responseMiddleware = jest.fn()
client = new GraphQLClient(ctx.url, { requestMiddleware, responseMiddleware })
})

it('request', async () => {
const requestPromise = client.request<{ result: number }>(`x`)
expect(requestMiddleware).toBeCalledTimes(1)
const res = await requestPromise
expect(responseMiddleware).toBeCalledTimes(1)
expect(res.result).toBe(123)
})

it('rawRequest', async () => {
const requestPromise = client.rawRequest<{ result: number }>(`x`)
expect(requestMiddleware).toBeCalledTimes(1)
await requestPromise
expect(responseMiddleware).toBeCalledTimes(1)
})

it('batchRequests', async () => {
const requestPromise = client.batchRequests<{ result: number }>([{ document: `x` }])
expect(requestMiddleware).toBeCalledTimes(1)
await requestPromise
expect(responseMiddleware).toBeCalledTimes(1)
})
})

// todo needs to be tested in browser environment
// the options under test here aren't used by node-fetch
test.skip('extra fetch options', async () => {
Expand Down

0 comments on commit 76abc28

Please sign in to comment.