From 76abc28f825fabd34858661515f9c980f48fb10c Mon Sep 17 00:00:00 2001 From: "alexander.sokolov" Date: Fri, 12 Jun 2020 02:48:23 +0300 Subject: [PATCH] feat: added middleware support --- src/index.ts | 91 +++++++++++++++++++++++++++---------------- src/types.ts | 15 ++++++- tests/general.test.ts | 42 ++++++++++++++++++++ 3 files changed, 112 insertions(+), 36 deletions(-) diff --git a/src/index.ts b/src/index.ts index 685ca240..7129c3e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,7 @@ import { Variables, PatchedRequestInit, MaybeFunction, - GraphQLError, + Response, } from './types' import * as Dom from './types.dom' @@ -132,6 +132,7 @@ const post = async ({ headers, fetch, fetchOptions, + middleware, }: { url: string query: string | string[] @@ -140,10 +141,11 @@ const post = async ({ 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' } : {}), @@ -151,7 +153,11 @@ const post = async ({ }, body, ...fetchOptions, - }) + }; + if (middleware) { + options = middleware(options) + } + return await fetch(url, options) } /** @@ -165,6 +171,7 @@ const get = async ({ headers, fetch, fetchOptions, + middleware, }: { url: string query: string | string[] @@ -173,6 +180,7 @@ const get = async ({ variables?: V headers?: HeadersInit operationName?: string + middleware?: (request: Dom.RequestInit) => Dom.RequestInit }) => { const queryParams = buildGetQueryParams({ query, @@ -181,24 +189,22 @@ const get = async ({ jsonSerializer: fetchOptions.jsonSerializer } as TBuildGetQueryParams) - 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. @@ -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> async rawRequest( options: RawRequestOptions - ): Promise<{ data: T; extensions?: any; headers: Dom.Headers; errors?: GraphQLError[]; status: number }> + ): Promise> async rawRequest( queryOrOptions: string | RawRequestOptions, variables?: V, requestHeaders?: Dom.RequestInit['headers'] - ): Promise<{ data: T; extensions?: any; headers: Dom.Headers; errors?: GraphQLError[]; status: number }> { + ): Promise> { const rawRequestOptions = parseRawRequestArgs(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 @@ -238,26 +244,32 @@ 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( + request( document: RequestDocument, variables?: V, requestHeaders?: Dom.RequestInit['headers'] ): Promise - async request(options: RequestOptions): Promise - async request( + request(options: RequestOptions): Promise + request( documentOrOptions: RequestDocument | RequestOptions, variables?: V, requestHeaders?: Dom.RequestInit['headers'] ): Promise { const requestOptions = parseRequestArgs(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 @@ -265,7 +277,7 @@ export class GraphQLClient { const { query, operationName } = resolveRequestDocument(requestOptions.document) - const { data } = await makeRequest({ + return makeRequest({ url, query, variables: requestOptions.variables, @@ -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( + batchRequests( documents: BatchRequestDocument[], requestHeaders?: Dom.RequestInit['headers'] ): Promise - async batchRequests(options: BatchRequestsOptions): Promise - async batchRequests( + batchRequests(options: BatchRequestsOptions): Promise + batchRequests( documentsOrOptions: BatchRequestDocument[] | BatchRequestsOptions, requestHeaders?: Dom.RequestInit['headers'] ): Promise { const batchRequestOptions = parseBatchRequestArgs(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 @@ -307,7 +323,7 @@ export class GraphQLClient { ) const variables = batchRequestOptions.documents.map(({ variables }) => variables) - const { data } = await makeRequest({ + return makeRequest({ url, query: queries, variables, @@ -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 { @@ -364,6 +384,7 @@ async function makeRequest({ fetch, method = 'POST', fetchOptions, + middleware, }: { url: string query: string | string[] @@ -373,7 +394,8 @@ async function makeRequest({ 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> { const fetcher = method.toUpperCase() === 'POST' ? post : get const isBathchingQuery = Array.isArray(query) @@ -385,6 +407,7 @@ async function makeRequest({ headers, fetch, fetchOptions, + middleware, }) const result = await getResult(response, fetchOptions.jsonSerializer) @@ -422,16 +445,16 @@ export async function rawRequest( query: string, variables?: V, requestHeaders?: Dom.RequestInit['headers'] -): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }> +): Promise> export async function rawRequest( options: RawRequestExtendedOptions -): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }> +): Promise> export async function rawRequest( urlOrOptions: string | RawRequestExtendedOptions, query?: string, variables?: V, requestHeaders?: Dom.RequestInit['headers'] -): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }> { +): Promise> { const requestOptions = parseRawRequestExtendedArgs(urlOrOptions, query, variables, requestHeaders) const client = new GraphQLClient(requestOptions.url) return client.rawRequest({ diff --git a/src/types.ts b/src/types.ts index 5249d446..21d669ad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -59,8 +59,19 @@ export type MaybeFunction = T | (() => T); export type RequestDocument = string | DocumentNode -export type PatchedRequestInit = Omit - & {headers?: MaybeFunction}; +export interface Response { + data: T + extensions?: any + headers: Dom.Headers + errors?: GraphQLError[] + status: number +} + +export type PatchedRequestInit = Omit & { + headers?: MaybeFunction + requestMiddleware?: (request: Dom.RequestInit) => Dom.RequestInit + responseMiddleware?: (response: Response) => void +}; export type BatchRequestDocument = { document: RequestDocument diff --git a/tests/general.test.ts b/tests/general.test.ts index 4119c901..097d7e90 100644 --- a/tests/general.test.ts +++ b/tests/general.test.ts @@ -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 () => {