UNPKG

altair-graphql-core

Version:

Several of the core logic for altair graphql client

202 lines 8.5 kB
import { Observable, from } from 'rxjs'; import { setByDotNotation } from '../../utils/dot-notation'; import { print } from 'graphql'; import { getOperations } from '../../utils/graphql'; import { meros } from 'meros/browser'; import compress from 'graphql-query-compress'; export class HttpRequestHandler { handle(request) { // Make the request and return an observable return new Observable((observer) => { const aborter = new AbortController(); const data = this.getData(request); const params = this.isGETRequest(request.method) ? this.getParamsFromData(data) : undefined; const requestStartTime = Date.now(); // Make the request fetch(this.getUrl(request.url, params), { method: request.method, headers: this.getHeaders(request), body: this.getBody(request, data), credentials: request.withCredentials ? 'include' : 'same-origin', signal: aborter.signal, }) .then(async (response) => { return { response, merosResponse: await meros(response) }; }) .then(async (result) => { const requestEndTime = Date.now(); const { response, merosResponse } = result; if (merosResponse instanceof Response) { // meros couldn't handle the response, so we'll handle it ourselves if (!merosResponse.ok || !merosResponse.body) { // don't handle streaming const buffer = await merosResponse.arrayBuffer(); return this.emitChunk(merosResponse, new Uint8Array(buffer), true, observer, requestStartTime, requestEndTime); } const reader = merosResponse.body.getReader(); while (true) { const { done, value } = await reader.read(); this.emitChunk(merosResponse, value, done, observer, requestStartTime, requestEndTime); if (done) { return true; } } } // Handle the response from meros return from(merosResponse).subscribe({ next: (chunk) => { this.emitChunk(response, chunk.json ? JSON.stringify(chunk.body) : chunk.body, false, observer, requestStartTime, requestEndTime); }, error: (error) => { observer.error(error); }, complete: () => { this.emitChunk(response, undefined, true, observer, requestStartTime, requestEndTime); }, }); }) .catch((error) => { if (error?.name === 'AbortError') { // Request was aborted observer.complete(); return; } // Send the error to the observer observer.error(error); }); // Return a function that will be called when the observable is unsubscribed return () => { // Cancel the ongoing request if unsubscribed aborter.abort(); }; }); } destroy() { // throw new Error('Method not implemented.'); } emitChunk(response, chunk, done, observer, requestStartTime, requestEndTime) { if (chunk) { const value = typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk); const res = { ok: response.ok, data: value, headers: response.headers, status: response.status, statusText: response.statusText, url: response.url, requestStartTimestamp: requestStartTime, requestEndTimestamp: requestEndTime, // this is redundant data responseTimeMs: requestEndTime - requestStartTime, }; // Send the data to the observer observer.next(res); } if (done) { observer.complete(); } return true; } isGETRequest(method) { return method.toLowerCase() === 'get'; } getParamsFromData(data) { return Object.entries(data).reduce((params, [key, value]) => { if (value) { const parsedValue = typeof value === 'object' ? JSON.stringify(value) : value.toString(); params.set(key, parsedValue); } return params; }, new URLSearchParams()); } getHeaders(request) { const headers = new Headers(); const jsonHeaders = { 'Content-Type': 'application/json', Accept: 'application/json', }; // with files, needs to be form data // GET request, needs to be query params const addJsonHeaders = !this.isGETRequest(request.method) && !request.files?.length; if (addJsonHeaders) { Object.entries(jsonHeaders).forEach(([key, value]) => { headers.set(key, value); }); } // Add the rest of the headers request.headers?.forEach((header) => { headers.set(header.key, header.value); }); return headers; } getUrl(url, params) { const u = new URL(url); if (params) { u.search = params.toString(); } return u.href; } getData(request) { const data = { query: request.query, variables: request.variables ?? {}, extensions: request.extensions, operationName: null, }; if (request.selectedOperation) { data.operationName = request.selectedOperation; } return data; } getBody(request, data) { // with files, needs to be form data // with neither, needs to be JSON // GET request, needs to be query params, so no body if (this.isGETRequest(request.method)) { return; } // Handle file uploads if (request.files?.length) { // https://github.com/jaydenseric/graphql-multipart-request-spec#multipart-form-field-structure // 1. operations: JSON string of the data, with the files in variables replaced by null // 2. map: JSON string of the file map, where the key is the index of the file in the files array, and the value is the path to the file in the operations JSON // 3. files: the files themselves, with the key being the index of the file in the files array const fileMap = {}; request.files.forEach((file, i) => { // this mutation should be done before setting the stringified data setByDotNotation(data.variables, file.name, null); fileMap[i] = [`variables.${file.name}`]; }); const formData = new FormData(); formData.append('operations', JSON.stringify(data)); formData.append('map', JSON.stringify(fileMap)); request.files.forEach((file, i) => { formData.append(`${i}`, file.data ?? ''); }); return formData; } // Handle batched requests if (request.batchedRequest) { const operations = getOperations(data.query); if (operations.length > 1) { const operationQueries = operations.map((operation) => { const operationName = operation.name?.value; const operationQuery = compress(print(operation)); const operationVariables = data.variables; const operationExtensions = data.extensions; return { operationName, query: operationQuery, variables: operationVariables, extensions: operationExtensions, }; }); return JSON.stringify(operationQueries); } } return JSON.stringify(data); } } //# sourceMappingURL=http.js.map