UNPKG

apollo-federation-upload-minimal

Version:

This library makes it easier to support file uploads to your federated micro-services. It uses the [Apollo](https://www.apollographql.com/docs/apollo-server/data/file-uploads/) server's solution. It works by simply redirecting the file uploaded stream to

169 lines (168 loc) 7.84 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const gateway_1 = require("@apollo/gateway"); const graphql_upload_minimal_1 = require("graphql-upload-minimal"); const apollo_server_env_1 = require("apollo-server-env"); const predicates_1 = require("@apollo/gateway/dist/utilities/predicates"); const lodash_clonedeep_1 = __importDefault(require("lodash.clonedeep")); const lodash_set_1 = __importDefault(require("lodash.set")); const FormData_1 = __importDefault(require("./FormData")); const addChunkedDataToForm = (form, resolvedFiles) => { resolvedFiles.forEach(({ createReadStream, filename, mimetype: contentType }, i) => { form.append(i.toString(), createReadStream(), { contentType, filename, /* Set knownLength to NaN so node-fetch does not set the Content-Length header and properly set the enconding to chunked. https://github.com/form-data/form-data/pull/397#issuecomment-471976669 */ knownLength: Number.NaN, }); }); return Promise.resolve(); }; const addDataToForm = (form, resolvedFiles) => Promise.all(resolvedFiles.map(({ createReadStream, filename, mimetype: contentType }, i) => __awaiter(void 0, void 0, void 0, function* () { const fileData = yield new Promise((resolve, reject) => { const stream = createReadStream(); const buffers = []; stream.on('error', reject); stream.on('data', (data) => { buffers.push(data); }); stream.on('end', () => { resolve(Buffer.concat(buffers)); }); }); form.append(i.toString(), fileData, { contentType, filename, knownLength: fileData.length, }); }))); class FileUploadDataSource extends gateway_1.RemoteGraphQLDataSource { constructor(config) { var _a; super(config); const useChunkedTransfer = (_a = config === null || config === void 0 ? void 0 : config.useChunkedTransfer) !== null && _a !== void 0 ? _a : true; this.addDataHandler = useChunkedTransfer ? addChunkedDataToForm : addDataToForm; } static extractFileVariables(rootVariables) { const extract = (variables, prefix) => { return Object.entries(variables || {}).reduce((acc, [name, value]) => { const p = prefix ? `${prefix}.` : ''; const key = `${p}${name}`; if (value instanceof Promise || value instanceof graphql_upload_minimal_1.Upload) { acc.push([ key, value instanceof graphql_upload_minimal_1.Upload ? value.promise : value, ]); return acc; } if (Array.isArray(value)) { const [first] = value; if (first instanceof Promise || first instanceof graphql_upload_minimal_1.Upload) { return acc.concat(value.map((v, idx) => [ `${key}.${idx}`, v instanceof graphql_upload_minimal_1.Upload ? v.promise : v, ])); } if ((0, predicates_1.isObject)(first)) { return acc.concat(...value.map((v, idx) => extract(v, `${key}.${idx}`))); } return acc; } if ((0, predicates_1.isObject)(value)) { return acc.concat(extract(value, key)); } return acc; }, []); }; return extract(rootVariables); } process(args) { const _super = Object.create(null, { process: { get: () => super.process } }); return __awaiter(this, void 0, void 0, function* () { const fileVariables = FileUploadDataSource.extractFileVariables(args.request.variables); if (fileVariables.length > 0) { return this.processFiles(args, fileVariables); } return _super.process.call(this, args); }); } processFiles(args, fileVariables) { return __awaiter(this, void 0, void 0, function* () { const { context, request } = args; const form = new FormData_1.default(); const variables = (0, lodash_clonedeep_1.default)(request.variables || {}); fileVariables.forEach(([variableName]) => { (0, lodash_set_1.default)(variables, variableName, null); }); const operations = JSON.stringify({ query: request.query, variables, }); form.append('operations', operations); const fileMap = {}; const resolvedFiles = yield Promise.all(fileVariables.map(([variableName, file], i) => __awaiter(this, void 0, void 0, function* () { const fileUpload = yield file; fileMap[i] = [`variables.${variableName}`]; return fileUpload; }))); // This must come before the file contents append bellow form.append('map', JSON.stringify(fileMap)); yield this.addDataHandler(form, resolvedFiles); const headers = (request.http && request.http.headers) || new apollo_server_env_1.Headers(); Object.entries(form.getHeaders() || {}).forEach(([k, value]) => { headers.set(k, value); }); request.http = { headers, method: 'POST', url: this.url, }; if (this.willSendRequest) { yield this.willSendRequest(args); } const options = Object.assign(Object.assign({}, request.http), { // Apollo types are not up-to-date, make TS happy body: form, headers: Object.fromEntries(request.http.headers) }); const httpRequest = new apollo_server_env_1.Request(request.http.url, options); let httpResponse; try { httpResponse = yield this.fetcher(request.http.url, options); const body = yield this.parseBody(httpResponse); if (!(0, predicates_1.isObject)(body)) { throw new Error(`Expected JSON response body, but received: ${body}`); } const response = Object.assign(Object.assign({}, body), { http: httpResponse }); if (typeof this.didReceiveResponse === 'function') { return this.didReceiveResponse({ context, request, response }); } return response; } catch (error) { this.didEncounterError(error, httpRequest, httpResponse); throw error; } }); } } exports.default = FileUploadDataSource;