From a9553bb30b093f79b9698038bbb651dcdb9c70a1 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 24 Jan 2024 23:02:59 -0500 Subject: [PATCH 1/4] refactor: modualarize api --- src/classes/GraphQLClient.ts | 481 ++++++++++++++ src/functions/batchRequests.ts | 100 +++ src/functions/gql.ts | 20 + src/functions/rawRequest.ts | 68 ++ src/functions/request.ts | 91 +++ src/graphql-ws.ts | 2 +- src/{ => helpers}/constants.ts | 0 src/helpers/graphql.ts | 4 + src/helpers/http.ts | 7 + src/{ => helpers}/resolveRequestDocument.ts | 2 +- src/index.ts | 684 +------------------- src/parseArgs.ts | 88 --- src/{helpers.ts => prelude.ts} | 6 + src/types.ts | 36 +- 14 files changed, 797 insertions(+), 792 deletions(-) create mode 100644 src/classes/GraphQLClient.ts create mode 100644 src/functions/batchRequests.ts create mode 100644 src/functions/gql.ts create mode 100644 src/functions/rawRequest.ts create mode 100644 src/functions/request.ts rename src/{ => helpers}/constants.ts (100%) create mode 100644 src/helpers/graphql.ts create mode 100644 src/helpers/http.ts rename src/{ => helpers}/resolveRequestDocument.ts (96%) delete mode 100644 src/parseArgs.ts rename src/{helpers.ts => prelude.ts} (73%) diff --git a/src/classes/GraphQLClient.ts b/src/classes/GraphQLClient.ts new file mode 100644 index 000000000..5df9eb734 --- /dev/null +++ b/src/classes/GraphQLClient.ts @@ -0,0 +1,481 @@ +import { defaultJsonSerializer } from '../defaultJsonSerializer.js' +import type { BatchRequestDocument, BatchRequestsOptions, BatchResult } from '../functions/batchRequests.js' +import { parseBatchRequestArgs } from '../functions/batchRequests.js' +import { parseRawRequestArgs } from '../functions/rawRequest.js' +import { parseRequestArgs } from '../functions/request.js' +import { + ACCEPT_HEADER, + CONTENT_TYPE_GQL, + CONTENT_TYPE_HEADER, + CONTENT_TYPE_JSON, +} from '../helpers/constants.js' +import { cleanQuery } from '../helpers/graphql.js' +import { isGraphQLContentType } from '../helpers/http.js' +import { resolveRequestDocument } from '../helpers/resolveRequestDocument.js' +import { callOrIdentity, HeadersInstanceToPlainObject, uppercase } from '../prelude.js' +import type { + Fetch, + FetchOptions, + HTTPMethodInput, + JsonSerializer, + RequestDocument, + RequestMiddleware, + RequestOptions, + VariablesAndRequestHeadersArgs, +} from '../types.js' +import { + ClientError, + type GraphQLClientResponse, + type RawRequestOptions, + type RequestConfig, + type Variables, +} from '../types.js' +import type { TypedDocumentNode } from '@graphql-typed-document-node/core' + +/** + * GraphQL Client. + */ +export class GraphQLClient { + constructor( + private url: string, + public readonly requestConfig: RequestConfig = {}, + ) {} + + /** + * Send a GraphQL query to the server. + */ + rawRequest: RawRequestMethod = async ( + ...args: RawRequestMethodArgs + ): Promise> => { + const [queryOrOptions, variables, requestHeaders] = args + const rawRequestOptions = parseRawRequestArgs(queryOrOptions, variables, requestHeaders) + + const { + headers, + fetch = globalThis.fetch, + method = `POST`, + requestMiddleware, + responseMiddleware, + ...fetchOptions + } = this.requestConfig + const { url } = this + if (rawRequestOptions.signal !== undefined) { + fetchOptions.signal = rawRequestOptions.signal + } + + const { operationName } = resolveRequestDocument(rawRequestOptions.query) + + return makeRequest({ + url, + query: rawRequestOptions.query, + variables: rawRequestOptions.variables as V, + headers: { + ...resolveHeaders(callOrIdentity(headers)), + ...resolveHeaders(rawRequestOptions.requestHeaders), + }, + operationName, + fetch, + method, + fetchOptions, + middleware: requestMiddleware, + }) + .then((response) => { + if (responseMiddleware) { + responseMiddleware(response) + } + return response + }) + .catch((error) => { + if (responseMiddleware) { + responseMiddleware(error) + } + throw error + }) + } + + /** + * Send a GraphQL document to the server. + */ + async request( + document: RequestDocument | TypedDocumentNode, + ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs + ): Promise + async request(options: RequestOptions): Promise + async request( + documentOrOptions: RequestDocument | TypedDocumentNode | RequestOptions, + ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs + ): Promise { + const [variables, requestHeaders] = variablesAndRequestHeaders + const requestOptions = parseRequestArgs(documentOrOptions, variables, requestHeaders) + + const { + headers, + fetch = globalThis.fetch, + method = `POST`, + requestMiddleware, + responseMiddleware, + ...fetchOptions + } = this.requestConfig + const { url } = this + if (requestOptions.signal !== undefined) { + fetchOptions.signal = requestOptions.signal + } + + const { query, operationName } = resolveRequestDocument(requestOptions.document) + + return makeRequest({ + url, + query, + variables: requestOptions.variables, + headers: { + ...resolveHeaders(callOrIdentity(headers)), + ...resolveHeaders(requestOptions.requestHeaders), + }, + operationName, + fetch, + method, + fetchOptions, + middleware: requestMiddleware, + }) + .then((response) => { + if (responseMiddleware) { + responseMiddleware(response) + } + return response.data + }) + .catch((error) => { + if (responseMiddleware) { + responseMiddleware(error) + } + throw error + }) + } + + /** + * Send GraphQL documents in batch to the server. + */ + // prettier-ignore + batchRequests(documents: BatchRequestDocument[], requestHeaders?: HeadersInit): Promise + // prettier-ignore + batchRequests(options: BatchRequestsOptions): Promise + // prettier-ignore + batchRequests(documentsOrOptions: BatchRequestDocument[] | BatchRequestsOptions, requestHeaders?: HeadersInit): Promise { + const batchRequestOptions = parseBatchRequestArgs(documentsOrOptions, requestHeaders) + const { headers, ...fetchOptions } = this.requestConfig + + if (batchRequestOptions.signal !== undefined) { + fetchOptions.signal = batchRequestOptions.signal + } + + const queries = batchRequestOptions.documents.map( + ({ document }) => resolveRequestDocument(document).query + ) + const variables = batchRequestOptions.documents.map(({ variables }) => variables) + + return makeRequest({ + url: this.url, + query: queries, + // @ts-expect-error TODO reconcile batch variables into system. + variables, + headers: { + ...resolveHeaders(callOrIdentity(headers)), + ...resolveHeaders(batchRequestOptions.requestHeaders), + }, + operationName: undefined, + fetch: this.requestConfig.fetch ?? globalThis.fetch, + method: this.requestConfig.method || `POST`, + fetchOptions, + middleware: this.requestConfig.requestMiddleware, + }) + .then((response) => { + if (this.requestConfig.responseMiddleware) { + this.requestConfig.responseMiddleware(response) + } + return response.data + }) + .catch((error) => { + if (this.requestConfig.responseMiddleware) { + this.requestConfig.responseMiddleware(error) + } + throw error + }) + } + + setHeaders(headers: HeadersInit): GraphQLClient { + this.requestConfig.headers = headers + return this + } + + /** + * Attach a header to the client. All subsequent requests will have this header. + */ + setHeader(key: string, value: string): GraphQLClient { + const { headers } = this.requestConfig + + if (headers) { + // todo what if headers is in nested array form... ? + //@ts-expect-error todo + headers[key] = value + } else { + this.requestConfig.headers = { [key]: value } + } + + return this + } + + /** + * Change the client endpoint. All subsequent requests will send to this endpoint. + */ + setEndpoint(value: string): GraphQLClient { + this.url = value + return this + } +} + +// prettier-ignore +interface RawRequestMethod { + (query: string, variables?: V, requestHeaders?: HeadersInit): Promise> + (options: RawRequestOptions): Promise> +} + +// prettier-ignore +type RawRequestMethodArgs = + | [query: string, variables?: V, requestHeaders?: HeadersInit] + | [RawRequestOptions] + +/** + * Convert the given headers configuration into a plain object. + */ +const resolveHeaders = (headers?: HeadersInit): Record => { + let oHeaders: Record = {} + if (headers) { + if (headers instanceof Headers) { + oHeaders = HeadersInstanceToPlainObject(headers) + } else if (Array.isArray(headers)) { + headers.forEach(([name, value]) => { + if (name && value !== undefined) { + oHeaders[name] = value + } + }) + } else { + oHeaders = headers + } + } + + return oHeaders +} + +const makeRequest = async (params: { + url: string + query: string | string[] + variables?: V + headers?: HeadersInit + operationName?: string + fetch: Fetch + method?: HTTPMethodInput + fetchOptions: FetchOptions + middleware?: RequestMiddleware +}): Promise> => { + const { query, variables, fetchOptions } = params + const fetcher = createHttpMethodFetcher(uppercase(params.method ?? `post`)) + const isBatchingQuery = Array.isArray(params.query) + const response = await fetcher(params) + const result = await getResult(response, fetchOptions.jsonSerializer ?? defaultJsonSerializer) + + const successfullyReceivedData = Array.isArray(result) + ? !result.some(({ data }) => !data) + : Boolean(result.data) + + const successfullyPassedErrorPolicy = + Array.isArray(result) || + !result.errors || + (Array.isArray(result.errors) && !result.errors.length) || + fetchOptions.errorPolicy === `all` || + fetchOptions.errorPolicy === `ignore` + + if (response.ok && successfullyPassedErrorPolicy && successfullyReceivedData) { + // @ts-expect-error TODO fixme + const { errors: _, ...rest } = Array.isArray(result) ? result : result + const data = fetchOptions.errorPolicy === `ignore` ? rest : result + const dataEnvelope = isBatchingQuery ? { data } : data + + // @ts-expect-error TODO + return { + ...dataEnvelope, + headers: response.headers, + status: response.status, + } + } else { + const errorResult = + typeof result === `string` + ? { + error: result, + } + : result + throw new ClientError( + // @ts-expect-error TODO + { ...errorResult, status: response.status, headers: response.headers }, + { query, variables }, + ) + } +} + +const getResult = async ( + response: Response, + jsonSerializer: JsonSerializer, +): Promise< + | { data: object; errors: undefined }[] + | { data: object; errors: undefined } + | { data: undefined; errors: object } + | { data: undefined; errors: object[] } +> => { + const contentType = response.headers.get(CONTENT_TYPE_HEADER) + + if (contentType && isGraphQLContentType(contentType)) { + return jsonSerializer.parse(await response.text()) as any + } else { + return response.text() as any + } +} + +const createHttpMethodFetcher = + (method: 'GET' | 'POST') => + async (params: RequestVerbParams) => { + const { url, query, variables, operationName, fetch, fetchOptions, middleware } = params + + const headers = new Headers(params.headers) + let queryParams = `` + let body = undefined + + if (!headers.has(ACCEPT_HEADER)) { + headers.set(ACCEPT_HEADER, [CONTENT_TYPE_GQL, CONTENT_TYPE_JSON].join(`, `)) + } + + if (method === `POST`) { + body = createRequestBody(query, variables, operationName, fetchOptions.jsonSerializer) + if (typeof body === `string` && !headers.has(CONTENT_TYPE_HEADER)) { + headers.set(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON) + } + } else { + // @ts-expect-error todo needs ADT for TS to understand the different states + queryParams = buildRequestConfig({ + query, + variables, + operationName, + jsonSerializer: fetchOptions.jsonSerializer ?? defaultJsonSerializer, + }) + } + + const init: RequestInit = { + method, + headers, + body, + ...fetchOptions, + } + + let urlResolved = url + let initResolved = init + if (middleware) { + const result = await Promise.resolve(middleware({ ...init, url, operationName, variables })) + const { url: urlNew, ...initNew } = result + urlResolved = urlNew + initResolved = initNew + } + if (queryParams) { + urlResolved = `${urlResolved}?${queryParams}` + } + return await fetch(urlResolved, initResolved) + } + +const createRequestBody = ( + query: string | string[], + variables?: Variables | Variables[], + operationName?: string, + jsonSerializer?: JsonSerializer, +): string => { + const jsonSerializer_ = jsonSerializer ?? defaultJsonSerializer + if (!Array.isArray(query)) { + return jsonSerializer_.stringify({ query, variables, operationName }) + } + + if (typeof variables !== `undefined` && !Array.isArray(variables)) { + throw new Error(`Cannot create request body with given variable type, array expected`) + } + + // Batch support + const payload = query.reduce<{ query: string; variables: Variables | undefined }[]>( + (acc, currentQuery, index) => { + acc.push({ query: currentQuery, variables: variables ? variables[index] : undefined }) + return acc + }, + [], + ) + + return jsonSerializer_.stringify(payload) +} + +/** + * Create query string for GraphQL request + */ +const buildRequestConfig = (params: BuildRequestConfigParams): string => { + if (!Array.isArray(params.query)) { + const params_ = params as BuildRequestConfigParamsSingle + const search: string[] = [`query=${encodeURIComponent(cleanQuery(params_.query))}`] + + if (params.variables) { + search.push(`variables=${encodeURIComponent(params_.jsonSerializer.stringify(params_.variables))}`) + } + + if (params_.operationName) { + search.push(`operationName=${encodeURIComponent(params_.operationName)}`) + } + + return search.join(`&`) + } + + if (typeof params.variables !== `undefined` && !Array.isArray(params.variables)) { + throw new Error(`Cannot create query with given variable type, array expected`) + } + + // Batch support + const params_ = params as BuildRequestConfigParamsBatch + const payload = params.query.reduce<{ query: string; variables: string | undefined }[]>( + (acc, currentQuery, index) => { + acc.push({ + query: cleanQuery(currentQuery), + variables: params_.variables ? params_.jsonSerializer.stringify(params_.variables[index]) : undefined, + }) + return acc + }, + [], + ) + + return `query=${encodeURIComponent(params_.jsonSerializer.stringify(payload))}` +} + +interface RequestVerbParams { + url: string + query: string | string[] + fetch: Fetch + fetchOptions: FetchOptions + variables?: V + headers?: HeadersInit + operationName?: string + middleware?: RequestMiddleware +} + +type BuildRequestConfigParams = BuildRequestConfigParamsSingle | BuildRequestConfigParamsBatch + +type BuildRequestConfigParamsBatch = { + query: string[] + variables: V[] | undefined + operationName: undefined + jsonSerializer: JsonSerializer +} + +type BuildRequestConfigParamsSingle = { + query: string + variables: V | undefined + operationName: string | undefined + jsonSerializer: JsonSerializer +} diff --git a/src/functions/batchRequests.ts b/src/functions/batchRequests.ts new file mode 100644 index 000000000..65b3e8a6b --- /dev/null +++ b/src/functions/batchRequests.ts @@ -0,0 +1,100 @@ +import { GraphQLClient } from '../index.js' +import type { RequestDocument, Variables } from '../types.js' + +export type BatchRequestDocument = { + document: RequestDocument + variables?: V +} + +export interface BatchRequestsOptions { + documents: BatchRequestDocument[] + requestHeaders?: HeadersInit + signal?: RequestInit['signal'] +} + +export interface BatchRequestsExtendedOptions + extends BatchRequestsOptions { + url: string +} + +/** + * Send a batch of GraphQL Document to the GraphQL server for execution. + * + * @example + * + * ```ts + * // You can pass a raw string + * + * await batchRequests('https://foo.bar/graphql', [ + * { + * query: ` + * { + * query { + * users + * } + * }` + * }, + * { + * query: ` + * { + * query { + * users + * } + * }` + * }]) + * + * // You can also pass a GraphQL DocumentNode as query. Convenient if you + * // are using graphql-tag package. + * + * import gql from 'graphql-tag' + * + * await batchRequests('https://foo.bar/graphql', [{ query: gql`...` }]) + * ``` + */ +export const batchRequests: BatchRequests = async (...args: BatchRequestsArgs) => { + const params = parseBatchRequestsArgsExtended(args) + const client = new GraphQLClient(params.url) + return client.batchRequests(params) +} + +type BatchRequestsArgs = + | [url: string, documents: BatchRequestDocument[], requestHeaders?: HeadersInit] + | [options: BatchRequestsExtendedOptions] + +export const parseBatchRequestsArgsExtended = (args: BatchRequestsArgs): BatchRequestsExtendedOptions => { + if (args.length === 1) { + return args[0] + } else { + return { + url: args[0], + documents: args[1], + requestHeaders: args[2], + signal: undefined, + } + } +} + +// prettier-ignore +interface BatchRequests { + (url: string, documents: BatchRequestDocument[], requestHeaders?: HeadersInit): Promise + (options: BatchRequestsExtendedOptions): Promise +} + +export type BatchResult = [Result, ...Result[]] + +interface Result { + data: Data +} + +export const parseBatchRequestArgs = ( + documentsOrOptions: BatchRequestDocument[] | BatchRequestsOptions, + requestHeaders?: HeadersInit, +): BatchRequestsOptions => { + return (documentsOrOptions as BatchRequestsOptions).documents + ? (documentsOrOptions as BatchRequestsOptions) + : { + documents: documentsOrOptions as BatchRequestDocument[], + requestHeaders: requestHeaders, + signal: undefined, + } +} diff --git a/src/functions/gql.ts b/src/functions/gql.ts new file mode 100644 index 000000000..e8dbe5df2 --- /dev/null +++ b/src/functions/gql.ts @@ -0,0 +1,20 @@ +/** + * Convenience passthrough template tag to get the benefits of tooling for the gql template tag. This does not actually parse the input into a GraphQL DocumentNode like graphql-tag package does. It just returns the string with any variables given interpolated. Can save you a bit of performance and having to install another package. + * + * @example + * ``` + * import { gql } from 'graphql-request' + * + * await request('https://foo.bar/graphql', gql`...`) + * ``` + * + * @remarks + * + * Several tools in the Node GraphQL ecosystem are hardcoded to specially treat any template tag named "gql". For example see this prettier issue: https://github.com/prettier/prettier/issues/4360. Using this template tag has no runtime effect beyond variable interpolation. + */ +export const gql = (chunks: TemplateStringsArray, ...variables: unknown[]): string => { + return chunks.reduce( + (acc, chunk, index) => `${acc}${chunk}${index in variables ? String(variables[index]) : ``}`, + ``, + ) +} diff --git a/src/functions/rawRequest.ts b/src/functions/rawRequest.ts new file mode 100644 index 000000000..8c87c890e --- /dev/null +++ b/src/functions/rawRequest.ts @@ -0,0 +1,68 @@ +import { GraphQLClient } from '../index.js' +import type { + GraphQLClientResponse, + RawRequestOptions, + Variables, + VariablesAndRequestHeadersArgs, +} from '../types.js' + +/** + * Send a GraphQL Query to the GraphQL server for execution. + */ +export const rawRequest: RawRequest = async ( + ...args: RawRequestArgs +): Promise> => { + const [urlOrOptions, query, ...variablesAndRequestHeaders] = args + const requestOptions = parseRawRequestExtendedArgs(urlOrOptions, query, ...variablesAndRequestHeaders) + const client = new GraphQLClient(requestOptions.url) + return client.rawRequest({ + ...requestOptions, + }) +} + +// prettier-ignore +interface RawRequest { + (url: string, query: string, ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs): Promise> + (options: RawRequestExtendedOptions): Promise> +} + +// prettier-ignore +type RawRequestArgs = + | [options: RawRequestExtendedOptions, query?: string, ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs] + | [url: string, query?: string, ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs] + +export const parseRawRequestExtendedArgs = ( + urlOrOptions: string | RawRequestExtendedOptions, + query?: string, + ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs +): RawRequestExtendedOptions => { + const [variables, requestHeaders] = variablesAndRequestHeaders + return typeof urlOrOptions === `string` + ? ({ + url: urlOrOptions, + query: query as string, + variables, + requestHeaders, + signal: undefined, + } as unknown as RawRequestExtendedOptions) + : urlOrOptions +} + +export type RawRequestExtendedOptions = { + url: string +} & RawRequestOptions + +export const parseRawRequestArgs = ( + queryOrOptions: string | RawRequestOptions, + variables?: V, + requestHeaders?: HeadersInit, +): RawRequestOptions => { + return (queryOrOptions as RawRequestOptions).query + ? (queryOrOptions as RawRequestOptions) + : ({ + query: queryOrOptions as string, + variables: variables, + requestHeaders: requestHeaders, + signal: undefined, + } as unknown as RawRequestOptions) +} diff --git a/src/functions/request.ts b/src/functions/request.ts new file mode 100644 index 000000000..60bc13a5d --- /dev/null +++ b/src/functions/request.ts @@ -0,0 +1,91 @@ +import type { RequestDocument, RequestOptions, Variables } from '../index.js' +import { GraphQLClient } from '../index.js' +import type { VariablesAndRequestHeadersArgs } from '../types.js' +import type { TypedDocumentNode } from '@graphql-typed-document-node/core' + +/** + * Send a GraphQL Document to the GraphQL server for execution. + * + * @example + * + * ```ts + * // You can pass a raw string + * + * await request('https://foo.bar/graphql', ` + * { + * query { + * users + * } + * } + * `) + * + * // You can also pass a GraphQL DocumentNode. Convenient if you + * // are using graphql-tag package. + * + * import gql from 'graphql-tag' + * + * await request('https://foo.bar/graphql', gql`...`) + * + * // If you don't actually care about using DocumentNode but just + * // want the tooling support for gql template tag like IDE syntax + * // coloring and prettier autoformat then note you can use the + * // passthrough gql tag shipped with graphql-request to save a bit + * // of performance and not have to install another dep into your project. + * + * import { gql } from 'graphql-request' + * + * await request('https://foo.bar/graphql', gql`...`) + * ``` + */ +// REMARKS: In order to have autocomplete for options work make it the first overload. If not +// then autocomplete will instead show the various methods for a string, which is not what we want. + +// prettier-ignore +export async function request(options: RequestExtendedOptions): Promise +// prettier-ignore +export async function request(url: string, document: RequestDocument | TypedDocumentNode, ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs): Promise +// prettier-ignore +// eslint-disable-next-line +export async function request(urlOrOptions: string | RequestExtendedOptions, document?: RequestDocument | TypedDocumentNode, ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs): Promise { + const requestOptions = parseRequestExtendedArgs(urlOrOptions, document, ...variablesAndRequestHeaders) + const client = new GraphQLClient(requestOptions.url) + return client.request({ + ...requestOptions, + }) +} + +export const parseRequestArgs = ( + documentOrOptions: RequestDocument | RequestOptions, + variables?: V, + requestHeaders?: HeadersInit, +): RequestOptions => { + return (documentOrOptions as RequestOptions).document + ? (documentOrOptions as RequestOptions) + : ({ + document: documentOrOptions as RequestDocument, + variables: variables, + requestHeaders: requestHeaders, + signal: undefined, + } as unknown as RequestOptions) +} + +export type RequestExtendedOptions = { + url: string +} & RequestOptions + +export const parseRequestExtendedArgs = ( + urlOrOptions: string | RequestExtendedOptions, + document?: RequestDocument, + ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs +): RequestExtendedOptions => { + const [variables, requestHeaders] = variablesAndRequestHeaders + return typeof urlOrOptions === `string` + ? ({ + url: urlOrOptions, + document: document as RequestDocument, + variables, + requestHeaders, + signal: undefined, + } as unknown as RequestExtendedOptions) + : urlOrOptions +} diff --git a/src/graphql-ws.ts b/src/graphql-ws.ts index fbb14a366..2bf908f9e 100644 --- a/src/graphql-ws.ts +++ b/src/graphql-ws.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import { resolveRequestDocument } from './resolveRequestDocument.js' +import { resolveRequestDocument } from './helpers/resolveRequestDocument.js' import type { RequestDocument, Variables } from './types.js' import { ClientError } from './types.js' import { TypedDocumentNode } from '@graphql-typed-document-node/core' diff --git a/src/constants.ts b/src/helpers/constants.ts similarity index 100% rename from src/constants.ts rename to src/helpers/constants.ts diff --git a/src/helpers/graphql.ts b/src/helpers/graphql.ts new file mode 100644 index 000000000..9471d7af9 --- /dev/null +++ b/src/helpers/graphql.ts @@ -0,0 +1,4 @@ +/** + * Clean a GraphQL document to send it via a GET query + */ +export const cleanQuery = (str: string): string => str.replace(/([\s,]|#[^\n\r]+)+/g, ` `).trim() diff --git a/src/helpers/http.ts b/src/helpers/http.ts new file mode 100644 index 000000000..8205c0968 --- /dev/null +++ b/src/helpers/http.ts @@ -0,0 +1,7 @@ +import { CONTENT_TYPE_GQL, CONTENT_TYPE_JSON } from './constants.js' + +export const isGraphQLContentType = (contentType: string) => { + const contentTypeLower = contentType.toLowerCase() + + return contentTypeLower.includes(CONTENT_TYPE_GQL) || contentTypeLower.includes(CONTENT_TYPE_JSON) +} diff --git a/src/resolveRequestDocument.ts b/src/helpers/resolveRequestDocument.ts similarity index 96% rename from src/resolveRequestDocument.ts rename to src/helpers/resolveRequestDocument.ts index e6369e6ff..285a700fb 100644 --- a/src/resolveRequestDocument.ts +++ b/src/helpers/resolveRequestDocument.ts @@ -1,4 +1,4 @@ -import type { RequestDocument } from './types.js' +import type { RequestDocument } from '../types.js' /** * Refactored imports from `graphql` to be more specific, this helps import only the required files (100KiB) * instead of the entire package (greater than 500KiB) where tree-shaking is not supported. diff --git a/src/index.ts b/src/index.ts index 01bfedf47..78f634627 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,682 +1,24 @@ -import { ACCEPT_HEADER, CONTENT_TYPE_GQL, CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON } from './constants.js' -import { defaultJsonSerializer } from './defaultJsonSerializer.js' -import { HeadersInstanceToPlainObject, uppercase } from './helpers.js' import { - parseBatchRequestArgs, - parseRawRequestArgs, - parseRawRequestExtendedArgs, - parseRequestArgs, - parseRequestExtendedArgs, -} from './parseArgs.js' -import { resolveRequestDocument } from './resolveRequestDocument.js' -import type { - BatchRequestDocument, - FetchOptions, - GraphQLClientResponse, - GraphQLResponse, - HTTPMethodInput, - JsonSerializer, - MaybeLazy, - RequestConfig, - RequestMiddleware, - ResponseMiddleware, - VariablesAndRequestHeadersArgs, -} from './types.js' -import { - BatchRequestsExtendedOptions, - BatchRequestsOptions, - ClientError, - RawRequestExtendedOptions, - RawRequestOptions, - RequestDocument, - RequestExtendedOptions, - RequestOptions, - Variables, -} from './types.js' -import type { TypedDocumentNode } from '@graphql-typed-document-node/core' - -/** - * Convert the given headers configuration into a plain object. - */ -const resolveHeaders = (headers?: HeadersInit): Record => { - let oHeaders: Record = {} - if (headers) { - if (headers instanceof Headers) { - oHeaders = HeadersInstanceToPlainObject(headers) - } else if (Array.isArray(headers)) { - headers.forEach(([name, value]) => { - if (name && value !== undefined) { - oHeaders[name] = value - } - }) - } else { - oHeaders = headers - } - } - - return oHeaders -} - -/** - * Clean a GraphQL document to send it via a GET query - */ -const cleanQuery = (str: string): string => str.replace(/([\s,]|#[^\n\r]+)+/g, ` `).trim() - -type BuildRequestConfigParamsBatch = { - query: string[] - variables: V[] | undefined - operationName: undefined - jsonSerializer: JsonSerializer -} - -type BuildRequestConfigParamsSingle = { - query: string - variables: V | undefined - operationName: string | undefined - jsonSerializer: JsonSerializer -} - -type BuildRequestConfigParams = BuildRequestConfigParamsSingle | BuildRequestConfigParamsBatch - -/** - * Create query string for GraphQL request - */ -const buildRequestConfig = (params: BuildRequestConfigParams): string => { - if (!Array.isArray(params.query)) { - const params_ = params as BuildRequestConfigParamsSingle - const search: string[] = [`query=${encodeURIComponent(cleanQuery(params_.query))}`] - - if (params.variables) { - search.push(`variables=${encodeURIComponent(params_.jsonSerializer.stringify(params_.variables))}`) - } - - if (params_.operationName) { - search.push(`operationName=${encodeURIComponent(params_.operationName)}`) - } - - return search.join(`&`) - } - - if (typeof params.variables !== `undefined` && !Array.isArray(params.variables)) { - throw new Error(`Cannot create query with given variable type, array expected`) - } - - // Batch support - const params_ = params as BuildRequestConfigParamsBatch - const payload = params.query.reduce<{ query: string; variables: string | undefined }[]>( - (acc, currentQuery, index) => { - acc.push({ - query: cleanQuery(currentQuery), - variables: params_.variables ? params_.jsonSerializer.stringify(params_.variables[index]) : undefined, - }) - return acc - }, - [], - ) - - return `query=${encodeURIComponent(params_.jsonSerializer.stringify(payload))}` -} - -type Fetch = (url: string, config: RequestInit) => Promise - -interface RequestVerbParams { - url: string - query: string | string[] - fetch: Fetch - fetchOptions: FetchOptions - variables?: V - headers?: HeadersInit - operationName?: string - middleware?: RequestMiddleware -} - -const createHttpMethodFetcher = - (method: 'GET' | 'POST') => - async (params: RequestVerbParams) => { - const { url, query, variables, operationName, fetch, fetchOptions, middleware } = params - - const headers = new Headers(params.headers) - let queryParams = `` - let body = undefined - - if (!headers.has(ACCEPT_HEADER)) { - headers.set(ACCEPT_HEADER, [CONTENT_TYPE_GQL, CONTENT_TYPE_JSON].join(`, `)) - } - - if (method === `POST`) { - body = createRequestBody(query, variables, operationName, fetchOptions.jsonSerializer) - if (typeof body === `string` && !headers.has(CONTENT_TYPE_HEADER)) { - headers.set(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON) - } - } else { - // @ts-expect-error todo needs ADT for TS to understand the different states - queryParams = buildRequestConfig({ - query, - variables, - operationName, - jsonSerializer: fetchOptions.jsonSerializer ?? defaultJsonSerializer, - }) - } - - const init: RequestInit = { - method, - headers, - body, - ...fetchOptions, - } - - let urlResolved = url - let initResolved = init - if (middleware) { - const result = await Promise.resolve(middleware({ ...init, url, operationName, variables })) - const { url: urlNew, ...initNew } = result - urlResolved = urlNew - initResolved = initNew - } - if (queryParams) { - urlResolved = `${urlResolved}?${queryParams}` - } - return await fetch(urlResolved, initResolved) - } - -/** - * GraphQL Client. - */ -class GraphQLClient { - constructor( - private url: string, - public readonly requestConfig: RequestConfig = {}, - ) {} - - /** - * Send a GraphQL query to the server. - */ - rawRequest: RawRequestMethod = async ( - ...args: RawRequestMethodArgs - ): Promise> => { - const [queryOrOptions, variables, requestHeaders] = args - const rawRequestOptions = parseRawRequestArgs(queryOrOptions, variables, requestHeaders) - - const { - headers, - fetch = globalThis.fetch, - method = `POST`, - requestMiddleware, - responseMiddleware, - ...fetchOptions - } = this.requestConfig - const { url } = this - if (rawRequestOptions.signal !== undefined) { - fetchOptions.signal = rawRequestOptions.signal - } - - const { operationName } = resolveRequestDocument(rawRequestOptions.query) - - return makeRequest({ - url, - query: rawRequestOptions.query, - variables: rawRequestOptions.variables as V, - headers: { - ...resolveHeaders(callOrIdentity(headers)), - ...resolveHeaders(rawRequestOptions.requestHeaders), - }, - operationName, - fetch, - method, - fetchOptions, - middleware: requestMiddleware, - }) - .then((response) => { - if (responseMiddleware) { - responseMiddleware(response) - } - return response - }) - .catch((error) => { - if (responseMiddleware) { - responseMiddleware(error) - } - throw error - }) - } - - /** - * Send a GraphQL document to the server. - */ - async request( - document: RequestDocument | TypedDocumentNode, - ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs - ): Promise - async request(options: RequestOptions): Promise - async request( - documentOrOptions: RequestDocument | TypedDocumentNode | RequestOptions, - ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs - ): Promise { - const [variables, requestHeaders] = variablesAndRequestHeaders - const requestOptions = parseRequestArgs(documentOrOptions, variables, requestHeaders) - - const { - headers, - fetch = globalThis.fetch, - method = `POST`, - requestMiddleware, - responseMiddleware, - ...fetchOptions - } = this.requestConfig - const { url } = this - if (requestOptions.signal !== undefined) { - fetchOptions.signal = requestOptions.signal - } - - const { query, operationName } = resolveRequestDocument(requestOptions.document) - - return makeRequest({ - url, - query, - variables: requestOptions.variables, - headers: { - ...resolveHeaders(callOrIdentity(headers)), - ...resolveHeaders(requestOptions.requestHeaders), - }, - operationName, - fetch, - method, - fetchOptions, - middleware: requestMiddleware, - }) - .then((response) => { - if (responseMiddleware) { - responseMiddleware(response) - } - return response.data - }) - .catch((error) => { - if (responseMiddleware) { - responseMiddleware(error) - } - throw error - }) - } - - /** - * Send GraphQL documents in batch to the server. - */ - // prettier-ignore - batchRequests(documents: BatchRequestDocument[], requestHeaders?: HeadersInit): Promise - // prettier-ignore - batchRequests(options: BatchRequestsOptions): Promise - // prettier-ignore - batchRequests(documentsOrOptions: BatchRequestDocument[] | BatchRequestsOptions, requestHeaders?: HeadersInit): Promise { - const batchRequestOptions = parseBatchRequestArgs(documentsOrOptions, requestHeaders) - const { headers, ...fetchOptions } = this.requestConfig - - if (batchRequestOptions.signal !== undefined) { - fetchOptions.signal = batchRequestOptions.signal - } - - const queries = batchRequestOptions.documents.map( - ({ document }) => resolveRequestDocument(document).query - ) - const variables = batchRequestOptions.documents.map(({ variables }) => variables) - - return makeRequest({ - url: this.url, - query: queries, - // @ts-expect-error TODO reconcile batch variables into system. - variables, - headers: { - ...resolveHeaders(callOrIdentity(headers)), - ...resolveHeaders(batchRequestOptions.requestHeaders), - }, - operationName: undefined, - fetch: this.requestConfig.fetch ?? globalThis.fetch, - method: this.requestConfig.method || `POST`, - fetchOptions, - middleware: this.requestConfig.requestMiddleware, - }) - .then((response) => { - if (this.requestConfig.responseMiddleware) { - this.requestConfig.responseMiddleware(response) - } - return response.data - }) - .catch((error) => { - if (this.requestConfig.responseMiddleware) { - this.requestConfig.responseMiddleware(error) - } - throw error - }) - } - - setHeaders(headers: HeadersInit): GraphQLClient { - this.requestConfig.headers = headers - return this - } - - /** - * Attach a header to the client. All subsequent requests will have this header. - */ - setHeader(key: string, value: string): GraphQLClient { - const { headers } = this.requestConfig - - if (headers) { - // todo what if headers is in nested array form... ? - //@ts-expect-error todo - headers[key] = value - } else { - this.requestConfig.headers = { [key]: value } - } - - return this - } - - /** - * Change the client endpoint. All subsequent requests will send to this endpoint. - */ - setEndpoint(value: string): GraphQLClient { - this.url = value - return this - } -} - -const makeRequest = async (params: { - url: string - query: string | string[] - variables?: V - headers?: HeadersInit - operationName?: string - fetch: Fetch - method?: HTTPMethodInput - fetchOptions: FetchOptions - middleware?: RequestMiddleware -}): Promise> => { - const { query, variables, fetchOptions } = params - const fetcher = createHttpMethodFetcher(uppercase(params.method ?? `post`)) - const isBatchingQuery = Array.isArray(params.query) - const response = await fetcher(params) - const result = await getResult(response, fetchOptions.jsonSerializer ?? defaultJsonSerializer) - - const successfullyReceivedData = Array.isArray(result) - ? !result.some(({ data }) => !data) - : Boolean(result.data) - - const successfullyPassedErrorPolicy = - Array.isArray(result) || - !result.errors || - (Array.isArray(result.errors) && !result.errors.length) || - fetchOptions.errorPolicy === `all` || - fetchOptions.errorPolicy === `ignore` - - if (response.ok && successfullyPassedErrorPolicy && successfullyReceivedData) { - // @ts-expect-error TODO fixme - const { errors: _, ...rest } = Array.isArray(result) ? result : result - const data = fetchOptions.errorPolicy === `ignore` ? rest : result - const dataEnvelope = isBatchingQuery ? { data } : data - - // @ts-expect-error TODO - return { - ...dataEnvelope, - headers: response.headers, - status: response.status, - } - } else { - const errorResult = - typeof result === `string` - ? { - error: result, - } - : result - throw new ClientError( - // @ts-expect-error TODO - { ...errorResult, status: response.status, headers: response.headers }, - { query, variables }, - ) - } -} - -// prettier-ignore -interface RawRequestMethod { - (query: string, variables?: V, requestHeaders?: HeadersInit): Promise> - (options: RawRequestOptions): Promise> -} - -// prettier-ignore -type RawRequestMethodArgs = - | [query: string, variables?: V, requestHeaders?: HeadersInit] - | [RawRequestOptions] - -// prettier-ignore -interface RawRequest { - (url: string, query: string, ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs): Promise> - (options: RawRequestExtendedOptions): Promise> -} - -// prettier-ignore -type RawRequestArgs = - | [options: RawRequestExtendedOptions, query?: string, ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs] - | [url: string, query?: string, ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs] - -/** - * Send a GraphQL Query to the GraphQL server for execution. - */ -const rawRequest: RawRequest = async ( - ...args: RawRequestArgs -): Promise> => { - const [urlOrOptions, query, ...variablesAndRequestHeaders] = args - const requestOptions = parseRawRequestExtendedArgs(urlOrOptions, query, ...variablesAndRequestHeaders) - const client = new GraphQLClient(requestOptions.url) - return client.rawRequest({ - ...requestOptions, - }) -} - -/** - * Send a GraphQL Document to the GraphQL server for execution. - * - * @example - * - * ```ts - * // You can pass a raw string - * - * await request('https://foo.bar/graphql', ` - * { - * query { - * users - * } - * } - * `) - * - * // You can also pass a GraphQL DocumentNode. Convenient if you - * // are using graphql-tag package. - * - * import gql from 'graphql-tag' - * - * await request('https://foo.bar/graphql', gql`...`) - * - * // If you don't actually care about using DocumentNode but just - * // want the tooling support for gql template tag like IDE syntax - * // coloring and prettier autoformat then note you can use the - * // passthrough gql tag shipped with graphql-request to save a bit - * // of performance and not have to install another dep into your project. - * - * import { gql } from 'graphql-request' - * - * await request('https://foo.bar/graphql', gql`...`) - * ``` - */ -// REMARKS: In order to have autocomplete for options work make it the first overload. If not -// then autocomplete will instead show the various methods for a string, which is not what we want. -// prettier-ignore -async function request(options: RequestExtendedOptions): Promise -// prettier-ignore -async function request(url: string, document: RequestDocument | TypedDocumentNode, ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs): Promise -// prettier-ignore -// eslint-disable-next-line -async function request(urlOrOptions: string | RequestExtendedOptions, document?: RequestDocument | TypedDocumentNode, ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs): Promise { - const requestOptions = parseRequestExtendedArgs(urlOrOptions, document, ...variablesAndRequestHeaders) - const client = new GraphQLClient(requestOptions.url) - return client.request({ - ...requestOptions, - }) -} - -/** - * Send a batch of GraphQL Document to the GraphQL server for execution. - * - * @example - * - * ```ts - * // You can pass a raw string - * - * await batchRequests('https://foo.bar/graphql', [ - * { - * query: ` - * { - * query { - * users - * } - * }` - * }, - * { - * query: ` - * { - * query { - * users - * } - * }` - * }]) - * - * // You can also pass a GraphQL DocumentNode as query. Convenient if you - * // are using graphql-tag package. - * - * import gql from 'graphql-tag' - * - * await batchRequests('https://foo.bar/graphql', [{ query: gql`...` }]) - * ``` - */ -const batchRequests: BatchRequests = async (...args: BatchRequestsArgs) => { - const params = parseBatchRequestsArgsExtended(args) - const client = new GraphQLClient(params.url) - return client.batchRequests(params) -} - -interface Result { - data: Data -} - -type BatchResult = [Result, ...Result[]] - -// prettier-ignore -interface BatchRequests { - (url: string, documents: BatchRequestDocument[], requestHeaders?: HeadersInit): Promise - (options: BatchRequestsExtendedOptions): Promise -} - -type BatchRequestsArgs = - | [url: string, documents: BatchRequestDocument[], requestHeaders?: HeadersInit] - | [options: BatchRequestsExtendedOptions] - -const parseBatchRequestsArgsExtended = (args: BatchRequestsArgs): BatchRequestsExtendedOptions => { - if (args.length === 1) { - return args[0] - } else { - return { - url: args[0], - documents: args[1], - requestHeaders: args[2], - signal: undefined, - } - } -} - -const createRequestBody = ( - query: string | string[], - variables?: Variables | Variables[], - operationName?: string, - jsonSerializer?: JsonSerializer, -): string => { - const jsonSerializer_ = jsonSerializer ?? defaultJsonSerializer - if (!Array.isArray(query)) { - return jsonSerializer_.stringify({ query, variables, operationName }) - } - - if (typeof variables !== `undefined` && !Array.isArray(variables)) { - throw new Error(`Cannot create request body with given variable type, array expected`) - } - - // Batch support - const payload = query.reduce<{ query: string; variables: Variables | undefined }[]>( - (acc, currentQuery, index) => { - acc.push({ query: currentQuery, variables: variables ? variables[index] : undefined }) - return acc - }, - [], - ) - - return jsonSerializer_.stringify(payload) -} - -const getResult = async ( - response: Response, - jsonSerializer: JsonSerializer, -): Promise< - | { data: object; errors: undefined }[] - | { data: object; errors: undefined } - | { data: undefined; errors: object } - | { data: undefined; errors: object[] } -> => { - const contentType = response.headers.get(CONTENT_TYPE_HEADER) - - if (contentType && isJsonContentType(contentType)) { - return jsonSerializer.parse(await response.text()) as any - } else { - return response.text() as any - } -} - -const isJsonContentType = (contentType: string) => { - const contentTypeLower = contentType.toLowerCase() - - return contentTypeLower.includes(CONTENT_TYPE_GQL) || contentTypeLower.includes(CONTENT_TYPE_JSON) -} - -const callOrIdentity = (value: MaybeLazy) => { - return typeof value === `function` ? (value as () => T)() : value -} - -/** - * Convenience passthrough template tag to get the benefits of tooling for the gql template tag. This does not actually parse the input into a GraphQL DocumentNode like graphql-tag package does. It just returns the string with any variables given interpolated. Can save you a bit of performance and having to install another package. - * - * @example - * ``` - * import { gql } from 'graphql-request' - * - * await request('https://foo.bar/graphql', gql`...`) - * ``` - * - * @remarks - * - * Several tools in the Node GraphQL ecosystem are hardcoded to specially treat any template tag named "gql". For example see this prettier issue: https://github.com/prettier/prettier/issues/4360. Using this template tag has no runtime effect beyond variable interpolation. - */ -export const gql = (chunks: TemplateStringsArray, ...variables: unknown[]): string => { - return chunks.reduce( - (acc, chunk, index) => `${acc}${chunk}${index in variables ? String(variables[index]) : ``}`, - ``, - ) -} - + type BatchRequestDocument, + type BatchRequestsExtendedOptions, + type BatchRequestsOptions, +} from './functions/batchRequests.js' +import { RequestExtendedOptions } from './functions/request.js' +import { request } from './functions/request.js' +import type { GraphQLResponse, RequestMiddleware, ResponseMiddleware } from './types.js' +import { ClientError, RawRequestOptions, RequestDocument, RequestOptions, Variables } from './types.js' +export { GraphQLClient } from './classes/GraphQLClient.js' +export { batchRequests } from './functions/batchRequests.js' +export { gql } from './functions/gql.js' +export { rawRequest } from './functions/rawRequest.js' export { GraphQLWebSocketClient } from './graphql-ws.js' -export { resolveRequestDocument } from './resolveRequestDocument.js' +export { resolveRequestDocument } from './helpers/resolveRequestDocument.js' export { BatchRequestDocument, - batchRequests, BatchRequestsExtendedOptions, BatchRequestsOptions, ClientError, - GraphQLClient, GraphQLResponse, - rawRequest, - RawRequestExtendedOptions, RawRequestOptions, request, RequestDocument, diff --git a/src/parseArgs.ts b/src/parseArgs.ts deleted file mode 100644 index 474735bc8..000000000 --- a/src/parseArgs.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { - BatchRequestDocument, - BatchRequestsOptions, - RawRequestExtendedOptions, - RawRequestOptions, - RequestDocument, - RequestExtendedOptions, - RequestOptions, - Variables, - VariablesAndRequestHeadersArgs, -} from './types.js' - -export const parseRequestArgs = ( - documentOrOptions: RequestDocument | RequestOptions, - variables?: V, - requestHeaders?: HeadersInit, -): RequestOptions => { - return (documentOrOptions as RequestOptions).document - ? (documentOrOptions as RequestOptions) - : ({ - document: documentOrOptions as RequestDocument, - variables: variables, - requestHeaders: requestHeaders, - signal: undefined, - } as unknown as RequestOptions) -} - -export const parseRawRequestArgs = ( - queryOrOptions: string | RawRequestOptions, - variables?: V, - requestHeaders?: HeadersInit, -): RawRequestOptions => { - return (queryOrOptions as RawRequestOptions).query - ? (queryOrOptions as RawRequestOptions) - : ({ - query: queryOrOptions as string, - variables: variables, - requestHeaders: requestHeaders, - signal: undefined, - } as unknown as RawRequestOptions) -} - -export const parseBatchRequestArgs = ( - documentsOrOptions: BatchRequestDocument[] | BatchRequestsOptions, - requestHeaders?: HeadersInit, -): BatchRequestsOptions => { - return (documentsOrOptions as BatchRequestsOptions).documents - ? (documentsOrOptions as BatchRequestsOptions) - : { - documents: documentsOrOptions as BatchRequestDocument[], - requestHeaders: requestHeaders, - signal: undefined, - } -} - -export const parseRequestExtendedArgs = ( - urlOrOptions: string | RequestExtendedOptions, - document?: RequestDocument, - ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs -): RequestExtendedOptions => { - const [variables, requestHeaders] = variablesAndRequestHeaders - return (urlOrOptions as RequestExtendedOptions).document - ? (urlOrOptions as RequestExtendedOptions) - : ({ - url: urlOrOptions as string, - document: document as RequestDocument, - variables, - requestHeaders, - signal: undefined, - } as unknown as RequestExtendedOptions) -} - -export const parseRawRequestExtendedArgs = ( - urlOrOptions: string | RawRequestExtendedOptions, - query?: string, - ...variablesAndRequestHeaders: VariablesAndRequestHeadersArgs -): RawRequestExtendedOptions => { - const [variables, requestHeaders] = variablesAndRequestHeaders - return (urlOrOptions as RawRequestExtendedOptions).query - ? (urlOrOptions as RawRequestExtendedOptions) - : ({ - url: urlOrOptions as string, - query: query as string, - variables, - requestHeaders, - signal: undefined, - } as unknown as RawRequestExtendedOptions) -} diff --git a/src/helpers.ts b/src/prelude.ts similarity index 73% rename from src/helpers.ts rename to src/prelude.ts index c573b6861..21a6c4264 100644 --- a/src/helpers.ts +++ b/src/prelude.ts @@ -14,3 +14,9 @@ export const HeadersInstanceToPlainObject = (headers: Response['headers']): Reco }) return o } + +export const callOrIdentity = (value: MaybeLazy) => { + return typeof value === `function` ? (value as () => T)() : value +} + +export type MaybeLazy = T | (() => T) diff --git a/src/types.ts b/src/types.ts index 905da6a16..c1a89211f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import type { RemoveIndex } from './helpers.js' +import type { MaybeLazy, RemoveIndex } from './prelude.js' import type { TypedDocumentNode } from '@graphql-typed-document-node/core' import type { GraphQLError } from 'graphql/error/GraphQLError.js' import type { DocumentNode } from 'graphql/language/ast.js' @@ -76,8 +76,6 @@ export class ClientError extends Error { } } -export type MaybeLazy = T | (() => T) - export type RequestDocument = string | DocumentNode export interface GraphQLClientResponse { @@ -99,11 +97,6 @@ export interface RequestConfig extends Omit, jsonSerializer?: JsonSerializer } -export type BatchRequestDocument = { - document: RequestDocument - variables?: V -} - export type RawRequestOptions = { query: string requestHeaders?: HeadersInit @@ -111,8 +104,8 @@ export type RawRequestOptions = { } & (V extends Record ? { variables?: V } : keyof RemoveIndex extends never - ? { variables?: V } - : { variables: V }) + ? { variables?: V } + : { variables: V }) export type RequestOptions = { document: RequestDocument | TypedDocumentNode @@ -121,27 +114,8 @@ export type RequestOptions = { } & (V extends Record ? { variables?: V } : keyof RemoveIndex extends never - ? { variables?: V } - : { variables: V }) - -export interface BatchRequestsOptions { - documents: BatchRequestDocument[] - requestHeaders?: HeadersInit - signal?: RequestInit['signal'] -} - -export type RequestExtendedOptions = { - url: string -} & RequestOptions - -export type RawRequestExtendedOptions = { - url: string -} & RawRequestOptions - -export interface BatchRequestsExtendedOptions - extends BatchRequestsOptions { - url: string -} + ? { variables?: V } + : { variables: V }) export type ResponseMiddleware = (response: GraphQLClientResponse | ClientError | Error) => void From 13431e094cf12f4892ed2682d8efe71de73aff73 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 24 Jan 2024 23:04:17 -0500 Subject: [PATCH 2/4] refactor --- src/classes/GraphQLClient.ts | 2 +- src/index.ts | 2 +- src/{ => lib}/graphql-ws.ts | 6 +++--- src/{ => lib}/prelude.ts | 0 src/types.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename src/{ => lib}/graphql-ws.ts (97%) rename src/{ => lib}/prelude.ts (100%) diff --git a/src/classes/GraphQLClient.ts b/src/classes/GraphQLClient.ts index 5df9eb734..9d4aac644 100644 --- a/src/classes/GraphQLClient.ts +++ b/src/classes/GraphQLClient.ts @@ -12,7 +12,7 @@ import { import { cleanQuery } from '../helpers/graphql.js' import { isGraphQLContentType } from '../helpers/http.js' import { resolveRequestDocument } from '../helpers/resolveRequestDocument.js' -import { callOrIdentity, HeadersInstanceToPlainObject, uppercase } from '../prelude.js' +import { callOrIdentity, HeadersInstanceToPlainObject, uppercase } from '../lib/prelude.js' import type { Fetch, FetchOptions, diff --git a/src/index.ts b/src/index.ts index 78f634627..dff583afc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,8 +11,8 @@ export { GraphQLClient } from './classes/GraphQLClient.js' export { batchRequests } from './functions/batchRequests.js' export { gql } from './functions/gql.js' export { rawRequest } from './functions/rawRequest.js' -export { GraphQLWebSocketClient } from './graphql-ws.js' export { resolveRequestDocument } from './helpers/resolveRequestDocument.js' +export { GraphQLWebSocketClient } from './lib/graphql-ws.js' export { BatchRequestDocument, BatchRequestsExtendedOptions, diff --git a/src/graphql-ws.ts b/src/lib/graphql-ws.ts similarity index 97% rename from src/graphql-ws.ts rename to src/lib/graphql-ws.ts index 2bf908f9e..237d36f0c 100644 --- a/src/graphql-ws.ts +++ b/src/lib/graphql-ws.ts @@ -1,7 +1,7 @@ /* eslint-disable */ -import { resolveRequestDocument } from './helpers/resolveRequestDocument.js' -import type { RequestDocument, Variables } from './types.js' -import { ClientError } from './types.js' +import { resolveRequestDocument } from '../helpers/resolveRequestDocument.js' +import type { RequestDocument, Variables } from '../types.js' +import { ClientError } from '../types.js' import { TypedDocumentNode } from '@graphql-typed-document-node/core' // import type WebSocket from 'ws' diff --git a/src/prelude.ts b/src/lib/prelude.ts similarity index 100% rename from src/prelude.ts rename to src/lib/prelude.ts diff --git a/src/types.ts b/src/types.ts index c1a89211f..c372c726a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import type { MaybeLazy, RemoveIndex } from './prelude.js' +import type { MaybeLazy, RemoveIndex } from './lib/prelude.js' import type { TypedDocumentNode } from '@graphql-typed-document-node/core' import type { GraphQLError } from 'graphql/error/GraphQLError.js' import type { DocumentNode } from 'graphql/language/ast.js' From a291985287f74a764de9b1f79ad7381a0e6c825b Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 24 Jan 2024 23:08:18 -0500 Subject: [PATCH 3/4] refactor --- examples/community-graphql-code-generator.ts | 2 +- .../configuration-fetch-custom-function.ts | 2 +- examples/configuration-fetch-options.ts | 2 +- .../configuration-incremental-endpoint.ts | 2 +- ...nfiguration-incremental-request-headers.ts | 2 +- .../configuration-request-json-serializer.ts | 2 +- examples/graphql-batching-requests.ts | 2 +- examples/graphql-document-variables.ts | 2 +- examples/graphql-mutations.ts | 2 +- examples/other-error-handling.ts | 2 +- examples/other-middleware.ts | 4 +- .../request-authentication-via-http-header.ts | 2 +- examples/request-cancellation.ts | 2 +- examples/request-handle-raw-response.ts | 2 +- .../request-headers-dynamic-per-request.ts | 2 +- .../request-headers-static-per-request.ts | 2 +- examples/request-method-get.ts | 2 +- examples/typescript-typed-document-node.ts | 2 +- package.json | 4 +- src/classes/GraphQLClient.ts | 18 +++------ src/entrypoints/main.ts | 37 +++++++++++++++++++ src/functions/batchRequests.ts | 4 +- src/functions/rawRequest.ts | 4 +- src/functions/request.ts | 10 +++-- src/{ => helpers}/defaultJsonSerializer.ts | 0 src/helpers/graphql.ts | 4 -- src/helpers/http.ts | 7 ---- src/helpers/resolveRequestDocument.ts | 2 +- src/{ => helpers}/types.ts | 2 +- src/index.ts | 31 ---------------- src/lib/graphql-ws.ts | 4 +- src/lib/graphql.ts | 12 ++++++ src/{helpers/constants.ts => lib/http.ts} | 0 tests/batching.test.ts | 2 +- tests/custom-fetch.test.ts | 2 +- tests/document-node.test.ts | 2 +- tests/endpoint.test.ts | 2 +- tests/errorPolicy.test.ts | 2 +- tests/exports.ts | 2 +- tests/fetch.test.ts | 2 +- tests/general.test.ts | 2 +- tests/gql.test.ts | 2 +- tests/headers.test.ts | 2 +- tests/json-serializer.test.ts | 4 +- tests/signal.test.ts | 2 +- tests/typed-document-node.test.ts | 2 +- 46 files changed, 105 insertions(+), 100 deletions(-) create mode 100644 src/entrypoints/main.ts rename src/{ => helpers}/defaultJsonSerializer.ts (100%) delete mode 100644 src/helpers/graphql.ts delete mode 100644 src/helpers/http.ts rename src/{ => helpers}/types.ts (98%) delete mode 100644 src/index.ts create mode 100644 src/lib/graphql.ts rename src/{helpers/constants.ts => lib/http.ts} (100%) diff --git a/examples/community-graphql-code-generator.ts b/examples/community-graphql-code-generator.ts index b3769c7f5..77a59c399 100644 --- a/examples/community-graphql-code-generator.ts +++ b/examples/community-graphql-code-generator.ts @@ -12,7 +12,7 @@ * @see https://www.the-guild.dev/graphql/codegen/docs/guides/react-vue. */ -import request, { gql } from '../src/index.js' +import request, { gql } from '../src/entrypoints/main.js' // @ts-expect-error todo make this actually work import { graphql } from './gql/gql' diff --git a/examples/configuration-fetch-custom-function.ts b/examples/configuration-fetch-custom-function.ts index 1e385fbb3..fffaa5455 100644 --- a/examples/configuration-fetch-custom-function.ts +++ b/examples/configuration-fetch-custom-function.ts @@ -1,4 +1,4 @@ -import { gql, GraphQLClient } from '../src/index.js' +import { gql, GraphQLClient } from '../src/entrypoints/main.js' import fetchCookie from 'fetch-cookie' const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` diff --git a/examples/configuration-fetch-options.ts b/examples/configuration-fetch-options.ts index 1330e0b8c..e412eec75 100644 --- a/examples/configuration-fetch-options.ts +++ b/examples/configuration-fetch-options.ts @@ -1,4 +1,4 @@ -import { gql, GraphQLClient } from '../src/index.js' +import { gql, GraphQLClient } from '../src/entrypoints/main.js' const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` diff --git a/examples/configuration-incremental-endpoint.ts b/examples/configuration-incremental-endpoint.ts index 1830b8e7f..bb60c1136 100644 --- a/examples/configuration-incremental-endpoint.ts +++ b/examples/configuration-incremental-endpoint.ts @@ -2,7 +2,7 @@ * If you want to change the endpoint after the GraphQLClient has been initialized, you can use the `setEndpoint()` function. */ -import { GraphQLClient } from '../src/index.js' +import { GraphQLClient } from '../src/entrypoints/main.js' const client = new GraphQLClient(`https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr`) diff --git a/examples/configuration-incremental-request-headers.ts b/examples/configuration-incremental-request-headers.ts index 0a423850c..1ef60bd52 100644 --- a/examples/configuration-incremental-request-headers.ts +++ b/examples/configuration-incremental-request-headers.ts @@ -2,7 +2,7 @@ * If you want to set headers after the GraphQLClient has been initialized, you can use the `setHeader()` or `setHeaders()` functions. */ -import { GraphQLClient } from '../src/index.js' +import { GraphQLClient } from '../src/entrypoints/main.js' const client = new GraphQLClient(`https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr`) diff --git a/examples/configuration-request-json-serializer.ts b/examples/configuration-request-json-serializer.ts index 7a3f4a91b..f699b0137 100644 --- a/examples/configuration-request-json-serializer.ts +++ b/examples/configuration-request-json-serializer.ts @@ -3,7 +3,7 @@ * An original use case for this feature is `BigInt` support: */ -import { gql, GraphQLClient } from '../src/index.js' +import { gql, GraphQLClient } from '../src/entrypoints/main.js' import JSONbig from 'json-bigint' const jsonSerializer = JSONbig({ useNativeBigInt: true }) diff --git a/examples/graphql-batching-requests.ts b/examples/graphql-batching-requests.ts index 06bbdc54a..1a5a6685b 100644 --- a/examples/graphql-batching-requests.ts +++ b/examples/graphql-batching-requests.ts @@ -2,7 +2,7 @@ * It is possible with `graphql-request` to use batching via the `batchRequests()` function. * @see https://github.com/graphql/graphql-over-http/blob/main/rfcs/Batching.md */ -import { batchRequests, gql } from '../src/index.js' +import { batchRequests, gql } from '../src/entrypoints/main.js' const endpoint = `https://api.spacex.land/graphql/` diff --git a/examples/graphql-document-variables.ts b/examples/graphql-document-variables.ts index 64c163376..45dfa3272 100644 --- a/examples/graphql-document-variables.ts +++ b/examples/graphql-document-variables.ts @@ -1,4 +1,4 @@ -import { gql, request } from '../src/index.js' +import { gql, request } from '../src/entrypoints/main.js' const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` diff --git a/examples/graphql-mutations.ts b/examples/graphql-mutations.ts index fe7c52160..405bd1be5 100644 --- a/examples/graphql-mutations.ts +++ b/examples/graphql-mutations.ts @@ -1,4 +1,4 @@ -import { gql, GraphQLClient } from '../src/index.js' +import { gql, GraphQLClient } from '../src/entrypoints/main.js' const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` diff --git a/examples/other-error-handling.ts b/examples/other-error-handling.ts index 52bbf9b37..4a2a46170 100644 --- a/examples/other-error-handling.ts +++ b/examples/other-error-handling.ts @@ -1,4 +1,4 @@ -import { gql, request } from '../src/index.js' +import { gql, request } from '../src/entrypoints/main.js' const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` diff --git a/examples/other-middleware.ts b/examples/other-middleware.ts index 548763583..4cfbcbadd 100644 --- a/examples/other-middleware.ts +++ b/examples/other-middleware.ts @@ -2,8 +2,8 @@ * It's possible to use a middleware to pre-process any request or handle raw response. */ -import { GraphQLClient } from '../src/index.js' -import type { RequestMiddleware, ResponseMiddleware } from '../src/types.js' +import { GraphQLClient } from '../src/entrypoints/main.js' +import type { RequestMiddleware, ResponseMiddleware } from '../src/helpers/types.js' const endpoint = `https://api.spacex.land/graphql/` diff --git a/examples/request-authentication-via-http-header.ts b/examples/request-authentication-via-http-header.ts index 921af8866..ab26ba00b 100644 --- a/examples/request-authentication-via-http-header.ts +++ b/examples/request-authentication-via-http-header.ts @@ -1,4 +1,4 @@ -import { gql, GraphQLClient } from '../src/index.js' +import { gql, GraphQLClient } from '../src/entrypoints/main.js' const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` diff --git a/examples/request-cancellation.ts b/examples/request-cancellation.ts index b9581b4c6..337ecac83 100644 --- a/examples/request-cancellation.ts +++ b/examples/request-cancellation.ts @@ -2,7 +2,7 @@ * It is possible to cancel a request using an `AbortController` signal. */ -import { gql, GraphQLClient } from '../src/index.js' +import { gql, GraphQLClient } from '../src/entrypoints/main.js' const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` diff --git a/examples/request-handle-raw-response.ts b/examples/request-handle-raw-response.ts index cb1844122..4b680bd52 100644 --- a/examples/request-handle-raw-response.ts +++ b/examples/request-handle-raw-response.ts @@ -3,7 +3,7 @@ * If you need to access the `extensions` key you can use the `rawRequest` method: */ -import { gql, rawRequest } from '../src/index.js' +import { gql, rawRequest } from '../src/entrypoints/main.js' const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` diff --git a/examples/request-headers-dynamic-per-request.ts b/examples/request-headers-dynamic-per-request.ts index e31562a4c..ebede9862 100644 --- a/examples/request-headers-dynamic-per-request.ts +++ b/examples/request-headers-dynamic-per-request.ts @@ -3,7 +3,7 @@ * To do that, pass a function that returns the headers to the `headers` property when creating a new `GraphQLClient`. */ -import { gql, GraphQLClient } from '../src/index.js' +import { gql, GraphQLClient } from '../src/entrypoints/main.js' const client = new GraphQLClient(`https://some-api`, { headers: () => ({ 'X-Sent-At-Time': Date.now().toString() }), diff --git a/examples/request-headers-static-per-request.ts b/examples/request-headers-static-per-request.ts index 63dcf7658..5e13dc412 100644 --- a/examples/request-headers-static-per-request.ts +++ b/examples/request-headers-static-per-request.ts @@ -2,7 +2,7 @@ * It is possible to pass custom headers for each request. `request()` and `rawRequest()` accept a header object as the third parameter */ -import { gql, GraphQLClient } from '../src/index.js' +import { gql, GraphQLClient } from '../src/entrypoints/main.js' const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` diff --git a/examples/request-method-get.ts b/examples/request-method-get.ts index 33a0062c8..068605540 100644 --- a/examples/request-method-get.ts +++ b/examples/request-method-get.ts @@ -1,7 +1,7 @@ /** * Queries can be sent as an HTTP GET request: */ -import { gql, GraphQLClient } from '../src/index.js' +import { gql, GraphQLClient } from '../src/entrypoints/main.js' const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` diff --git a/examples/typescript-typed-document-node.ts b/examples/typescript-typed-document-node.ts index c165930a7..a38331218 100644 --- a/examples/typescript-typed-document-node.ts +++ b/examples/typescript-typed-document-node.ts @@ -1,4 +1,4 @@ -import { gql, GraphQLClient, request } from '../src/index.js' +import { gql, GraphQLClient, request } from '../src/entrypoints/main.js' import type { TypedDocumentNode } from '@graphql-typed-document-node/core' import { parse } from 'graphql' diff --git a/package.json b/package.json index bfba2d456..a359066ed 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "exports": { ".": { "import": { - "types": "./build/index.d.ts", - "default": "./build/index.js" + "types": "./build/entrypoints/main.d.ts", + "default": "./build/entrypoints/main.js" } } }, diff --git a/src/classes/GraphQLClient.ts b/src/classes/GraphQLClient.ts index 9d4aac644..00d8db2d1 100644 --- a/src/classes/GraphQLClient.ts +++ b/src/classes/GraphQLClient.ts @@ -1,18 +1,9 @@ -import { defaultJsonSerializer } from '../defaultJsonSerializer.js' import type { BatchRequestDocument, BatchRequestsOptions, BatchResult } from '../functions/batchRequests.js' import { parseBatchRequestArgs } from '../functions/batchRequests.js' import { parseRawRequestArgs } from '../functions/rawRequest.js' import { parseRequestArgs } from '../functions/request.js' -import { - ACCEPT_HEADER, - CONTENT_TYPE_GQL, - CONTENT_TYPE_HEADER, - CONTENT_TYPE_JSON, -} from '../helpers/constants.js' -import { cleanQuery } from '../helpers/graphql.js' -import { isGraphQLContentType } from '../helpers/http.js' +import { defaultJsonSerializer } from '../helpers/defaultJsonSerializer.js' import { resolveRequestDocument } from '../helpers/resolveRequestDocument.js' -import { callOrIdentity, HeadersInstanceToPlainObject, uppercase } from '../lib/prelude.js' import type { Fetch, FetchOptions, @@ -22,14 +13,17 @@ import type { RequestMiddleware, RequestOptions, VariablesAndRequestHeadersArgs, -} from '../types.js' +} from '../helpers/types.js' import { ClientError, type GraphQLClientResponse, type RawRequestOptions, type RequestConfig, type Variables, -} from '../types.js' +} from '../helpers/types.js' +import { cleanQuery, isGraphQLContentType } from '../lib/graphql.js' +import { ACCEPT_HEADER, CONTENT_TYPE_GQL, CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON } from '../lib/http.js' +import { callOrIdentity, HeadersInstanceToPlainObject, uppercase } from '../lib/prelude.js' import type { TypedDocumentNode } from '@graphql-typed-document-node/core' /** diff --git a/src/entrypoints/main.ts b/src/entrypoints/main.ts new file mode 100644 index 000000000..87ce8dffe --- /dev/null +++ b/src/entrypoints/main.ts @@ -0,0 +1,37 @@ +import { + type BatchRequestDocument, + type BatchRequestsExtendedOptions, + type BatchRequestsOptions, +} from '../functions/batchRequests.js' +import { RequestExtendedOptions } from '../functions/request.js' +import { request } from '../functions/request.js' +import type { GraphQLResponse, RequestMiddleware, ResponseMiddleware } from '../helpers/types.js' +import { + ClientError, + RawRequestOptions, + RequestDocument, + RequestOptions, + Variables, +} from '../helpers/types.js' +export { GraphQLClient } from '../classes/GraphQLClient.js' +export { batchRequests } from '../functions/batchRequests.js' +export { gql } from '../functions/gql.js' +export { rawRequest } from '../functions/rawRequest.js' +export { resolveRequestDocument } from '../helpers/resolveRequestDocument.js' +export { GraphQLWebSocketClient } from '../lib/graphql-ws.js' +export { + BatchRequestDocument, + BatchRequestsExtendedOptions, + BatchRequestsOptions, + ClientError, + GraphQLResponse, + RawRequestOptions, + request, + RequestDocument, + RequestExtendedOptions, + RequestMiddleware, + RequestOptions, + ResponseMiddleware, + Variables, +} +export default request diff --git a/src/functions/batchRequests.ts b/src/functions/batchRequests.ts index 65b3e8a6b..dfa099f8c 100644 --- a/src/functions/batchRequests.ts +++ b/src/functions/batchRequests.ts @@ -1,5 +1,5 @@ -import { GraphQLClient } from '../index.js' -import type { RequestDocument, Variables } from '../types.js' +import { GraphQLClient } from '../classes/GraphQLClient.js' +import type { RequestDocument, Variables } from '../helpers/types.js' export type BatchRequestDocument = { document: RequestDocument diff --git a/src/functions/rawRequest.ts b/src/functions/rawRequest.ts index 8c87c890e..1684475e2 100644 --- a/src/functions/rawRequest.ts +++ b/src/functions/rawRequest.ts @@ -1,10 +1,10 @@ -import { GraphQLClient } from '../index.js' +import { GraphQLClient } from '../classes/GraphQLClient.js' import type { GraphQLClientResponse, RawRequestOptions, Variables, VariablesAndRequestHeadersArgs, -} from '../types.js' +} from '../helpers/types.js' /** * Send a GraphQL Query to the GraphQL server for execution. diff --git a/src/functions/request.ts b/src/functions/request.ts index 60bc13a5d..d166f2618 100644 --- a/src/functions/request.ts +++ b/src/functions/request.ts @@ -1,6 +1,10 @@ -import type { RequestDocument, RequestOptions, Variables } from '../index.js' -import { GraphQLClient } from '../index.js' -import type { VariablesAndRequestHeadersArgs } from '../types.js' +import { GraphQLClient } from '../classes/GraphQLClient.js' +import type { + RequestDocument, + RequestOptions, + Variables, + VariablesAndRequestHeadersArgs, +} from '../helpers/types.js' import type { TypedDocumentNode } from '@graphql-typed-document-node/core' /** diff --git a/src/defaultJsonSerializer.ts b/src/helpers/defaultJsonSerializer.ts similarity index 100% rename from src/defaultJsonSerializer.ts rename to src/helpers/defaultJsonSerializer.ts diff --git a/src/helpers/graphql.ts b/src/helpers/graphql.ts deleted file mode 100644 index 9471d7af9..000000000 --- a/src/helpers/graphql.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Clean a GraphQL document to send it via a GET query - */ -export const cleanQuery = (str: string): string => str.replace(/([\s,]|#[^\n\r]+)+/g, ` `).trim() diff --git a/src/helpers/http.ts b/src/helpers/http.ts deleted file mode 100644 index 8205c0968..000000000 --- a/src/helpers/http.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { CONTENT_TYPE_GQL, CONTENT_TYPE_JSON } from './constants.js' - -export const isGraphQLContentType = (contentType: string) => { - const contentTypeLower = contentType.toLowerCase() - - return contentTypeLower.includes(CONTENT_TYPE_GQL) || contentTypeLower.includes(CONTENT_TYPE_JSON) -} diff --git a/src/helpers/resolveRequestDocument.ts b/src/helpers/resolveRequestDocument.ts index 285a700fb..e6369e6ff 100644 --- a/src/helpers/resolveRequestDocument.ts +++ b/src/helpers/resolveRequestDocument.ts @@ -1,4 +1,4 @@ -import type { RequestDocument } from '../types.js' +import type { RequestDocument } from './types.js' /** * Refactored imports from `graphql` to be more specific, this helps import only the required files (100KiB) * instead of the entire package (greater than 500KiB) where tree-shaking is not supported. diff --git a/src/types.ts b/src/helpers/types.ts similarity index 98% rename from src/types.ts rename to src/helpers/types.ts index c372c726a..99ddab804 100644 --- a/src/types.ts +++ b/src/helpers/types.ts @@ -1,4 +1,4 @@ -import type { MaybeLazy, RemoveIndex } from './lib/prelude.js' +import type { MaybeLazy, RemoveIndex } from '../lib/prelude.js' import type { TypedDocumentNode } from '@graphql-typed-document-node/core' import type { GraphQLError } from 'graphql/error/GraphQLError.js' import type { DocumentNode } from 'graphql/language/ast.js' diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index dff583afc..000000000 --- a/src/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - type BatchRequestDocument, - type BatchRequestsExtendedOptions, - type BatchRequestsOptions, -} from './functions/batchRequests.js' -import { RequestExtendedOptions } from './functions/request.js' -import { request } from './functions/request.js' -import type { GraphQLResponse, RequestMiddleware, ResponseMiddleware } from './types.js' -import { ClientError, RawRequestOptions, RequestDocument, RequestOptions, Variables } from './types.js' -export { GraphQLClient } from './classes/GraphQLClient.js' -export { batchRequests } from './functions/batchRequests.js' -export { gql } from './functions/gql.js' -export { rawRequest } from './functions/rawRequest.js' -export { resolveRequestDocument } from './helpers/resolveRequestDocument.js' -export { GraphQLWebSocketClient } from './lib/graphql-ws.js' -export { - BatchRequestDocument, - BatchRequestsExtendedOptions, - BatchRequestsOptions, - ClientError, - GraphQLResponse, - RawRequestOptions, - request, - RequestDocument, - RequestExtendedOptions, - RequestMiddleware, - RequestOptions, - ResponseMiddleware, - Variables, -} -export default request diff --git a/src/lib/graphql-ws.ts b/src/lib/graphql-ws.ts index 237d36f0c..12aeac63c 100644 --- a/src/lib/graphql-ws.ts +++ b/src/lib/graphql-ws.ts @@ -1,7 +1,7 @@ /* eslint-disable */ import { resolveRequestDocument } from '../helpers/resolveRequestDocument.js' -import type { RequestDocument, Variables } from '../types.js' -import { ClientError } from '../types.js' +import type { RequestDocument, Variables } from '../helpers/types.js' +import { ClientError } from '../helpers/types.js' import { TypedDocumentNode } from '@graphql-typed-document-node/core' // import type WebSocket from 'ws' diff --git a/src/lib/graphql.ts b/src/lib/graphql.ts new file mode 100644 index 000000000..e2bd247e9 --- /dev/null +++ b/src/lib/graphql.ts @@ -0,0 +1,12 @@ +import { CONTENT_TYPE_GQL, CONTENT_TYPE_JSON } from './http.js' + +/** + * Clean a GraphQL document to send it via a GET query + */ +export const cleanQuery = (str: string): string => str.replace(/([\s,]|#[^\n\r]+)+/g, ` `).trim() + +export const isGraphQLContentType = (contentType: string) => { + const contentTypeLower = contentType.toLowerCase() + + return contentTypeLower.includes(CONTENT_TYPE_GQL) || contentTypeLower.includes(CONTENT_TYPE_JSON) +} diff --git a/src/helpers/constants.ts b/src/lib/http.ts similarity index 100% rename from src/helpers/constants.ts rename to src/lib/http.ts diff --git a/tests/batching.test.ts b/tests/batching.test.ts index e45d0a636..5f25c7592 100644 --- a/tests/batching.test.ts +++ b/tests/batching.test.ts @@ -1,4 +1,4 @@ -import { batchRequests } from '../src/index.js' +import { batchRequests } from '../src/entrypoints/main.js' import type { MockSpecBatch } from './__helpers.js' import { setupMockServer } from './__helpers.js' import { expect, test } from 'vitest' diff --git a/tests/custom-fetch.test.ts b/tests/custom-fetch.test.ts index c84605481..36343d84a 100644 --- a/tests/custom-fetch.test.ts +++ b/tests/custom-fetch.test.ts @@ -1,4 +1,4 @@ -import { GraphQLClient } from '../src/index.js' +import { GraphQLClient } from '../src/entrypoints/main.js' import { setupMockServer } from './__helpers.js' import { expect, test } from 'vitest' diff --git a/tests/document-node.test.ts b/tests/document-node.test.ts index 314c15489..a2a98033f 100644 --- a/tests/document-node.test.ts +++ b/tests/document-node.test.ts @@ -1,4 +1,4 @@ -import { request } from '../src/index.js' +import { request } from '../src/entrypoints/main.js' import { setupMockServer } from './__helpers.js' import { gql } from 'graphql-tag' import { expect, it } from 'vitest' diff --git a/tests/endpoint.test.ts b/tests/endpoint.test.ts index e679aafae..f4830904a 100644 --- a/tests/endpoint.test.ts +++ b/tests/endpoint.test.ts @@ -1,4 +1,4 @@ -import { GraphQLClient } from '../src/index.js' +import { GraphQLClient } from '../src/entrypoints/main.js' import { setupMockServer } from './__helpers.js' import { describe, expect, test } from 'vitest' diff --git a/tests/errorPolicy.test.ts b/tests/errorPolicy.test.ts index 1702cbaee..db1f13956 100644 --- a/tests/errorPolicy.test.ts +++ b/tests/errorPolicy.test.ts @@ -1,4 +1,4 @@ -import { GraphQLClient } from '../src/index.js' +import { GraphQLClient } from '../src/entrypoints/main.js' import { setupMockServer } from './__helpers.js' import { expect, test } from 'vitest' diff --git a/tests/exports.ts b/tests/exports.ts index 1b375a0d4..f1fdc5894 100644 --- a/tests/exports.ts +++ b/tests/exports.ts @@ -1,2 +1,2 @@ /* eslint-disable */ -import { RequestMiddleware, ResponseMiddleware } from '../src/index.js' +import { RequestMiddleware, ResponseMiddleware } from '../src/entrypoints/main.js' diff --git a/tests/fetch.test.ts b/tests/fetch.test.ts index 8d1179541..48bb2b5a5 100644 --- a/tests/fetch.test.ts +++ b/tests/fetch.test.ts @@ -1,4 +1,4 @@ -import { gql, GraphQLClient } from '../src/index.js' +import { gql, GraphQLClient } from '../src/entrypoints/main.js' import { expect, test, vitest } from 'vitest' test(`custom fetch configuration is passed through`, async () => { diff --git a/tests/general.test.ts b/tests/general.test.ts index 1229309fa..5bfc88f81 100644 --- a/tests/general.test.ts +++ b/tests/general.test.ts @@ -1,4 +1,4 @@ -import { GraphQLClient, rawRequest, request } from '../src/index.js' +import { GraphQLClient, rawRequest, request } from '../src/entrypoints/main.js' import { setupMockServer } from './__helpers.js' import { gql } from 'graphql-tag' import type { Mock } from 'vitest' diff --git a/tests/gql.test.ts b/tests/gql.test.ts index 193a57be0..a40f220c4 100644 --- a/tests/gql.test.ts +++ b/tests/gql.test.ts @@ -1,4 +1,4 @@ -import { request } from '../src/index.js' +import { request } from '../src/entrypoints/main.js' import { setupMockServer } from './__helpers.js' import { gql } from 'graphql-tag' import { describe, expect, it } from 'vitest' diff --git a/tests/headers.test.ts b/tests/headers.test.ts index 473af88a3..006a279a7 100644 --- a/tests/headers.test.ts +++ b/tests/headers.test.ts @@ -1,4 +1,4 @@ -import { GraphQLClient, request } from '../src/index.js' +import { GraphQLClient, request } from '../src/entrypoints/main.js' import { setupMockServer } from './__helpers.js' import { describe, expect, test } from 'vitest' diff --git a/tests/json-serializer.test.ts b/tests/json-serializer.test.ts index cde39ea2c..569aaecad 100644 --- a/tests/json-serializer.test.ts +++ b/tests/json-serializer.test.ts @@ -1,5 +1,5 @@ -import { GraphQLClient } from '../src/index.js' -import type { Fetch, Variables } from '../src/types.js' +import { GraphQLClient } from '../src/entrypoints/main.js' +import type { Fetch, Variables } from '../src/helpers/types.js' import { setupMockServer } from './__helpers.js' import { beforeEach, describe, expect, test, vitest } from 'vitest' diff --git a/tests/signal.test.ts b/tests/signal.test.ts index 739f4108f..c1673e5b3 100644 --- a/tests/signal.test.ts +++ b/tests/signal.test.ts @@ -1,4 +1,4 @@ -import { batchRequests, GraphQLClient, rawRequest, request } from '../src/index.js' +import { batchRequests, GraphQLClient, rawRequest, request } from '../src/entrypoints/main.js' import { setupMockServer, sleep } from './__helpers.js' import { expect, it } from 'vitest' diff --git a/tests/typed-document-node.test.ts b/tests/typed-document-node.test.ts index 242df04d8..9b09d69cc 100644 --- a/tests/typed-document-node.test.ts +++ b/tests/typed-document-node.test.ts @@ -1,4 +1,4 @@ -import request, { gql } from '../src/index.js' +import request, { gql } from '../src/entrypoints/main.js' import { setupMockServer } from './__helpers.js' import type { TypedDocumentNode } from '@graphql-typed-document-node/core' import { parse } from 'graphql' From e8ff30c38499b1551b252f8103579c85813c7a33 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 24 Jan 2024 23:08:45 -0500 Subject: [PATCH 4/4] format --- src/helpers/types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 99ddab804..331002b1b 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -104,8 +104,8 @@ export type RawRequestOptions = { } & (V extends Record ? { variables?: V } : keyof RemoveIndex extends never - ? { variables?: V } - : { variables: V }) + ? { variables?: V } + : { variables: V }) export type RequestOptions = { document: RequestDocument | TypedDocumentNode @@ -114,8 +114,8 @@ export type RequestOptions = { } & (V extends Record ? { variables?: V } : keyof RemoveIndex extends never - ? { variables?: V } - : { variables: V }) + ? { variables?: V } + : { variables: V }) export type ResponseMiddleware = (response: GraphQLClientResponse | ClientError | Error) => void