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
JavaScript
;
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;