From 016a15c9139faf9c6aabe79d05278f25fc14800e Mon Sep 17 00:00:00 2001 From: Artur Tagisow <5359825+sethidden@users.noreply.github.com> Date: Sat, 23 Apr 2022 23:53:19 +0200 Subject: [PATCH] feat: dynamic headers (#341) --- README.md | 26 ++++++++++++++++++++++++++ src/index.ts | 16 +++++++++++----- src/types.ts | 5 +++++ tests/headers.test.ts | 20 ++++++++++++++++++++ 4 files changed, 62 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 53651e6c..eaf8f3b3 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Minimal GraphQL client supporting Node and browsers for scripts or simple apps - [Authentication via HTTP header](#authentication-via-http-header) - [Incrementally setting headers](#incrementally-setting-headers) - [Passing Headers in each request](#passing-headers-in-each-request) + - [Passing dynamic headers to the client](#passing-dynamic-headers-to-the-client) - [Passing more options to `fetch`](#passing-more-options-to-fetch) - [Custom JSON serializer](#custom-json-serializer) - [Using GraphQL Document variables](#using-graphql-document-variables) @@ -214,6 +215,31 @@ const requestHeaders = { const data = await client.request(query, variables, requestHeaders) ``` +#### Passing dynamic headers to the client + +It's possible to recalculate the global client headers dynamically before each request. +To do that, pass a function that returns the headers to the `headers` property when creating a new `GraphQLClient`. + +```js +import { GraphQLClient } from 'graphql-request' + +const client = new GraphQLClient(endpoint, + { + headers: () => ({ 'X-Sent-At-Time': Date.now() }) + } +) + +const query = gql` + query getCars { + cars { + name + } + } + +// Function saved in the client runs and calculates fresh headers before each request +const data = await client.request(query) +``` + ### Passing more options to `fetch` ```js diff --git a/src/index.ts b/src/index.ts index bd327b83..72d2572b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,8 @@ import { RawRequestExtendedOptions, RequestExtendedOptions, Variables, + PatchedRequestInit, + MaybeFunction, } from './types' import * as Dom from './types.dom' @@ -190,9 +192,9 @@ const get = async ({ */ export class GraphQLClient { private url: string - private options: Dom.RequestInit + private options: PatchedRequestInit - constructor(url: string, options?: Dom.RequestInit) { + constructor(url: string, options?: PatchedRequestInit) { this.url = url this.options = options || {} } @@ -228,7 +230,7 @@ export class GraphQLClient { query: rawRequestOptions.query, variables: rawRequestOptions.variables, headers: { - ...resolveHeaders(headers), + ...resolveHeaders(callOrIdentity(headers)), ...resolveHeaders(rawRequestOptions.requestHeaders), }, operationName, @@ -267,7 +269,7 @@ export class GraphQLClient { query, variables: requestOptions.variables, headers: { - ...resolveHeaders(headers), + ...resolveHeaders(callOrIdentity(headers)), ...resolveHeaders(requestOptions.requestHeaders), }, operationName, @@ -309,7 +311,7 @@ export class GraphQLClient { query: queries, variables, headers: { - ...resolveHeaders(headers), + ...resolveHeaders(callOrIdentity(headers)), ...resolveHeaders(batchRequestOptions.requestHeaders), }, operationName: undefined, @@ -592,6 +594,10 @@ function resolveRequestDocument(document: RequestDocument): { query: string; ope return { query: print(document), operationName } } +function callOrIdentity(value: MaybeFunction) { + return typeof value === 'function' ? (value as () => T)() : value; +} + /** * Convenience passthrough template tag to get the benefits of tooling for the gql template tag. This does not actually parse the input into a GraphQL DocumentNode like graphql-tag package does. It just returns the string with any variables given interpolated. Can save you a bit of performance and having to install another package. * diff --git a/src/types.ts b/src/types.ts index 0a77e67e..5249d446 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,8 +55,13 @@ export class ClientError extends Error { } } +export type MaybeFunction = T | (() => T); + export type RequestDocument = string | DocumentNode +export type PatchedRequestInit = Omit + & {headers?: MaybeFunction}; + export type BatchRequestDocument = { document: RequestDocument variables?: V diff --git a/tests/headers.test.ts b/tests/headers.test.ts index 537fb59d..0837b061 100644 --- a/tests/headers.test.ts +++ b/tests/headers.test.ts @@ -92,6 +92,26 @@ describe('using class', () => { }); }) + + describe('gets fresh dynamic headers before each request', () => { + test('with request method', async () => { + const objectChangedThroughReference = { 'x-foo': 'old' } + const client = new GraphQLClient(ctx.url, { headers: () => objectChangedThroughReference }); + objectChangedThroughReference['x-foo'] = 'new'; + const mock = ctx.res() + await client.request(`{ me { id } }`); + expect(mock.requests[0].headers['x-foo']).toEqual('new') + }) + + test('with rawRequest method', async () => { + const objectChangedThroughReference = { 'x-foo': 'old' } + const client = new GraphQLClient(ctx.url, { headers: () => objectChangedThroughReference }); + objectChangedThroughReference['x-foo'] = 'new'; + const mock = ctx.res() + await client.rawRequest(`{ me { id } }`); + expect(mock.requests[0].headers['x-foo']).toEqual('new') + }) + }) }) })