Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 6.2 kB view raw
1import { 2 stringifyDocument, 3 getOperationName, 4 stringifyVariables, 5 extractFiles, 6} from '../utils'; 7 8import type { AnyVariables, GraphQLRequest, Operation } from '../types'; 9 10/** Abstract definition of the JSON data sent during GraphQL HTTP POST requests. */ 11export interface FetchBody { 12 query?: string; 13 documentId?: string; 14 operationName: string | undefined; 15 variables: undefined | Record<string, any>; 16 extensions: undefined | Record<string, any>; 17} 18 19/** Creates a GraphQL over HTTP compliant JSON request body. 20 * @param request - An object containing a `query` document and `variables`. 21 * @returns A {@link FetchBody} 22 * @see {@link https://github.com/graphql/graphql-over-http} for the GraphQL over HTTP spec. 23 */ 24export function makeFetchBody< 25 Data = any, 26 Variables extends AnyVariables = AnyVariables, 27>(request: Omit<GraphQLRequest<Data, Variables>, 'key'>): FetchBody { 28 const body: FetchBody = { 29 query: undefined, 30 documentId: undefined, 31 operationName: getOperationName(request.query), 32 variables: request.variables || undefined, 33 extensions: request.extensions, 34 }; 35 36 if ( 37 'documentId' in request.query && 38 request.query.documentId && 39 // NOTE: We have to check that the document will definitely be sent 40 // as a persisted document to avoid breaking changes 41 (!request.query.definitions || !request.query.definitions.length) 42 ) { 43 body.documentId = request.query.documentId; 44 } else if ( 45 !request.extensions || 46 !request.extensions.persistedQuery || 47 !!request.extensions.persistedQuery.miss 48 ) { 49 body.query = stringifyDocument(request.query); 50 } 51 52 return body; 53} 54 55/** Creates a URL that will be called for a GraphQL HTTP request. 56 * 57 * @param operation - An {@link Operation} for which to make the request. 58 * @param body - A {@link FetchBody} which may be replaced with a URL. 59 * 60 * @remarks 61 * Creates the URL that’ll be called as part of a GraphQL HTTP request. 62 * Built-in fetch exchanges support sending GET requests, even for 63 * non-persisted full requests, which this function supports by being 64 * able to serialize GraphQL requests into the URL. 65 */ 66export const makeFetchURL = ( 67 operation: Operation, 68 body?: FetchBody 69): string => { 70 const useGETMethod = 71 operation.kind === 'query' && operation.context.preferGetMethod; 72 if (!useGETMethod || !body) return operation.context.url; 73 74 const urlParts = splitOutSearchParams(operation.context.url); 75 for (const key in body) { 76 const value = body[key]; 77 if (value) { 78 urlParts[1].set( 79 key, 80 typeof value === 'object' ? stringifyVariables(value) : value 81 ); 82 } 83 } 84 const finalUrl = urlParts.join('?'); 85 if (finalUrl.length > 2047 && useGETMethod !== 'force') { 86 operation.context.preferGetMethod = false; 87 return operation.context.url; 88 } 89 90 return finalUrl; 91}; 92 93const splitOutSearchParams = ( 94 url: string 95): readonly [string, URLSearchParams] => { 96 const start = url.indexOf('?'); 97 return start > -1 98 ? [url.slice(0, start), new URLSearchParams(url.slice(start + 1))] 99 : [url, new URLSearchParams()]; 100}; 101 102/** Serializes a {@link FetchBody} into a {@link RequestInit.body} format. */ 103const serializeBody = ( 104 operation: Operation, 105 body?: FetchBody 106): FormData | string | undefined => { 107 const omitBody = 108 operation.kind === 'query' && !!operation.context.preferGetMethod; 109 if (body && !omitBody) { 110 const json = stringifyVariables(body); 111 const files = extractFiles(body.variables); 112 if (files.size) { 113 const form = new FormData(); 114 form.append('operations', json); 115 form.append( 116 'map', 117 stringifyVariables({ 118 ...[...files.keys()].map(value => [value]), 119 }) 120 ); 121 let index = 0; 122 for (const file of files.values()) form.append(`${index++}`, file); 123 return form; 124 } 125 return json; 126 } 127}; 128 129const isHeaders = (headers: HeadersInit): headers is Headers => 130 'has' in headers && !Object.keys(headers).length; 131 132/** Creates a `RequestInit` object for a given `Operation`. 133 * 134 * @param operation - An {@link Operation} for which to make the request. 135 * @param body - A {@link FetchBody} which is added to the options, if the request isn’t a GET request. 136 * 137 * @remarks 138 * Creates the fetch options {@link RequestInit} object that’ll be passed to the Fetch API 139 * as part of a GraphQL over HTTP request. It automatically sets a default `Content-Type` 140 * header. 141 * 142 * @see {@link https://github.com/graphql/graphql-over-http} for the GraphQL over HTTP spec. 143 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API} for the Fetch API spec. 144 */ 145export const makeFetchOptions = ( 146 operation: Operation, 147 body?: FetchBody 148): RequestInit => { 149 const headers: HeadersInit = { 150 accept: 151 operation.kind === 'subscription' 152 ? 'text/event-stream, multipart/mixed' 153 : 'application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed', 154 }; 155 const extraOptions = 156 (typeof operation.context.fetchOptions === 'function' 157 ? operation.context.fetchOptions() 158 : operation.context.fetchOptions) || {}; 159 if (extraOptions.headers) { 160 if (isHeaders(extraOptions.headers)) { 161 extraOptions.headers.forEach((value, key) => { 162 headers[key] = value; 163 }); 164 } else if (Array.isArray(extraOptions.headers)) { 165 (extraOptions.headers as Array<[string, string]>).forEach( 166 (value, key) => { 167 if (Array.isArray(value)) { 168 if (headers[value[0]]) { 169 headers[value[0]] = `${headers[value[0]]},${value[1]}`; 170 } else { 171 headers[value[0]] = value[1]; 172 } 173 } else { 174 headers[key] = value; 175 } 176 } 177 ); 178 } else { 179 for (const key in extraOptions.headers) { 180 headers[key.toLowerCase()] = extraOptions.headers[key]; 181 } 182 } 183 } 184 185 const serializedBody = serializeBody(operation, body); 186 if (typeof serializedBody === 'string' && !headers['content-type']) 187 headers['content-type'] = 'application/json'; 188 return { 189 ...extraOptions, 190 method: serializedBody ? 'POST' : 'GET', 191 body: serializedBody, 192 headers, 193 }; 194};