This is the second part of doing a file upload using GraphQL-ruby to Active Storage. You can find the previous blog: File upload in GraphQL-ruby using Active Storage Direct Upload
Yes. I am writing this blog because I finally figured out what I was doing wrong when doing a multipart file upload.
Problem Statement
Let say Sam uploads a document to the application. The document gets direct uploaded to S3(or any other service you use), but Sam doesn’t save the form for some reason. The document doesn’t get associated in the database so the application doesn’t show wrong data but we have a stale file in the S3 which is never needed or would be duplicated when Sam retires to upload the document later.
Multipart file upload to rescue
Multipart file upload does upload the document to the Active Storage service(S3) unless the request to mutate the record is sent to the server.
Setup client-side
- Add the npm package apollo-upload-client. The NPM package helps in making the multipart request to the server via Apollo.
yarn add apollo-upload-client
or
npm install apollo-upload-client
- Update the component which configures the ApolloClient
for the application. It should delegate all the multipart
HTTP request to upload link provided by
apollo-upload-client
.
// Main.jsx
import { createUploadLink } from 'apollo-upload-client';
const httpLink = ApolloLink.split(
(operation) => operation.getContext().hasUpload,
createUploadLink({ uri: '/graphql' }),
new HttpLink({ uri: '/graphql' }),
);
const client = new ApolloClient({
...
link: httpLink,
});
In the above code, we are delegating all the
request to createUploadLink
when the
hasUpload
is set to true
in mutation context.
Apollo doesn’t know unless mentioned explicitly
that the request has file upload and needs to
delegate to createUploadLink
instead of HttpLink
.
- Let’s take an example of profile avatar upload react component and the mutation setup.
import React, { Fragment } from 'react';
import { useMutation } from '@apollo/client';
import { UPDATE_PROFILE } from "./mutations";
export default function EditProfile() {
const [updateProfile] = useMutation(UPDATE_PROFILE, {
context: { hasUpload: true },
onComplete: {
// do something
},
});
const handleFileChange = ({ target: { validity, files } }) => {
if (validity.valid) {
// save to component state
// or save the file to the form state management library you are using.
}
}
const handleSubmit = (payload) => {
// make sure payload has files in the namespace you require.
// that can be nested attributes, single or many attachments.
updateProfile({ variables: payload });
}
return (
<Fragment>
// form HTML or React component need for basic profile update.
...
<input name="users[avatar]" type="file" onChange={handleFileChange} />
</Fragment>
)
}
Setup server-side
- Add the ApolloUploadServer
gem to the
gemfile
. ApolloUploadServer adds a middleware that helps in parsing the multipart request sent by the client.
# gemfile
# for multipart file upload in GraphQL
gem 'apollo_upload_server', '2.0.2'
- Add a graphql mutation which accepts a file as an argument
# apps/graphql/input_object_attributes/user_update_attributes.rb
class InputObjectAttributes::UserUpdateAttributes < Types::BaseInputObject
description "Attributes for update User"
argument :id, ID, required: false
argument :email, String, required: true
...
argument :avatar, CustomTypes::FileType, required: true
end
# apps/graphql/mutations/users/update.rb
class Mutations::Users::Update < Mutations::BaseMutation
graphql_name "UpdateProfile"
description "Mutation to update the user details"
argument :id, ID, required: true
argument :user_attributes,
InputObjectAttributes::UserUpdateAttributes,
required: true
field :user, Types::UserType, null: false
def resolve(id:, user_attributes:)
user = User.find_by!(id: id)
if user.update!(user_attributes.to_h)
{ user: user }
end
end
end
- The last part is adding a custom scalar instead of using
ApolloUploadServer::Upload
scalar type provided by ApolloUploadServer gem. Rails Active Storage excepts the file to beActionDispatch::Http::UploadedFile
class butApolloUploadServer
return aApolloUploadServer::Wrappers::UploadedFile
.
# apps/graphql/custom_types/file_type
class CustomTypes::FileType < Types::BaseScalar
description "A valid URL, transported as a string"
def self.coerce_input(file, context)
ActionDispatch::Http::UploadedFile.new(
filename: file.original_filename,
type: file.content_type,
headers: file.headers,
tempfile: file.tempfile,
)
end
end
Happy Coding!!