From 20f18d3be52c85b4a0928ed8fe9ebba736e26f4c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 26 Jul 2021 19:24:56 +0200 Subject: [PATCH] feat: add operationName to payload if defined in gql (#280) --- src/createRequestBody.ts | 4 +- src/index.ts | 189 +++++++++++++++++++-------- tests/__snapshots__/gql.test.ts.snap | 9 +- tests/gql.test.ts | 11 +- 4 files changed, 148 insertions(+), 65 deletions(-) diff --git a/src/createRequestBody.ts b/src/createRequestBody.ts index 499ad3c3f..b0e9a8bb3 100644 --- a/src/createRequestBody.ts +++ b/src/createRequestBody.ts @@ -16,8 +16,8 @@ const isExtractableFileEnhanced = (value: any): value is ExtractableFile | { pip * (https://github.com/jaydenseric/graphql-multipart-request-spec) * Otherwise returns JSON */ -export default function createRequestBody(query: string, variables?: Variables): string | FormData { - const { clone, files } = extractFiles({ query, variables }, '', isExtractableFileEnhanced) +export default function createRequestBody(query: string, variables?: Variables, operationName?: string): string | FormData { + const { clone, files } = extractFiles({ query, variables, operationName }, '', isExtractableFileEnhanced) if (files.size === 0) { return JSON.stringify(clone) diff --git a/src/index.ts b/src/index.ts index 0a5b5cbf0..c40fd02f3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import crossFetch, * as CrossFetch from 'cross-fetch' +import { OperationDefinitionNode } from 'graphql/language/ast' import { print } from 'graphql/language/printer' import createRequestBody from './createRequestBody' import { ClientError, RequestDocument, Variables } from './types' @@ -32,56 +33,70 @@ const resolveHeaders = (headers: Dom.RequestInit['headers']): Record( - url: string, - query: string, - fetch: any, - options: Dom.RequestInit, - variables?: V, - headers?: HeadersInit, - requestHeaders?: Dom.RequestInit['headers'], -) => { - const body = createRequestBody(query, variables) +const post = async ({ + url, + query, + variables, + operationName, + headers, + fetch, + fetchOptions, +}: { + url: string + query: string + fetch: any + fetchOptions: Dom.RequestInit + variables?: V + headers?: Dom.RequestInit['headers'] + operationName?: string +}) => { + const body = createRequestBody(query, variables, operationName) return await fetch(url, { method: 'POST', headers: { ...(typeof body === 'string' ? { 'Content-Type': 'application/json' } : {}), - ...resolveHeaders(headers), - ...resolveHeaders(requestHeaders) + ...headers, }, body, - ...options + ...fetchOptions, }) } /** * Fetch data using GET method */ -const get = async ( - url: string, - query: string, - fetch: any, - options: Dom.RequestInit, - variables?: V, - headers?: HeadersInit, - requestHeaders?: Dom.RequestInit['headers'], -) => { - const search: string[] = [ - `query=${encodeURIComponent(query.replace(/([\s,]|#[^\n\r]+)+/g, ' ').trim())}`, - ] +const get = async ({ + url, + query, + variables, + operationName, + headers, + fetch, + fetchOptions, +}: { + url: string + query: string + fetch: any + fetchOptions: Dom.RequestInit + variables?: V + headers?: HeadersInit + operationName?: string +}) => { + const search: string[] = [`query=${encodeURIComponent(query.replace(/([\s,]|#[^\n\r]+)+/g, ' ').trim())}`] if (variables) { search.push(`variables=${encodeURIComponent(JSON.stringify(variables))}`) } + if (operationName) { + search.push(`operationName=${encodeURIComponent(operationName)}`) + } + return await fetch(`${url}?${search.join('&')}`, { method: 'GET', - headers: { - ...resolveHeaders(headers), - ...resolveHeaders(requestHeaders) - }, - ...options + headers, + ...fetchOptions, }) } @@ -97,27 +112,27 @@ export class GraphQLClient { this.options = options || {} } - async rawRequest( + rawRequest( query: string, variables?: V, requestHeaders?: Dom.RequestInit['headers'] ): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }> { - let { headers, fetch: localFetch = crossFetch, method = 'POST', ...others } = this.options - - const fetcher = method.toUpperCase() === 'POST' ? post : get - const response = await fetcher(this.url, query, localFetch, others, variables, headers, requestHeaders) - const result = await getResult(response) + let { headers, fetch = crossFetch, method = 'POST', ...fetchOptions } = this.options + let { url } = this - if (response.ok && !result.errors && result.data) { - const { headers, status } = response - return { ...result, headers, status } - } else { - const errorResult = typeof result === 'string' ? { error: result } : result - throw new ClientError( - { ...errorResult, status: response.status, headers: response.headers }, - { query, variables } - ) - } + return makeRequest({ + url, + query, + variables, + headers: { + ...resolveHeaders(headers), + ...resolveHeaders(requestHeaders), + }, + operationName: undefined, + fetch, + method, + fetchOptions, + }) } /** @@ -128,8 +143,25 @@ export class GraphQLClient { variables?: V, requestHeaders?: Dom.RequestInit['headers'] ): Promise { - const query = resolveRequestDocument(document) - const { data } = await this.rawRequest(query, variables, requestHeaders) + let { headers, fetch = crossFetch, method = 'POST', ...fetchOptions } = this.options + let { url } = this + + const { query, operationName } = resolveRequestDocument(document) + + const { data } = await makeRequest({ + url, + query, + variables, + headers: { + ...resolveHeaders(headers), + ...resolveHeaders(requestHeaders), + }, + operationName, + fetch, + method, + fetchOptions, + }) + return data } @@ -156,6 +188,50 @@ export class GraphQLClient { } } +async function makeRequest({ + url, + query, + variables, + headers, + operationName, + fetch, + method = 'POST', + fetchOptions, +}: { + url: string + query: string + variables?: V + headers?: Dom.RequestInit['headers'] + operationName?: string + fetch: any + method: string + fetchOptions: Dom.RequestInit +}): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }> { + const fetcher = method.toUpperCase() === 'POST' ? post : get + + const response = await fetcher({ + url, + query, + variables, + operationName, + headers, + fetch, + fetchOptions, + }) + const result = await getResult(response) + + if (response.ok && !result.errors && result.data) { + const { headers, status } = response + return { ...result, headers, status } + } else { + const errorResult = typeof result === 'string' ? { error: result } : result + throw new ClientError( + { ...errorResult, status: response.status, headers: response.headers }, + { query, variables } + ) + } +} + /** * todo */ @@ -231,9 +307,20 @@ function getResult(response: Dom.Response): Promise { * helpers */ -function resolveRequestDocument(document: RequestDocument): string { - if (typeof document === 'string') return document - return print(document) +function resolveRequestDocument(document: RequestDocument): { query: string; operationName?: string } { + if (typeof document === 'string') return { query: document } + + let operationName = undefined + + let operationDefinitions = document.definitions.filter( + (definition) => definition.kind === 'OperationDefinition' + ) as OperationDefinitionNode[] + + if (operationDefinitions.length === 1) { + operationName = operationDefinitions[0].name?.value + } + + return { query: print(document), operationName } } /** diff --git a/tests/__snapshots__/gql.test.ts.snap b/tests/__snapshots__/gql.test.ts.snap index 00ebd8d7f..be7356239 100644 --- a/tests/__snapshots__/gql.test.ts.snap +++ b/tests/__snapshots__/gql.test.ts.snap @@ -5,10 +5,9 @@ Object { "requests": Array [ Object { "body": Object { - "query": "{ - query { - users - } + "operationName": "allUsers", + "query": "query allUsers { + users } ", }, @@ -16,7 +15,7 @@ Object { "accept": "*/*", "accept-encoding": "gzip,deflate", "connection": "close", - "content-length": "45", + "content-length": "69", "content-type": "application/json", "host": "DYNAMIC", "user-agent": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)", diff --git a/tests/gql.test.ts b/tests/gql.test.ts index cc064ea5e..6d8252e87 100644 --- a/tests/gql.test.ts +++ b/tests/gql.test.ts @@ -9,13 +9,10 @@ describe('gql', () => { const mock = ctx.res({ body: { data: { foo: 1 } } }) await request( ctx.url, - gql` - { - query { - users - } - } - ` + gql`query allUsers { + users +} +` ) expect(mock).toMatchSnapshot() })