How would you do file uploads in a React-Relay app?

后端 未结 5 1220
时光说笑
时光说笑 2020-12-30 03:45

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 inte

相关标签:
5条回答
  • 2020-12-30 03:47

    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
    
    0 讨论(0)
  • 2020-12-30 03:49

    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

    0 讨论(0)
  • 2020-12-30 03:59

    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.

    0 讨论(0)
  • 2020-12-30 04:01

    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

    0 讨论(0)
  • 2020-12-30 04:07

    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.

    0 讨论(0)
提交回复
热议问题