From 952e972717229af559f0ba3a369a1cb91ff840a2 Mon Sep 17 00:00:00 2001 From: Moe Haydar Date: Mon, 16 Nov 2020 14:05:15 +0100 Subject: [PATCH] feat: Support per-request headers (#33) (#228) closes #33 There is a new third parameter on the `request` an `rawRequest` methods for passing headers particular to that request. For example: ```js import { GraphQLClient } from 'graphql-request' const client = new GraphQLClient(endpoint) const query = gql` query getMovie($title: String!) { Movie(title: $title) { releaseDate actors { name } } } ` const variables = { title: 'Inception', } const requestHeaders = { authorization: 'Bearer MY_TOKEN' } // Overrides the clients headers with the passed values const data = await client.request(query, variables, requestHeaders) ``` --- README.md | 34 +++++++++++ examples/passing-custom-header-per-request.ts | 33 +++++++++++ src/index.ts | 15 +++-- tests/headers.test.ts | 59 ++++++++++++++++++- 4 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 examples/passing-custom-header-per-request.ts diff --git a/README.md b/README.md index e0d7ca925..87dc346fd 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Minimal GraphQL client supporting Node and browsers for scripts or simple apps - [Examples](#examples) - [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 more options to `fetch`](#passing-more-options-to-fetch) - [Using GraphQL Document variables](#using-graphql-document-variables) - [GraphQL Mutations](#graphql-mutations) @@ -153,6 +154,39 @@ client.setHeaders({ }) ``` +#### passing-headers-in-each-request + +It is possible to pass custom headers for each request. `request()` and `rawRequest()` accept a header object as the third parameter + + +```js +import { GraphQLClient } from 'graphql-request' + +const client = new GraphQLClient(endpoint) + +const query = gql` + query getMovie($title: String!) { + Movie(title: $title) { + releaseDate + actors { + name + } + } + } +` + +const variables = { + title: 'Inception', +} + +const requestHeaders = { + authorization: 'Bearer MY_TOKEN' +} + +// Overrides the clients headers with the passed values +const data = await client.request(query, variables, requestHeaders) +``` + ### Passing more options to `fetch` ```js diff --git a/examples/passing-custom-header-per-request.ts b/examples/passing-custom-header-per-request.ts new file mode 100644 index 000000000..d4c3433f6 --- /dev/null +++ b/examples/passing-custom-header-per-request.ts @@ -0,0 +1,33 @@ +import { GraphQLClient } from '../src' +;(async function () { + const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr' + + const client = new GraphQLClient(endpoint, { + headers: { + authorization: 'Bearer MY_TOKEN', + }, + }) + + const query = /* GraphQL */ ` + { + Movie(title: "Inception") { + releaseDate + actors { + name + } + } + } + ` + + const requestHeaders = { + authorization: 'Bearer MY_TOKEN_2', + 'x-custom': 'foo' + } + + interface TData { + Movie: { releaseDate: string; actors: Array<{ name: string }> } + } + + const data = await client.request(query, {}, requestHeaders) + console.log(JSON.stringify(data, undefined, 2)) +})().catch((error) => console.error(error)) diff --git a/src/index.ts b/src/index.ts index a2e6abdea..acdcca632 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,17 +43,19 @@ export class GraphQLClient { async rawRequest( query: string, - variables?: V + variables?: V, + requestHeaders?: Dom.RequestInit['headers'] ): Promise<{ data?: T; extensions?: any; headers: Dom.Headers; status: number; errors?: GraphQLError[] }> { let { headers, fetch: localFetch = crossFetch, ...others } = this.options const body = createRequestBody(query, variables) - headers = resolveHeaders(headers) + const response = await localFetch(this.url, { method: 'POST', headers: { ...(typeof body === 'string' ? { 'Content-Type': 'application/json' } : {}), - ...headers, + ...resolveHeaders(headers), + ...resolveHeaders(requestHeaders) }, body, ...others, @@ -76,9 +78,9 @@ export class GraphQLClient { /** * Send a GraphQL document to the server. */ - async request(document: RequestDocument, variables?: V): Promise { + async request(document: RequestDocument, variables?: V, + requestHeaders?: Dom.RequestInit['headers']): Promise { let { headers, fetch: localFetch = crossFetch, ...others } = this.options - headers = resolveHeaders(headers) const resolvedDoc = resolveRequestDocument(document) const body = createRequestBody(resolvedDoc, variables) @@ -86,7 +88,8 @@ export class GraphQLClient { method: 'POST', headers: { ...(typeof body === 'string' ? { 'Content-Type': 'application/json' } : {}), - ...headers, + ...resolveHeaders(headers), + ...resolveHeaders(requestHeaders) }, body, ...others, diff --git a/tests/headers.test.ts b/tests/headers.test.ts index f1e8ba70d..c7b61857c 100644 --- a/tests/headers.test.ts +++ b/tests/headers.test.ts @@ -1,8 +1,11 @@ import * as CrossFetch from 'cross-fetch' +import * as Dom from '../src/types.dom' import { GraphQLClient } from '../src' import { setupTestServer } from './__helpers' const ctx = setupTestServer() +// Headers not defined globally in Node +const H = typeof Headers === 'undefined' ? CrossFetch.Headers : Headers describe('using class', () => { test('.setHeader() sets a header that get sent to server', async () => { @@ -16,8 +19,6 @@ describe('using class', () => { describe('.setHeaders() sets headers that get sent to the server', () => { test('with headers instance', async () => { const client = new GraphQLClient(ctx.url) - // Headers not defined globally in Node - const H = typeof Headers === 'undefined' ? CrossFetch.Headers : Headers client.setHeaders(new H({ 'x-foo': 'bar' })) const mock = ctx.res() await client.request(`{ me { id } }`) @@ -38,4 +39,58 @@ describe('using class', () => { expect(mock.requests[0].headers['x-foo']).toEqual('bar') }) }) + + describe('custom header in the request', () => { + describe.each([ + [new H({ 'x-request-foo': 'request-bar' })], + [{ 'x-request-foo': 'request-bar' }], + [[['x-request-foo', 'request-bar']]] + ])('request unique header with request', (headerCase: Dom.RequestInit['headers']) => { + + test('with request method', async () => { + const client = new GraphQLClient(ctx.url) + + client.setHeaders(new H({ 'x-foo': 'bar' })) + const mock = ctx.res() + await client.request(`{ me { id } }`, {}, headerCase) + + expect(mock.requests[0].headers['x-foo']).toEqual('bar') + expect(mock.requests[0].headers['x-request-foo']).toEqual('request-bar') + }) + + test('with rawRequest method', async () => { + const client = new GraphQLClient(ctx.url) + + client.setHeaders(new H({ 'x-foo': 'bar' })) + const mock = ctx.res() + await client.rawRequest(`{ me { id } }`, {}, headerCase) + + expect(mock.requests[0].headers['x-foo']).toEqual('bar') + expect(mock.requests[0].headers['x-request-foo']).toEqual('request-bar') + }) + }) + + describe.each([ + [new H({ 'x-foo': 'request-bar' })], + [{ 'x-foo': 'request-bar' }], + [[['x-foo', 'request-bar']]] + ])('request header overriding the client header', (headerCase: Dom.RequestInit['headers']) => { + test('with request method', async () => { + const client = new GraphQLClient(ctx.url) + client.setHeader('x-foo', 'bar') + const mock = ctx.res() + await client.request(`{ me { id } }`, {}, headerCase); + expect(mock.requests[0].headers['x-foo']).toEqual('request-bar') + }); + + test('with rawRequest method', async () => { + const client = new GraphQLClient(ctx.url) + client.setHeader('x-foo', 'bar') + const mock = ctx.res() + await client.rawRequest(`{ me { id } }`, {}, headerCase); + expect(mock.requests[0].headers['x-foo']).toEqual('request-bar') + }); + + }) + }) })