From e805be927e9e61d67fe060ded14b22d0e6dd4d98 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 14 Oct 2020 22:56:35 -0400 Subject: [PATCH] fix: global Headers class reference (#218) Couple things that have been done to avoid similar problem in future: 1. Use namespaced import style to avoid apparent references to global Headers that are actually references to the imported TypeScript interface. 2. Run tests in jsDOM and Node environments. closes #206 --- .github/workflows/pr.yml | 3 ++- .github/workflows/trunk.yml | 3 ++- README.md | 34 ++++++++++++++-------------------- package.json | 4 +++- src/index.ts | 30 ++++++++++++++++++------------ tests/headers.test.ts | 5 ++++- 6 files changed, 43 insertions(+), 36 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 25a3e1feb..d1ea2cd47 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -10,6 +10,7 @@ jobs: strategy: matrix: node: [10.x, 12.x, 14.x] + environment: [dom, node] name: Node ${{ matrix.node }} steps: - uses: actions/checkout@v2 @@ -28,4 +29,4 @@ jobs: ${{ runner.os }}-yarn- - run: yarn install - run: yarn build - - run: yarn test + - run: yarn test:${{ matrix.environment }} diff --git a/.github/workflows/trunk.yml b/.github/workflows/trunk.yml index 75e778f18..96e769db5 100644 --- a/.github/workflows/trunk.yml +++ b/.github/workflows/trunk.yml @@ -10,6 +10,7 @@ jobs: strategy: matrix: node: [10.x, 12.x, 14.x] + environment: [dom, node] name: Node ${{ matrix.node }} steps: - uses: actions/checkout@v2 @@ -28,7 +29,7 @@ jobs: ${{ runner.os }}-yarn- - run: yarn install - run: yarn build - - run: yarn test + - run: yarn test:${{ matrix.environment }} release: needs: [tests] runs-on: ubuntu-latest diff --git a/README.md b/README.md index 2499a72d6..952b127c1 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ main().catch((error) => console.error(error)) [TypeScript Source](examples/authentication-via-http-header.ts) -#### Dynamically setting headers +#### Incrementally setting headers If you want to set headers after the GraphQLClient has been initialised, you can use the `setHeader()` or `setHeaders()` functions. @@ -106,7 +106,7 @@ client.setHeaders({ }) ``` -### Passing more options to fetch +### Passing more options to `fetch` ```js import { GraphQLClient, gql } from 'graphql-request' @@ -139,7 +139,7 @@ main().catch((error) => console.error(error)) [TypeScript Source](examples/passing-more-options-to-fetch.ts) -### Using variables +### Using GraphQL Document variables ```js import { request, gql } from 'graphql-request' @@ -169,7 +169,7 @@ async function main() { main().catch((error) => console.error(error)) ``` -### Mutations +### GraphQL Mutations ```js import { GraphQLClient, gql } from 'graphql-request' @@ -184,17 +184,17 @@ async function main() { }) const mutation = gql` - mutation AddMovie($title: String!, $releaseDate: Int!) { - insert_movies_one(object: { title: $title, releaseDate: $releaseDate }) { - title - releaseDate - } + mutation AddMovie($title: String!, $releaseDate: Int!) { + insert_movies_one(object: { title: $title, releaseDate: $releaseDate }) { + title + releaseDate + } } ` const variables = { title: 'Inception', - releaseDate: 2010 + releaseDate: 2010, } const data = await graphQLClient.request(mutation, variables) @@ -204,7 +204,6 @@ async function main() { main().catch((error) => console.error(error)) ``` - [TypeScript Source](examples/using-variables.ts) ### Error handling @@ -306,7 +305,7 @@ main().catch((error) => console.error(error)) [TypeScript Source](examples/cookie-support-for-node) -### Using a custom fetch method +### Using a custom `fetch` method ```sh npm install fetch-cookie @@ -314,14 +313,14 @@ npm install fetch-cookie ```js import { GraphQLClient, gql } from 'graphql-request' -import crossFetch from 'cross-fetch'; +import crossFetch from 'cross-fetch' async function main() { const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr' // a cookie jar scoped to the client object const fetch = require('fetch-cookie')(crossFetch) - const graphQLClient = new GraphQLClient(endpoint, { fetch: fetch}) + const graphQLClient = new GraphQLClient(endpoint, { fetch }) const query = gql` { @@ -389,7 +388,7 @@ request('/api/graphql', UploadUserAvatar, { }) ``` -#### NodeJS +#### Node ```js import { createReadStream } from 'fs' @@ -409,11 +408,6 @@ request('/api/graphql', UploadUserAvatar, { [TypeScript Source](examples/receiving-a-raw-response) -### More examples coming soon... - -- Fragments -- Using [`graphql-tag`](https://github.com/apollographql/graphql-tag) - ## FAQ #### Why do I have to install `graphql`? diff --git a/package.json b/package.json index 2eb6227f3..95da7ec8c 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,9 @@ "format": "prettier --write .", "prepublishOnly": "yarn build", "build": "rm -rf dist && tsc -d", - "test": "jest", + "test:node": "jest --testEnvironment node", + "test:dom": "jest --testEnvironment jsdom", + "test": "yarn test:node && yarn test:dom", "release:stable": "dripip stable", "release:preview": "dripip preview", "release:pr": "dripip pr" diff --git a/src/index.ts b/src/index.ts index 2b7dfde3b..a2e6abdea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,23 @@ -import crossFetch from 'cross-fetch' +import crossFetch, * as CrossFetch from 'cross-fetch' import { print } from 'graphql/language/printer' import createRequestBody from './createRequestBody' import { ClientError, GraphQLError, RequestDocument, Variables } from './types' -import { Headers, RequestInit, Response } from './types.dom' +import * as Dom from './types.dom' export { ClientError } from './types' -const resolveHeaders = (headers: RequestInit['headers']): Record => { +/** + * Convert the given headers configuration into a plain object. + */ +const resolveHeaders = (headers: Dom.RequestInit['headers']): Record => { let oHeaders: Record = {} if (headers) { - if (headers instanceof Headers) { + if ( + (typeof Headers !== 'undefined' && headers instanceof Headers) || + headers instanceof CrossFetch.Headers + ) { oHeaders = HeadersInstanceToPlainObject(headers) - } else if (headers instanceof Array) { + } else if (Array.isArray(headers)) { headers.forEach(([name, value]) => { oHeaders[name] = value }) @@ -28,9 +34,9 @@ const resolveHeaders = (headers: RequestInit['headers']): Record */ export class GraphQLClient { private url: string - private options: RequestInit + private options: Dom.RequestInit - constructor(url: string, options?: RequestInit) { + constructor(url: string, options?: Dom.RequestInit) { this.url = url this.options = options || {} } @@ -38,7 +44,7 @@ export class GraphQLClient { async rawRequest( query: string, variables?: V - ): Promise<{ data?: T; extensions?: any; headers: Headers; status: number; errors?: GraphQLError[] }> { + ): 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) @@ -96,7 +102,7 @@ export class GraphQLClient { } } - setHeaders(headers: RequestInit['headers']): GraphQLClient { + setHeaders(headers: Dom.RequestInit['headers']): GraphQLClient { this.options.headers = headers return this } @@ -126,7 +132,7 @@ export async function rawRequest( url: string, query: string, variables?: V -): Promise<{ data?: T; extensions?: any; headers: Headers; status: number; errors?: GraphQLError[] }> { +): Promise<{ data?: T; extensions?: any; headers: Dom.Headers; status: number; errors?: GraphQLError[] }> { const client = new GraphQLClient(url) return client.rawRequest(query, variables) } @@ -179,7 +185,7 @@ export default request /** * todo */ -function getResult(response: Response): Promise { +function getResult(response: Dom.Response): Promise { const contentType = response.headers.get('Content-Type') if (contentType && contentType.startsWith('application/json')) { return response.json() @@ -221,7 +227,7 @@ export function gql(chunks: TemplateStringsArray, ...variables: any[]): string { /** * Convert Headers instance into regular object */ -function HeadersInstanceToPlainObject(headers: Response['headers']): Record { +function HeadersInstanceToPlainObject(headers: Dom.Response['headers']): Record { const o: any = {} headers.forEach((v, k) => { o[k] = v diff --git a/tests/headers.test.ts b/tests/headers.test.ts index 5a0e16fd7..f1e8ba70d 100644 --- a/tests/headers.test.ts +++ b/tests/headers.test.ts @@ -1,3 +1,4 @@ +import * as CrossFetch from 'cross-fetch' import { GraphQLClient } from '../src' import { setupTestServer } from './__helpers' @@ -15,7 +16,9 @@ describe('using class', () => { describe('.setHeaders() sets headers that get sent to the server', () => { test('with headers instance', async () => { const client = new GraphQLClient(ctx.url) - client.setHeaders(new Headers({ 'x-foo': 'bar' })) + // 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 } }`) expect(mock.requests[0].headers['x-foo']).toEqual('bar')