Skip to content

Instantly share code, notes, and snippets.

@bl42
Last active May 7, 2020 21:05
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bl42/881d85ee0dd92bbe68e93dcd0c99cfdb to your computer and use it in GitHub Desktop.
Save bl42/881d85ee0dd92bbe68e93dcd0c99cfdb to your computer and use it in GitHub Desktop.
import { RemoteGraphQLDataSource } from '@apollo/gateway';
import { fetch, Request, Headers } from 'apollo-server-env';
import { isObject } from '@apollo/gateway/dist/utilities/predicates';
import FormData from 'form-data';
import _ from 'lodash';
export default class FileUploadDataSource extends RemoteGraphQLDataSource {
async process(args) {
const { request, context } = args;
const fileVariables = this.extract(request.variables);
if (fileVariables.length > 0) {
return this.processFileUpload(args, fileVariables);
} else {
return super.process(args);
}
}
async processFileUpload({ request, context }, fileVariables) {
// GraphQL multipart request spec:
// https://github.com/jaydenseric/graphql-multipart-request-spec
const form = new FormData();
// cannot mutate the request object
const variables = _.cloneDeep(request.variables);
for (const [variableName] of fileVariables) {
_.set(variables, variableName, null);
}
const operations = JSON.stringify({
query: request.query,
variables,
});
form.append('operations', operations);
const resolvedFiles = await Promise.all(
fileVariables.map(async ([variableName, file]) => {
const contents = await file;
return [variableName, contents];
})
);
// e.g. { "0": ["variables.file"] }
const fileMap = resolvedFiles.reduce(
(map, [variableName], i) => ({
...map,
[i]: [`variables.${variableName}`],
}),
{}
);
form.append('map', JSON.stringify(fileMap));
await Promise.all(
resolvedFiles.map(async ([, contents], i) => {
const { filename, mimetype, createReadStream } = contents;
const readStream = await createReadStream();
// TODO: Buffers performance issues? may be better solution.
const buffer = await this.onReadStream(readStream);
form.append(i, buffer, { filename, contentType: mimetype });
})
);
// Respect incoming http headers (eg, apollo-federation-include-trace).
const headers = (request.http && request.http.headers) || new Headers();
form.getLength(function(err, length) {
headers.set('Content-Length', length);
});
Object.entries(form.getHeaders() || {}).forEach(([k, value]) => {
headers.set(k, value);
});
request.http = {
method: 'POST',
url: this.url,
headers,
};
if (this.willSendRequest) {
await this.willSendRequest({ request, context });
}
const options = {
...request.http,
body: form,
};
const httpRequest = new Request(request.http.url, options);
try {
const httpResponse = await fetch(httpRequest);
const body = await this.didReceiveResponse(httpResponse, httpRequest);
if (!isObject(body)) {
throw new Error(`Expected JSON response body, but received: ${body}`);
}
const response = {
...body,
http: httpResponse,
};
return response;
} catch (error) {
this.didEncounterError(error, httpRequest);
throw error;
}
}
extract(obj) {
const files = [];
const _extract = (obj, keys) =>
Object.entries(obj || {}).forEach(([k, value]) => {
const key = keys ? `${keys}.${k}` : k;
if (value instanceof Promise) {
return files.push([key, value]);
}
// TODO: support arrays of files
if (value instanceof Object) {
return _extract(value, key);
}
});
_extract(obj);
return files;
}
onReadStream = readStream => {
return new Promise((resolve, reject) => {
var buffers = [];
readStream.on('data', function(data) {
buffers.push(data);
});
readStream.on('end', function() {
var actualContents = Buffer.concat(buffers);
resolve(actualContents);
});
readStream.on('error', function(err) {
reject(err);
});
});
};
}
@ariusxi
Copy link

ariusxi commented May 6, 2020

I'm trying to implement this code, but when i call the function didReceiveResponse he's undefined can you help me?

this is my code:

import {ApolloServer} from 'apollo-server'
import {ApolloGateway} from '@apollo/gateway'
import FileUploadDataSource from './middlewares/FileUploadDataSource'

const runServer = async () => {
    const server = new ApolloServer({
        gateway: new ApolloGateway({
            buildService: ({url}) => new FileUploadDataSource({url}),
            serviceList: [{
                name: 'crm',
                url: 'http://localhost:7000/api/v1',
            }],
        }),
        subscriptions: false,
        context: ({req}) => {
            const token = req.headers.authorization || ''
            return {token}
        }
    })

    const {url} = await server.listen()

    console.log(`Server ready at ${url}`)
}

runServer().catch(error => {
    console.error(`Failed to start server: `, error)
    process.exit(1)
})

@kojuka
Copy link

kojuka commented May 7, 2020

@ariusxi @bl42

me too. any help here would be great.

@kojuka
Copy link

kojuka commented May 7, 2020

@ariusxi I got mine working by swapping out these lines:

from:

const body = await this.didReceiveResponse(httpResponse, httpRequest);

to:

const body = await this.parseBody(httpResponse);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment