diff --git a/README.md b/README.md index 20bf4f4c..d516743b 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ const client = new GraphQLClient(config) - `middleware`: Accepts an array of middleware functions, default: none, see more in [middlewares readme](packages/graphql-hooks/src/middlewares/README.md) - `onError({ operation, result })`: Custom error handler - `operation`: Object with `query`, `variables` and `operationName` - - `result`: Object containing `data` and `error` object that contains `fetchError`, `httpError` and `graphqlErrors` + - `result`: Object containing `data`, `headers` and `error` object that contains `fetchError`, `httpError` and `graphqlErrors` ### `client` methods @@ -270,6 +270,7 @@ const { loading, error, data, refetch, cacheHit } = useQuery(QUERY) - `loading`: Boolean - `true` if the query is in flight - `data`: Object - the result of your GraphQL query +- `headers`: Object - response headers - `refetch(options)`: Function - useful when refetching the same query after a mutation; NOTE this presets `skipCache=true` & will bypass the `options.updateData` function that was passed into `useQuery`. You can pass a new `updateData` into `refetch` if necessary. - `options`: Object - options that will be merged into the `options` that were passed into `useQuery` (see above). - `cacheHit`: Boolean - `true` if the query result came from the cache, useful for debugging @@ -405,6 +406,7 @@ The `options` object that can be passed either to `useMutation(mutation, options In addition, there is an option to reset the current state before calling the mutation again, by calling `resetFn(desiredState)` where `desiredState` is optional and if passed, it will override the initial state with: - `data`: Object - the data +- `headers`: Object - response headers - `error`: Error - the error - `loading`: Boolean - true if it is still loading - `cacheHit`: Boolean - true if the result was cached diff --git a/packages/graphql-hooks/src/GraphQLClient.ts b/packages/graphql-hooks/src/GraphQLClient.ts index 2d4d2c61..5ac05fc3 100644 --- a/packages/graphql-hooks/src/GraphQLClient.ts +++ b/packages/graphql-hooks/src/GraphQLClient.ts @@ -158,7 +158,8 @@ class GraphQLClient { fetchError, httpError, graphQLErrors, - data + data, + headers }: GenerateResultOptions): Result< ResponseData, TGraphQLError @@ -169,8 +170,8 @@ class GraphQLClient { httpError ) return !errorFound - ? { data } - : { data, error: { fetchError, httpError, graphQLErrors } } + ? { data, headers } + : { data, error: { fetchError, httpError, graphQLErrors }, headers } } getCacheKey( @@ -339,7 +340,8 @@ class GraphQLClient { status, statusText, body - } + }, + headers: response.headers }) }) } else { @@ -350,7 +352,8 @@ class GraphQLClient { // enrich data with responseReducer if defined (typeof options.responseReducer === 'function' && options.responseReducer(data, response)) || - data + data, + headers: response.headers, }) }) } diff --git a/packages/graphql-hooks/src/types/common-types.ts b/packages/graphql-hooks/src/types/common-types.ts index 780aab43..ce32bcd7 100644 --- a/packages/graphql-hooks/src/types/common-types.ts +++ b/packages/graphql-hooks/src/types/common-types.ts @@ -147,6 +147,7 @@ export interface Result< > { data?: ResponseData error?: APIError + headers?: Response['headers'] } export interface RequestOptions { @@ -163,6 +164,7 @@ export interface GenerateResultOptions< httpError?: HttpError graphQLErrors?: TGraphQLError[] data?: ResponseData + headers?: Response['headers'] } export interface UseClientRequestOptions< diff --git a/packages/graphql-hooks/test-jsdom/integration/GraphQLClient.test.ts b/packages/graphql-hooks/test-jsdom/integration/GraphQLClient.test.ts index d27faf9a..8be77fec 100644 --- a/packages/graphql-hooks/test-jsdom/integration/GraphQLClient.test.ts +++ b/packages/graphql-hooks/test-jsdom/integration/GraphQLClient.test.ts @@ -27,7 +27,7 @@ describe('GraphQLClient', () => { }) it('caches + logs', async () => { - const MOCK_DATA = { data: 'data' } + const MOCK_DATA = { data: 'data', headers: expect.any(Headers) } fetchMock.mockResponseOnce(JSON.stringify(MOCK_DATA)) await client.request({ @@ -81,7 +81,7 @@ describe('GraphQLClient', () => { }) it('Handles async response hooks in correct order', async () => { - const MOCK_DATA = { data: 'data' } + const MOCK_DATA = { data: 'data', headers: expect.any(Headers) } fetchMock.mockResponseOnce(JSON.stringify(MOCK_DATA)) await client.request({ @@ -111,7 +111,7 @@ describe('GraphQLClient', () => { }) it('logs request start and end', async () => { - const MOCK_DATA = { data: 'data' } + const MOCK_DATA = { data: 'data', headers: expect.any(Headers) } fetchMock.mockResponseOnce(JSON.stringify(MOCK_DATA)) const res = await client.request({ @@ -142,7 +142,7 @@ describe('GraphQLClient', () => { }) it('caches first result', async () => { - const MOCK_DATA = { data: 'data' } + const MOCK_DATA = { data: 'data', headers: expect.any(Headers) } fetchMock.mockResponseOnce(() => Promise.resolve(JSON.stringify(MOCK_DATA)) ) diff --git a/packages/graphql-hooks/test-jsdom/unit/GraphQLClient.test.ts b/packages/graphql-hooks/test-jsdom/unit/GraphQLClient.test.ts index 9548635e..d4282fd1 100644 --- a/packages/graphql-hooks/test-jsdom/unit/GraphQLClient.test.ts +++ b/packages/graphql-hooks/test-jsdom/unit/GraphQLClient.test.ts @@ -331,7 +331,8 @@ describe('GraphQLClient', () => { } const result = client.generateResult(data) expect(result).toEqual({ - data: data.data + data: data.data, + headers: undefined }) }) @@ -353,9 +354,37 @@ describe('GraphQLClient', () => { fetchError: data.fetchError, httpError: data.httpError }, - data: data.data + data: data.data, + headers: undefined }) }) + + it('returns headers if available', () => { + const client = new GraphQLClient({ ...validConfig }) + const data = { + graphQLErrors: [ + { message: 'graphQL error 1' }, + { message: 'graphQL error 2' } + ], + fetchError: new Error('fetch error'), + httpError: { status: 400, statusText: '', body: 'http error' }, + data: 'data!', + headers: new Headers({ + 'header-name': 'header-value' + }) + } + const result = client.generateResult(data) + expect(result).toEqual({ + error: { + graphQLErrors: data.graphQLErrors, + fetchError: data.fetchError, + httpError: data.httpError + }, + data: data.data, + headers: expect.any(Headers) + }) + expect(result.headers?.get('header-name')).toBe('header-value') + }) }) describe('getCacheKey', () => { @@ -554,7 +583,8 @@ describe('GraphQLClient', () => { status: 403, statusText: 'Forbidden' } - } + }, + headers: expect.any(Headers) } }) }) @@ -633,6 +663,34 @@ describe('GraphQLClient', () => { expect(_data.contentType).toEqual(headers['content-type']) }) + it('will pass headers to middleware', async () => { + const data = { some: 'data' }, + status = 200, + statusText = 'OK', + headers = { + 'content-type': 'application/json', + 'trace-id': '123456789000' + } + let responseHeaders = new Headers; + const client = new GraphQLClient({ ...validConfig, middleware: [({ addResponseHook }, next) => { + addResponseHook(response => { + responseHeaders = response.headers as Headers + return response + }) + next() + }] }) + fetchMock.mockResponseOnce(JSON.stringify({ data }), { + status, + statusText, + headers + }) + const { data: _data, headers: _headers } = await client.request( + { query: TEST_QUERY }, + ) + expect((_headers as Headers).get('trace-id')).toBe('123456789000') + expect(responseHeaders.get('trace-id')).toBe('123456789000') + }) + describe('GET Support', () => { it('should support client.fetchOptions.method=GET', async () => { fetchMock.mockResponseOnce(JSON.stringify({ data: 'data' })) diff --git a/packages/graphql-hooks/test-jsdom/unit/apqMiddleware.test.ts b/packages/graphql-hooks/test-jsdom/unit/apqMiddleware.test.ts index 1cc98924..2bfc5bb6 100644 --- a/packages/graphql-hooks/test-jsdom/unit/apqMiddleware.test.ts +++ b/packages/graphql-hooks/test-jsdom/unit/apqMiddleware.test.ts @@ -12,7 +12,7 @@ const TEST_QUERY = /* GraphQL */ ` ` describe('APQMiddleware', () => { - const MOCK_DATA = { data: [{ id: 1 }, { id: 2 }, { id: 3 }] } + const MOCK_DATA = { data: [{ id: 1 }, { id: 2 }, { id: 3 }], headers: expect.any(Headers) } const MOCK_ERROR_RESP = { errors: [ {