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
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
The client-side code further consists of 2 parts:
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
}
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:
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] }
)
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
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
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.
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
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.