From 717d6462e525ad7471218f5a228fcc6de4a7843c Mon Sep 17 00:00:00 2001 From: Erik O'Leary Date: Thu, 28 Dec 2023 14:39:47 -0600 Subject: [PATCH 1/4] Added ability to override how operation is serialized --- .../src/AwesomeGraphQLClient.ts | 11 ++++- .../test/browser.test.ts | 47 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts b/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts index 2e23267..c686b9b 100644 --- a/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts +++ b/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts @@ -18,6 +18,7 @@ export class AwesomeGraphQLClient< private fetch: (url: string, options?: TFetchOptions) => Promise private fetchOptions?: TFetchOptions private formatQuery?: (query: TQuery) => string + private formatOperation: (operation: any) => string private FormData: any private onError?: (error: GraphQLRequestError | Error) => void private isFileUpload: (value: unknown) => boolean @@ -33,6 +34,8 @@ export class AwesomeGraphQLClient< fetchOptions?: TFetchOptions /** Custom query formatter */ formatQuery?: (query: TQuery) => string + /** Custom operation formatter */ + formatOperation?: (operation: any) => string /** Callback will be called on error */ onError?: (error: GraphQLRequestError | Error) => void /** Custom predicate function for checking if value is a file */ @@ -50,6 +53,11 @@ export class AwesomeGraphQLClient< 'Invalid config value: `formatQuery` must be a function', ) + assert( + !config.formatOperation || typeof config.formatOperation === 'function', + 'Invalid config value: `formatOperation` must be a function', + ) + assert( !config.onError || typeof config.onError === 'function', 'Invalid config value: `onError` must be a function', @@ -72,6 +80,7 @@ export class AwesomeGraphQLClient< : undefined this.formatQuery = config.formatQuery + this.formatOperation = config.formatOperation || JSON.stringify; this.onError = config.onError this.isFileUpload = config.isFileUpload || isFileUpload } @@ -85,7 +94,7 @@ export class AwesomeGraphQLClient< '', this.isFileUpload as (value: unknown) => value is FileUpload, ) - const operationJSON = JSON.stringify(clone) + const operationJSON = this.formatOperation(clone) if (files.size === 0) { return operationJSON diff --git a/packages/awesome-graphql-client/test/browser.test.ts b/packages/awesome-graphql-client/test/browser.test.ts index 0b83718..e7bb073 100644 --- a/packages/awesome-graphql-client/test/browser.test.ts +++ b/packages/awesome-graphql-client/test/browser.test.ts @@ -399,6 +399,53 @@ it('sends additional headers', async () => { }) }) +it('Custom formats operations', async () => { + interface GetUsers { + users: { id: number; login: string }[] + } + + let extraValue: string = ""; + + server = await createServer( + ` + type Query { + hello: String! + } + `, + { + Query: { + hello(source, args, val) { + const { body } = val.reply.request; + // const rqe = reply.request; + if (body !== null && typeof body === "object" && "otherValue" in body) + extraValue = body?.otherValue as string; + return 'world!' + }, + }, + }, + ) + + const query = gql` + query Hello { + hello + } + ` + + const client = new AwesomeGraphQLClient({ + endpoint: server.endpoint, + formatOperation: operation => { + return JSON.stringify({ + ...operation, + otherValue: "test" + }); + }, + }) + + await client.request(query) + + expect(extraValue).toBe("test"); +}) + it('requestSafe returns data and response on success', async () => { interface GetUsers { users: { id: number; login: string }[] From 26b088d58ea882318010d33896ba3e2709dd251a Mon Sep 17 00:00:00 2001 From: Erik O'Leary Date: Thu, 28 Dec 2023 14:51:37 -0600 Subject: [PATCH 2/4] Add support for custom formatting of GET request URLs --- .../src/AwesomeGraphQLClient.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts b/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts index c686b9b..ead96a8 100644 --- a/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts +++ b/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts @@ -18,6 +18,7 @@ export class AwesomeGraphQLClient< private fetch: (url: string, options?: TFetchOptions) => Promise private fetchOptions?: TFetchOptions private formatQuery?: (query: TQuery) => string + private formatGetRequestUrl: (operation: any) => string private formatOperation: (operation: any) => string private FormData: any private onError?: (error: GraphQLRequestError | Error) => void @@ -36,6 +37,8 @@ export class AwesomeGraphQLClient< formatQuery?: (query: TQuery) => string /** Custom operation formatter */ formatOperation?: (operation: any) => string + /** Custom request URL formatter */ + formatGetRequestUrl?: (url: string) => string /** Callback will be called on error */ onError?: (error: GraphQLRequestError | Error) => void /** Custom predicate function for checking if value is a file */ @@ -58,6 +61,11 @@ export class AwesomeGraphQLClient< 'Invalid config value: `formatOperation` must be a function', ) + assert( + !config.formatGetRequestUrl || typeof config.formatGetRequestUrl === 'function', + 'Invalid config value: `formatRequestUrl` must be a function', + ) + assert( !config.onError || typeof config.onError === 'function', 'Invalid config value: `onError` must be a function', @@ -81,6 +89,7 @@ export class AwesomeGraphQLClient< this.formatQuery = config.formatQuery this.formatOperation = config.formatOperation || JSON.stringify; + this.formatGetRequestUrl = config.formatGetRequestUrl || formatGetRequestUrl; this.onError = config.onError this.isFileUpload = config.isFileUpload || isFileUpload } @@ -212,7 +221,7 @@ export class AwesomeGraphQLClient< let response: TRequestResult | Response if (options.method?.toUpperCase() === 'GET') { - const url = formatGetRequestUrl({ + const url = this.formatGetRequestUrl({ endpoint: this.endpoint, query: queryAsString, variables, From acea5f83e10ad2e73dd77ebde5ca668dab1e9d23 Mon Sep 17 00:00:00 2001 From: Erik O'Leary Date: Fri, 29 Dec 2023 08:35:39 -0600 Subject: [PATCH 3/4] Updated readme, fixed get request url args --- README.md | 2 ++ .../src/AwesomeGraphQLClient.ts | 11 +++++++---- .../src/util/formatGetRequestUrl.ts | 13 ++++++++----- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 65be0e6..edbf64d 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,8 @@ const client = new AwesomeGraphQLClient(config) - `fetchOptions`: _object_ - Overrides for fetch options - `FormData`: _object_ - FormData polyfill (necessary in NodeJS if you are using file upload, see [example](#nodejs)) - `formatQuery`: _function(query: any): string_ - Custom query formatter (see [example](#graphql-tag)) +- `formatOperation`: _function(operation: any): string_ - Custom POST operation serializer +- `formatGetRequestUrl`: _function(params: FormatGetRequestUrlParams): string_ - Custom GET request URL formatter - `onError`: _function(error: GraphQLRequestError | Error): void_ - Provided callback will be called before throwing an error (see [example](#error-logging)) - `isFileUpload`: _function(value: unknown): boolean_ - Custom predicate function for checking if value is a file (see [example](#custom-isfileupload-predicate)) diff --git a/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts b/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts index ead96a8..f7afd18 100644 --- a/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts +++ b/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts @@ -3,7 +3,10 @@ import { extractFiles } from 'extract-files' import { GraphQLRequestError } from './GraphQLRequestError' import { assert } from './util/assert' -import { formatGetRequestUrl } from './util/formatGetRequestUrl' +import { + FormatGetRequestUrlParams, + formatGetRequestUrl, +} from './util/formatGetRequestUrl' import { isFileUpload, FileUpload } from './util/isFileUpload' import { isResponseJSON } from './util/isResponseJSON' import { normalizeHeaders } from './util/normalizeHeaders' @@ -38,7 +41,7 @@ export class AwesomeGraphQLClient< /** Custom operation formatter */ formatOperation?: (operation: any) => string /** Custom request URL formatter */ - formatGetRequestUrl?: (url: string) => string + formatGetRequestUrl?: (params: FormatGetRequestUrlParams) => string /** Callback will be called on error */ onError?: (error: GraphQLRequestError | Error) => void /** Custom predicate function for checking if value is a file */ @@ -88,8 +91,8 @@ export class AwesomeGraphQLClient< : undefined this.formatQuery = config.formatQuery - this.formatOperation = config.formatOperation || JSON.stringify; - this.formatGetRequestUrl = config.formatGetRequestUrl || formatGetRequestUrl; + this.formatOperation = config.formatOperation || JSON.stringify + this.formatGetRequestUrl = config.formatGetRequestUrl || formatGetRequestUrl this.onError = config.onError this.isFileUpload = config.isFileUpload || isFileUpload } diff --git a/packages/awesome-graphql-client/src/util/formatGetRequestUrl.ts b/packages/awesome-graphql-client/src/util/formatGetRequestUrl.ts index a2dd12c..72e3a63 100644 --- a/packages/awesome-graphql-client/src/util/formatGetRequestUrl.ts +++ b/packages/awesome-graphql-client/src/util/formatGetRequestUrl.ts @@ -2,15 +2,18 @@ * Returns URL for GraphQL GET Requests: * https://graphql.org/learn/serving-over-http/#get-request */ + +export type FormatGetRequestUrlParams = { + endpoint: string + query: string + variables?: Record +} + export function formatGetRequestUrl({ endpoint, query, variables, -}: { - endpoint: string - query: string - variables?: Record -}): string { +}: FormatGetRequestUrlParams): string { const searchParams = new URLSearchParams() searchParams.set('query', query) From 360fd295923592e6cfb0038c539cd202c7552715 Mon Sep 17 00:00:00 2001 From: Erik O'Leary Date: Fri, 29 Dec 2023 08:43:57 -0600 Subject: [PATCH 4/4] Added test for get formatter --- .../test/browser.test.ts | 77 +++++++++++++++++-- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/packages/awesome-graphql-client/test/browser.test.ts b/packages/awesome-graphql-client/test/browser.test.ts index e7bb073..f7396bc 100644 --- a/packages/awesome-graphql-client/test/browser.test.ts +++ b/packages/awesome-graphql-client/test/browser.test.ts @@ -244,6 +244,69 @@ it('sends GraphQL GET request without variables', async () => { expect(data).toEqual({ users }) }) +it('sends GraphQL GET request with custom args', async () => { + interface GetUsers { + users: { id: number; login: string }[] + } + + const users = [{ id: 10, login: 'admin' }] + + let referenceValue: string = '' + + server = await createServer( + ` + type Query { + users: [User!]! + } + type User { + id: Int! + login: String! + } + `, + { + Query: { + users: (source, args, val) => { + const urlString = val.reply.request.url + const searchParams = new URLSearchParams(urlString) + referenceValue = searchParams.get('reference') as string + return users + }, + }, + }, + ) + + const client = new AwesomeGraphQLClient({ + endpoint: server.endpoint, + fetchOptions: { method: 'GET' }, + formatGetRequestUrl: ({ endpoint, query, variables }) => { + const searchParams = new URLSearchParams() + searchParams.set('query', query) + if (variables && Object.keys(variables).length > 0) { + searchParams.set('variables', JSON.stringify(variables)) + } + searchParams.set('reference', '418aa9adde3d4ad6bbdbfac63d7dfb8e') + return `${endpoint}?${searchParams.toString()}` + }, + }) + + const query = gql` + query GetUsers { + users { + id + login + } + } + ` + + expect(referenceValue).toBe('') + + const data = await client.request(query) + + expect(referenceValue).toBe('418aa9adde3d4ad6bbdbfac63d7dfb8e') + + expect(data).toEqual({ users }) +}) + it('sends GraphQL GET request with variables', async () => { interface GetUser { user: { id: number; login: string } | null @@ -404,7 +467,7 @@ it('Custom formats operations', async () => { users: { id: number; login: string }[] } - let extraValue: string = ""; + let extraValue: string = '' server = await createServer( ` @@ -415,10 +478,10 @@ it('Custom formats operations', async () => { { Query: { hello(source, args, val) { - const { body } = val.reply.request; + const { body } = val.reply.request // const rqe = reply.request; - if (body !== null && typeof body === "object" && "otherValue" in body) - extraValue = body?.otherValue as string; + if (body !== null && typeof body === 'object' && 'otherValue' in body) + extraValue = body?.otherValue as string return 'world!' }, }, @@ -436,14 +499,14 @@ it('Custom formats operations', async () => { formatOperation: operation => { return JSON.stringify({ ...operation, - otherValue: "test" - }); + otherValue: 'test', + }) }, }) await client.request(query) - expect(extraValue).toBe("test"); + expect(extraValue).toBe('test') }) it('requestSafe returns data and response on success', async () => {