- graphql-upload
- aws-sdk
- apollo-upload-client
Navigate to https://s3.console.aws.amazon.com/s3/home?region=us-east-1 click on “create bucket”, enter a name, choose a region, and leave all other options as default.
Your express app needs to be able accept multipart/form-data requests. To do that, you will use the graphQLUploadExpress
from the graphql-upload
package.
This middleware will make it so that the /graphql
path will only accept multipart/form-data requests.
In server.js
, require it at the top, const { graphQLUploadExpress } = require('graphql-upload');
Then, in server.js
, use it as the middleware for your /graphql
path:
app.use(
"/graphql",
// use graphQLUploadExpress as the middleware for /graphql path
graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }),
expressGraphQL({
schema,
graphiql: true
})
);
If you want to know more about
graphqlUploadExpress
and how to use it, look here: https://github.com/jaydenseric/graphql-upload#function-graphqluploadexpress
Make a file called s3.js
in your schema
folder or in your services
folder.
In there, you will use the package, aws-sdk
, set up your credentials for aws, and then export it.
const AWS = require("aws-sdk");
if (process.env.NODE_ENV !== "production") {
AWS.config.loadFromPath("./credentials.json");
}
const s3 = new AWS.S3({ apiVersion: "2006-03-01" });
module.exports = { s3 };
This function is allowing us to configure our aws keys using a json file.
You can learn more about how this works here: https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-json-file.html
You do not need a credentials.json in production. Instead, all you need to do is set environmental keys for AWS_ACCESS_KEY_ID
and AWS_SECRET_ACCESS_KEY
.
You can read more about how AWS uses the environment variables here: https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html
Make a file called credentials.json
at the root of your project.
In there, you will set your aws credentials.
{
"accessKeyId": "<Your AWS Access Key ID>",
"secretAccessKey": "<Your AWS Secret Access Key>",
"region": "us-east-1"
}
MAKE SURE TO GITIGNORE THIS FILE
We are going to create a function that accepts a single file, uploads it to our AWS S3 bucket, and returns the key to retrieve it from your bucket later (this will be saved to our database).
In s3.js
, define the following function:
const singleFileUpload = async file => {
const { filename, mimetype, createReadStream } = await file;
const fileStream = createReadStream();
const path = require("path");
// name of the file in your S3 bucket will be the date in ms plus the extension name
const Key = new Date().getTime().toString() + path.extname(filename);
const uploadParams = {
// name of your bucket here
Bucket: "aws-graphql-dev-testing",
Key,
Body: fileStream
};
const result = await s3.upload(uploadParams).promise();
// save the name of the file in your bucket as the key in your database to retrieve for later
return result.Key;
};
Export this from your s3.js
:
module.exports = { s3, singleFileUpload };
I followed the instructions for creating a filestream (
createReadStream
) here: https://github.com/jaydenseric/graphql-upload#class-graphqlupload
Then I followed the instructions for uploading to AWS S3 using that stream here, underneath the section "Configure the Server and AWS SDK": File Upload With GraphQL Using Apollo Server
In your backend's Mutation file, we will import the singleFileUpload
function we just created. (eg. const { singleFileUpload } = require("./s3")
)
We will also import the GraphQL type GraphQLUpload
from the package graphql-upload
:
const { GraphQLUpload } = require('graphql-upload');
If you want to read more about GraphQLUpload, you can do so here: https://github.com/jaydenseric/graphql-upload#class-graphqlupload
In this example mutation, we are going to be uploading an image
to a User
.
The args for the mutation will have a key of image
that has a type
of GraphQLUpload
.
The resolve function is asynchronous and will be calling singleFileUpload
, passing in image
from args
.
Afterwards, the return on the singleFileUpload
will be saved as the image
key on the new `User.
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: () => ({
newUser: {
type: UserType,
args: {
name: { type: new GraphQLNonNull(GraphQLString) },
email: { type: new GraphQLNonNull(GraphQLString) },
// type for the image file is GraphQLUpload
image: { type: GraphQLUpload }
},
async resolve(_, { name, email, image }) {
const updateObj = {};
if (name) updateObj.name = name;
if (email) updateObj.email = email;
if (image) {
updateObj.image = await singleFileUpload(image);
}
return new User(updateObj).save();
}
}
})
});
Now that we have upload to AWS S3 in our backend set up, we also need to be able to retrieve the files that we just uploaded.
We can do so by using the function getSignedUrl
on the package, aws-sdk
.
getSignedUrl
generates a secure url using our AWS credentials for our file that we uploaded and allows us to keep our AWS S3 files private.
We do not need to make any of the files or the bucket on AWS S3 public if we use getSignedUrl
It expects us to give it the key that we saved to our database.
const params = { Bucket: '<Name of your bucket>', Key: '<Key that we saved to our database>' };
const url = s3.getSignedUrl('getObject', params);
console.log('The URL we can send up to our frontend', url);
To learn more about the
getSignedUrl
function, look here: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
We want the image
field on a GraphQL UserType
to return the url of the image saved on AWS S3. But the image
field on a User
document is currently just the key to the file in our S3 bucket. We need to add a resolve to the image
field on a UserType
that will return the actual url of our file.
First, we import s3
from the s3.js
file that we created before at the top of the UserType.js
file.
Then we create a resolve function on the image
field and call s3.getSignedUrl
, passing in as the key, parentValue.image
.
We return the url that s3.getSignedUrl
returns.
const { GraphQLObjectType, GraphQLID, GraphQLString } = require('graphql');
const { s3 } = require('./s3');
const UserType = new GraphQLObjectType({
name: 'UserType',
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
email: { type: GraphQLString },
// to retrieve the image from aws
image: {
type: GraphQLString,
resolve(parentValue) {
let imageUrl;
if (parentValue.image) {
imageUrl = s3.getSignedUrl('getObject', {
Bucket: "aws-graphql-dev-testing",
Key: parentValue.image
});
}
return imageUrl || parentValue.image;
}
}
})
});
module.exports = UserType;
Great! Now we finally finished setting up the backend. Let's set up the frontend.
Instead of using createHttpLink
from apollo-link-http
, we will be using createUploadLink
from apollo-upload-client
in our client/index.js
file.
createUploadLink
is what we will use in place of createHttpLink
. We can invoke createUploadLink
with the same options object that we passed into createHttpLink
.
It will allow us to file upload using GraphQL on our frontend.
To learn more about the function
createUploadLink
, see here: https://github.com/jaydenseric/apollo-upload-client#function-createuploadlink
//... other imports
import { createUploadLink } from 'apollo-upload-client';
//... setting up cache
let uri = "http://localhost:5000/graphql";
if (process.env.NODE_ENV === 'production') {
uri = "https://aws-s3-graphql.herokuapp.com/graphql";
}
const httpLink = createUploadLink({
uri,
headers: {
authorization: localStorage.getItem("auth-token")
}
});
const client = new ApolloClient({
uri,
link: httpLink,
cache,
onError: ({ networkError, graphQLErrors }) => {
console.log("graphQLErrors", graphQLErrors);
console.log("networkError", networkError);
}
});
Queries will stay the same as they were before.
Mutations are the way that we post information to our backend.
Two files to look at are client/src/graphql/mutations.js
and client/src/components/CreateUser.js
In client/src/graphql/mutations.js
, we are defining a mutation called CreateUser
that will be able to accept the variables, name
, email
, and image
. name
and email
's types will be String
, but the image
's type will be Upload
.
import gql from "graphql-tag";
// the type for $image is Upload
export const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!, $image: Upload!) {
newUser(name: $name, email: $email, image: $image) {
id
name
email
image
}
}
`;
In client/src/components/CreateUser.js
, we are making the component CreateUser
that will make the CreateUser
mutation once the form is submitted.
The only thing that looks out of the ordinary from a regular Apollo Mutation is how the input type file is defined inside of the form.
<input
type="file"
required
onChange={({
target: {
validity,
files: [file]
}
}) => validity.valid && this.setState({ image: file })}
/>
And voila! We finished setting up the frontend to accept files in GraphQL!
See the Live Version here