16

A file upload seems like a mutation. It's often accompanied by other data. But it's a big binary blob, so I'm not sure how GraphQL can deal with it. How would you integrate file uploads into an app built with Relay?

YasserKaddour
  • 812
  • 11
  • 22
Nick Retallack
  • 17,432
  • 17
  • 85
  • 110

5 Answers5

10

First you need to write the Relay update in your frontend component. Like this:

onDrop: function(files) {
  files.forEach((file)=> {
    Relay.Store.commitUpdate(
      new AddImageMutation({
        file,
        images: this.props.User,
      }),
      {onSuccess, onFailure}
    );
  });
},

And then follow by implementing the mutation on the frontend:

class AddImageMutation extends Relay.Mutation {
   static fragments = {
     images: () => Relay.QL`
       fragment on User {
         id,
       }`,
     };

   getMutation() {
     return Relay.QL`mutation{ introduceImage }`;
   }

   getFiles() {
     return {
       file: this.props.file,
     };
   }

   getVariables() {
     return {
       imageName: this.props.file.name,
     };
   }

   getFatQuery() {
     return Relay.QL`
       fragment on IntroduceImagePayload {
         User {
           images(first: 30) {
             edges {
               node {
                 id,
               }
             }
           }
         },
         newImageEdge,
       }
     `;
   }

   getConfigs() {
     return [{
       type: 'RANGE_ADD',
       parentName: 'User',
       parentID: this.props.images.id,
       connectionName: 'images',
       edgeName: 'newImageEdge',
       rangeBehaviors: {
         '': 'prepend',
       },
     }];
   }
 }

And last, implement the handler on the server/schema.

const imageMutation = Relay.mutationWithClientMutationId({
  name: 'IntroduceImage',
  inputFields: {
    imageName: {
      type: new GraphQL.GraphQLNonNull(GraphQL.GraphQLString),
    },
  },
  outputFields: {
    newImageEdge: {
      type: ImageEdge,
      resolve: (payload, args, options) => {
        const file = options.rootValue.request.file;
        //write the image to you disk
        return uploadFile(file.buffer, filePath, filename)
        .then(() => {
          /* Find the offset for new edge*/
          return Promise.all(
            [(new myImages()).getAll(),
              (new myImages()).getById(payload.insertId)])
          .spread((allImages, newImage) => {
            const newImageStr = JSON.stringify(newImage);
            /* If edge is in list return index */
            const offset = allImages.reduce((pre, ele, idx) => {
              if (JSON.stringify(ele) === newImageStr) {
                return idx;
              }
              return pre;
            }, -1);

            return {
              cursor: offset !== -1 ? Relay.offsetToCursor(offset) : null,
              node: newImage,
            };
          });
        });
      },
    },
    User: {
      type: UserType,
      resolve: () => (new myImages()).getAll(),
    },
  },
  mutateAndGetPayload: (input) => {
    //break the names to array.
    let imageName = input.imageName.substring(0, input.imageName.lastIndexOf('.'));
    const mimeType = input.imageName.substring(input.imageName.lastIndexOf('.'));
    //wirte the image to database
    return (new myImages())
    .add(imageName)
    .then(id => {
    //prepare to wirte disk
      return {
        insertId: id,
        imgNmae: imageName,
      };
    });
  },
});

All the code above you can find them in my repo https://github.com/bfwg/relay-gallery There is also a live demo https://fanjin.io

Fan Jin
  • 2,115
  • 13
  • 25
  • Please include the relevant code in your answer. Stack Overflow discourages relying on outside links for the "core" of an answer, in order to prevent link rot. It's perfectly OK to only quote what's relevant, and then link to the complete repository. – mech Feb 23 '16 at 19:01
5

I found an explanation in the docs. You can subclass Relay.Mutation and implement the getFiles function.

Also, express-graphql provides an example in its test cases of how to handle this on the server side.

mpen
  • 237,624
  • 230
  • 766
  • 1,119
Nick Retallack
  • 17,432
  • 17
  • 85
  • 110
4

I am merely sharing the findings of Marc-Andre Giroux from his blog, which is Rails-specific, so I will try to make it more generic, and providing the details of the answer provided by @Nick.

There are 2 parts:

  • Client-side Javascript code
  • Server-side server-specific code

Client-side Javascript Code

The client-side code further consists of 2 parts:

  1. The mutation to upload file, which extends Relay.Mutation (UploadFileMutation)

    // The actual mutation
    class UploadFileMutation extends Relay.Mutation {
      getFiles() {
        return {
          file: this.props.file,
        };
      }
    
      // ... Rest of your mutation
    }
    
  2. The component that contains the React component (FileUploader) to render the UI for selecting the file, and calls the mutation to do the upload

    // A react component to upload a file
    class FileUploader extends React.Component {
      onSubmit() {
        const name = this.refs.name.value;
        const file = this.refs.fileInput.files.item(0);
        Relay.Store.update(
          new UploadFileMutation({
            name: name,
            file: file,
          })
        );
      }
    
      // ... Rest of React component, e.g., render()
    }
    

Server-side Server-Specific Code

The server-side code also consists of 2 parts:

  1. The part to handle retrieving the uploaded file in MIME multipart format and pass it to the Mutation defined in the GraphQL schema. We provide NodeJS and Rails examples, which should help you derive solutions for other servers.

For NodeJS Express server (extracted from express-graqphl test cases as pointed out by @Nick):

    import multer from 'multer';

    var app = express();
    var graphqlHTTP = require('express-graphql');

    // Multer provides multipart form data parsing.
    var storage = multer.memoryStorage();

    app.use(urlString(), multer({ storage }).single('file'));

    // Providing the request, which contains the file MIME
    // multipart as `rootValue` to enable it to
    // be accessible from within Schema resolve functions.
    app.use(urlString(), graphqlHTTP(req => {
      return {
        schema: YourMutationSchema,
        rootValue: { request: req }
      };
    }));

Similarly, for a non-JS server, e.g., RubyOnRails:

    def create
      query_string = params[:query]
      query_variables = ensure_hash(params[:variables]) || {}

      query = GraphQL::Query.new(
        YourSchema,
        query_string,
        variables: query_variables,
        # Shove the file MIME multipart into context to make it
        # accessible by GraphQL Schema Mutation resolve methods
        context: { file: request.params[:file] }
     )
  1. The Mutation can retrieve the file MIME multipart passed to it

For Javascript GraphQL Schema:

    var YourMutationSchema = new GraphQLSchema({
      query: new GraphQLObjectType({
        // ... QueryType Schema
      }),
      mutation: new GraphQLObjectType({
        name: 'MutationRoot',
        fields: {
          uploadFile: {
            type: UploadedFileType,
            resolve(rootValue) {
              // Access file MIME multipart using
              const _file = rootValue.request.file;

              // ... Do something with file
            }
          }
        }
      })
    });

For Rails GraphQL Schema:

    AddFileMutation = GraphQL::Relay::Mutation.define do
      name "AddFile"
      input_field :name, !types.String

      # ... Add your standard mutation schema stuff here

      resolve -> (args, ctx) {
        # Retrieve the file MIME multipart
        file = ctx[:file]
        raise StandardError.new("Expected a file") unless file

        # ... Do something with file
      }
    end
nethsix
  • 702
  • 6
  • 15
1

To add to the other answers, with Relay Modern, there was a small change on how you should send the files from the client. Instead of having a getFiles in your mutation and passing the files to the constructor, you can use something like the following:

UploadFileMutation.js

// @flow

import { commitMutation, graphql } from 'react-relay';

import type { Environment } from 'react-relay';
import type { UploadFileInput, UploadFileMutationResponse } from './__generated__/uploadFileMutation.graphql';

const mutation = graphql`
  mutation UploadFileMutation( $input: UploadFileInput! ) {
    UploadFile(input: $input) {
      error
      file {
        url
      }
    }
  }
`;

const getOptimisticResponse = (file: File | Blob) => ({
  UploadFile: {
    error: null,
    file: {
      url: file.uri,
    },
  },
});

function commit(
  environment: Environment,
  { fileName }: UploadFileInput,
  onCompleted: (data: UploadFileMutationResponse) => void,
  onError: () => void,
  uploadables,
) {
  return commitMutation(environment, {
    mutation,
    variables: {
      input: { fileName },
    },
    optimisticResponse: getOptimisticResponse(uploadables.fileToUpload),
    onCompleted,
    onError,
    uploadables,
  });
}

export default { commit };

Usage on component:

const uploadables = {
  fileToUpload: file, // file is the value of an input field for example
};

UploadFileMutation.commit(
  this.props.relay.environment,
  { fileName },
  onCompleted,
  onError,
  uploadables
);

The uploadables config option is kinda of hidden, since there is no mention to it on the docs, but it can be found here: https://github.com/facebook/relay/blob/c4430643002ec409d815366b0721ba88ed3a855a/packages/relay-runtime/mutations/commitRelayModernMutation.js#L32

jonathancardoso
  • 9,319
  • 6
  • 49
  • 63
0

While you can definitely implement uploading files to your GraphQL API endpoint, it's considered to be an anti-pattern (you will bump into issues with max file size etc.).

A better alternative would be obtaining a signed URL from your GraphQL API for uploading a file directly from the client-side app to Amazon S3, Google Cloud Storage etc.

If the server-side code needs to save URL in the database once the upload is complete, it can subscribe to this event directly. Check object change notification in Google Cloud as an example.

Konstantin Tarkus
  • 35,208
  • 14
  • 127
  • 117