Skip to content

Instantly share code, notes, and snippets.

@newerton
Forked from bl42/FileUploadDataSource.js
Created October 12, 2024 23:11
Show Gist options
  • Save newerton/9655bb66b99bd98c7f6d103bb57ad5db to your computer and use it in GitHub Desktop.
Save newerton/9655bb66b99bd98c7f6d103bb57ad5db to your computer and use it in GitHub Desktop.

Revisions

  1. @bl42 bl42 revised this gist Sep 5, 2019. 1 changed file with 4 additions and 2 deletions.
    6 changes: 4 additions & 2 deletions FileUploadDataSource.js
    Original file line number Diff line number Diff line change
    @@ -21,7 +21,7 @@ export default class FileUploadDataSource extends RemoteGraphQLDataSource {
    // https://github.com/jaydenseric/graphql-multipart-request-spec
    const form = new FormData();

    //cannot mutate the orginal request
    // cannot mutate the request object
    const variables = _.cloneDeep(request.variables);
    for (const [variableName] of fileVariables) {
    _.set(variables, variableName, null);
    @@ -31,7 +31,7 @@ export default class FileUploadDataSource extends RemoteGraphQLDataSource {
    query: request.query,
    variables,
    });

    form.append('operations', operations);

    const resolvedFiles = await Promise.all(
    @@ -55,6 +55,7 @@ export default class FileUploadDataSource extends RemoteGraphQLDataSource {
    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 });
    })
    @@ -116,6 +117,7 @@ export default class FileUploadDataSource extends RemoteGraphQLDataSource {
    if (value instanceof Promise) {
    return files.push([key, value]);
    }
    // TODO: support arrays of files
    if (value instanceof Object) {
    return _extract(value, key);
    }
  2. @bl42 bl42 revised this gist Sep 5, 2019. 1 changed file with 4 additions and 2 deletions.
    6 changes: 4 additions & 2 deletions FileUploadDataSource.js
    Original file line number Diff line number Diff line change
    @@ -5,7 +5,7 @@ import FormData from 'form-data';
    import _ from 'lodash';

    export default class FileUploadDataSource extends RemoteGraphQLDataSource {
    process(args) {
    async process(args) {
    const { request, context } = args;

    const fileVariables = this.extract(request.variables);
    @@ -21,7 +21,8 @@ export default class FileUploadDataSource extends RemoteGraphQLDataSource {
    // https://github.com/jaydenseric/graphql-multipart-request-spec
    const form = new FormData();

    const variables = request.variables;
    //cannot mutate the orginal request
    const variables = _.cloneDeep(request.variables);
    for (const [variableName] of fileVariables) {
    _.set(variables, variableName, null);
    }
    @@ -30,6 +31,7 @@ export default class FileUploadDataSource extends RemoteGraphQLDataSource {
    query: request.query,
    variables,
    });

    form.append('operations', operations);

    const resolvedFiles = await Promise.all(
  3. @bl42 bl42 created this gist Sep 4, 2019.
    140 changes: 140 additions & 0 deletions FileUploadDataSource.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,140 @@
    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 {
    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();

    const variables = 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();
    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]);
    }
    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);
    });
    });
    };
    }