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 2e23267..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' @@ -18,6 +21,8 @@ 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 private isFileUpload: (value: unknown) => boolean @@ -33,6 +38,10 @@ export class AwesomeGraphQLClient< fetchOptions?: TFetchOptions /** Custom query formatter */ formatQuery?: (query: TQuery) => string + /** Custom operation formatter */ + formatOperation?: (operation: any) => string + /** Custom request URL formatter */ + 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 */ @@ -50,6 +59,16 @@ 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.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', @@ -72,6 +91,8 @@ export class AwesomeGraphQLClient< : undefined this.formatQuery = config.formatQuery + this.formatOperation = config.formatOperation || JSON.stringify + this.formatGetRequestUrl = config.formatGetRequestUrl || formatGetRequestUrl this.onError = config.onError this.isFileUpload = config.isFileUpload || isFileUpload } @@ -85,7 +106,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 @@ -203,7 +224,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, 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) diff --git a/packages/awesome-graphql-client/test/browser.test.ts b/packages/awesome-graphql-client/test/browser.test.ts index 0b83718..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 @@ -399,6 +462,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 }[]