GraphQL multi-part request spec outlines a specification for multipart form requests in GraphQL. Jayden Seric, the author of the spec, has also authored two libraries apollo-upload-client and apollo-upload-server which makes it effortless to integrate file uploads in Apollo Server.
GridFS is a simple file system abstraction on top of MongoDB. This facilitates storage of large files (beyond BSON document size limit) in mongo database.
This post outlines the minimal integration required to save the files uploaded through the GraphQL API to GridFS.
We will use Koa as the HTTP server but the approach is applicable to any Apollo supported backend.
The Apollo-koa integration library comes built in with apollo-upload-server, so if we initialize our Apollo server and configure it use koa server, it will automatically configure body parser and enable file uploads.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import Koa from "koa"; import { ApolloServer, gql } from "apollo-server-koa"; const graphQLServer = new ApolloServer({ typeDefs, // Described below resolvers: { Query: { // ... add queries here }, Mutation: { addFile // Described below // ... add other mutations here } } }); graphQLServer.applyMiddleware({ app: server }); |
Apollo server exports an Upload scalar type which we can use in our typedefs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import { gql } from "apollo-server-koa"; const typeDefs = gql` type File { _id: ID! path: String! filename: String! mimetype: String! encoding: String! } type Mutation { uploadFile(file: Upload!): File } ` |
Now our add file resolver is ready to accept the incoming file stream and can pass it on to GridFS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import mongodb from "mongodb"; const addFile = async (_parent, { file }) => { const { stream, filename, mimetype, encoding } = await file; const bucket = new mongodb.GridFSBucket(db._db); const uploadStream = bucket.openUploadStream(filename); await new Promise((resolve, reject) => { stream .pipe(uploadStream) .on("error", reject) .on("finish", resolve); }); return { _id: uploadStream.id, filename, mimetype, encoding } }; |
In order to facilitate uploading of files from the client, we need to configure Apollo client to use an upload link:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import { ApolloProvider } from "react-apollo"; import { ApolloClient } from "apollo-client"; import { InMemoryCache } from "apollo-cache-inmemory"; import { HttpLink } from "apollo-link-http"; import { onError } from "apollo-link-error"; import { ApolloLink } from "apollo-link"; import { createUploadLink } from "apollo-upload-client"; const client = new ApolloClient({ link: ApolloLink.from([ // Report errors to console in a user friendly format onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) graphQLErrors.map(({ message, locations, path }) => console.log( `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}` ) ); if (networkError) console.log(`[Network error]: ${networkError}`); }), createUploadLink({ uri: process.env.APP_URL + "/graphql", credentials: "same-origin" }) ]), cache: new InMemoryCache() }); |
Note that using http link is no longer required, and any configuration options that could be passed to http link can be passed to createUploadLink
. In fact, if http link is used before createUploadLink
file uploads won’t work at all because http link is a terminal link and the upload link added after would not be used.
Now we can upload files to our GraphQL endpoint using Mutation
component.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import gql from "graphql-tag"; import { Mutation } from "react-apollo"; const UPLOAD_FILE = gql` mutation($file: Upload!) { uploadFile(file: $file) { _id filename } } `; export default () => ( <ApolloProvider client={client}> <Mutation mutation={UPLOAD_FILE}> {mutate => ( <input type="file" required onChange={event => { const [file] = event.target.files; mutate({ variables: { file } }); }} /> )} </Mutation> </ApolloProvider> ); |
This concludes our server side as well as client side setup involved in connecting a file input field to mongodb gridfs.