import fetch from 'cross-fetch'; import { print } from 'graphql'; import FormData from 'form-data'; // import { FormData } from '../../node_modules/formdata-node'; /** * This executor can execute both normal json reqeuests and multipart for file upload. * It works with normal cases of uploads but hasnt beedn tested if the file field is nested * in a deep object. It hasnt been tested with mutiple mutations either. */ const getFiles = async (variables: any) => { const files = []; const parseFiles = async (variables: any) => { if (Array.isArray(variables)) { for (let i = 0; i < variables.length; i++) { if (variables[i].promise) { const file = await variables[i].promise; files.push({ file, key: i }); variables[i] = null; } else { parseFiles(variables[i]); } } } else if (typeof variables === 'object') { for (const key of Object.keys(variables)) { if (variables[key].promise) { const file = await variables[key].promise; files.push({ file, key }); variables[key] = null; } else { parseFiles(variables[key]); } } } }; await parseFiles(variables); return files; }; // Helper function for fetch with timeout const fetchWithTimeout = async (url: string, options: any, timeoutMs = 10000) => { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { ...options, signal: controller.signal }); return response; } catch (error: any) { if (error.name === 'AbortError') { console.log('Request timed out'); throw new Error(`Request to ${url} timed out after ${timeoutMs}ms`); } throw error; } finally { clearTimeout(timeout); } }; // Builds a remote schema executor function, // customize any way that you need (auth, headers, etc). // Expects to receive an object with "document" and "variable" params, // and asynchronously returns a JSON response from the remote. export default async function makeRemoteExecutor(url: string) { return async ({ document, variables, _, context}) => { const json = !context || !context.req || 'application/json' === context?.req?.headers['content-type']; const query = typeof document === 'string' ? document : print(document); if (json) { const fetchResult = await fetchWithTimeout(url, { method: 'POST', headers: { authorization: context?.req.headers.authorization, ...context?.req.headers, 'Content-Type': 'application/json', }, body: JSON.stringify({ query, variables }), }, 10000); return fetchResult.json(); } const files = await getFiles(variables); // We get the boundry of the original request just to keep as much intact as possible const boundry = context.req.headers['content-type'].split('boundary=')[1]; const operations = JSON.stringify({ query, variables }); const form = new FormData(); form.setBoundary(boundry); form.append('operations', operations); form.append( 'map', JSON.stringify( files.reduce((result, file, index) => { result[(index + 1).toString()] = [`variables.${file.key}`]; return result; }, {}) ) ); let i = 0; for await (const file of files) { form.append((i + 1).toString(), file.file.createReadStream(), file.file.filename); i++; } const fetchResult = await fetch(url, { method: 'POST', headers: { ...context?.req.headers, authorization: context?.req.headers.authorization, }, body: form as any, }); return fetchResult.json(); }; }