Prisma File Handling

File Handling with AWS S3, Prisma & graphql-yoga.

manticarodrigo Node.js Leave a Comment

File Handling Using Prisma, graphql-yoga, and AWS S3

After implementing prisma’s official file handling example I decided to investigate a possible GraphQL implementation where files were included in the mutation rather than as a separate REST request. I discovered that graphql-yoga has a built-in implementation of apollo-upload-server. All that is missing is a client-side GraphQL multi-part request which is easily done with apollo-upload-client and React.js. You can find the finished project repo here.

You might be asking yourself what some advantages or shortcomings this approach might have. One advantage is the ability to include the files in a GraphQL mutation along with any other data you may be sending to your server (think of a post that includes text and images). This saves you at least one round-trip where you would otherwise have to wait for the file upload to complete and then send the meta data with the rest of your mutation. A potential disadvantage is the control you have on the client-side when uploading a file (e.g. progress bar). However, you can work around this by creating a subscription to communicate the progress or other information with your server (I might include an example later).

First Steps

If you haven’t already, install prisma with npm install -g prisma.

Go to the directory you would like to have your project’s files in via terminal.

Run prisma init prisma-s3 or replace prisma-s3 with your project name. Then it’ll ask you what kind of boilerplate you want to use. For this tutorial I’ll be using react-fullstack-basic on a local deployment using Docker. This will give you a server folder with your prisma and graphql-yoga setup, and a src folder with your React.js boilerplate structure and components.

The first thing you want to do after initializing the service is install the necessary dependencies with npm install aws-sdk cors uuid.

Then, replace the contents of /server/database/datamodel.graphql with:


type File {
  id: ID! @unique
  createdAt: DateTime!
  updatedAt: DateTime!
  filename: String!
  mimetype: String!
  encoding: String!
  url: String! @unique
}

This creates the SDL model for prisma to generate it’s API. Running prisma deploy will generate the files in /server/src/generated/prisma.graphql necessary to interact with prisma. Do not modify this file yourself.

Creating the server

Now you create the directory and file /server/src/modules/fileApi.js and paste the following:


const uuid = require('uuid/v1')
const aws = require('aws-sdk')

const s3 = new aws.S3({
  accessKeyId: 'foo',
  secretAccessKey: 'bar',
  params: {
    Bucket: 'com.prisma.s3'
  },
  endpoint: new aws.Endpoint('http://localhost:4569') // fake s3 endpoint for local dev
})

exports.processUpload = async ( upload, ctx ) => {
  if (!upload) {
    return console.log('ERROR: No file received.')
  }
  
  const { stream, filename, mimetype, encoding } = await upload
  const key = uuid() + '-' + filename

  // Upload to S3
  const response = await s3
    .upload({
      Key: key,
      ACL: 'public-read',
      Body: stream
    }).promise()

  const url = response.Location

  // Sync with Prisma
  const data = {
    filename,
    mimetype,
    encoding,
    url,
  }

  const { id } = await ctx.db.mutation.createFile({ data }, ` { id } `)

  const file = {
    id,
    filename,
    mimetype,
    encoding,
    url,
  }

  console.log('saved prisma file:')
  console.log(file)

  return file
}

This module interacts with the aws-sdk api for S3 uploading and exposes the function processUpload(). This example uses a local fake s3 docker deployment, but you can replace the credentials with the ones for your deployed S3 instance.

Then, go to /server/src/index.js in a file editor and apply the changes reflected here:


const { GraphQLServer } = require('graphql-yoga')
const { Prisma } = require('prisma-binding')
const { processUpload } = require('./modules/fileApi')
const cors = require('cors')

const resolvers = {
  Query: {
    file(parent, { id }, context, info) {
      return context.db.query.file({ where: { id } }, info)
    },
    
    files(parent, args, context, info) {
      return context.db.query.files(args, info)
    }
  },
  Mutation: {
    async uploadFile(parent, { file }, ctx, info) {
      return await processUpload(await file, ctx)
    },
  
    async uploadFiles(parent, { files }, ctx, info) {
      return Promise.all(files.map(file => processUpload(file, ctx)))
    },
  
    async renameFile(parent, { id, name }, ctx, info) {
      return ctx.db.mutation.updateFile({ data: { name }, where: { id } }, info)
    },
  
    async deleteFile(parent, { id }, ctx, info) {
      return await ctx.db.mutation.deleteFile({ where: { id } }, info)
    },
  },
}

const server = new GraphQLServer({
  typeDefs: './src/schema.graphql',
  resolvers,
  context: req => ({
    ...req,
    db: new Prisma({
      typeDefs: 'src/generated/prisma.graphql',
      endpoint: 'http://localhost:4466/prisma-s3/dev',
      secret: 'mysecret123',
      debug: true,
    }),
  }),
})

server.express.use('/*', cors()) // allow cors

server.start(() => console.log('Server is running on http://localhost:4000'))

We are importing the processUpload() function with the import statement const { processUpload } = require('./modules/fileApi') at the top.

The resolvers include queries for file meta-data fetching and mutations for file upload.

Finally, we are importing cors and using it on any endpoint server.express.use('/*', cors()) to avoid CORS errors.

To finish the server-side implementation, we need to modify /server/src/schema.graphql to look like this:


# import ID, File from "./generated/prisma.graphql"

scalar Upload

type Query {
  file(id: ID!): File
  files: [File!]!
}

type Mutation {
  uploadFile (file: Upload!): File!
  uploadFiles (files: [Upload!]!): [File!]!
  renameFile(id: ID!, name: String!): File
  deleteFile(id: ID!): File
}

The important bit is making sure you define scalar Upload which activates graphql-yoga‘s implementation of apollo-upload-server and let’s you receive files in GraphQL mutations.

Creating the client

Using your command line, go up to the root directory and npm install apollo-upload-client apollo-client-preset.

In my project, I deleted /src/assets, /src/constants and all the pre-configured files within /src/components. What you need to do is add /src/components/UploadFile.js with the following content:


import React from 'react'
import { withRouter } from 'react-router-dom'
import { graphql } from 'react-apollo'
import gql from 'graphql-tag'

class UploadFile extends React.Component {
  state = {
    file: null,
  }

  render() {
    return (
      <div>
        <form>
          <h1>Upload File</h1>
          <input
            type='file'
            accept='image/*'
            onChange={(event)=> { 
              this._uploadFile(event) 
            }}
            onClick={(event)=> { 
              event.target.value = null
            }}
          />
        </form>
      </div>
    )
  }

  _uploadFile = (event) => {
    const files = event.target.files
    const file = files[0]
    this.props.uploadMutation({
      variables: {
        file
      }
    }).catch(error => {
      console.log(error)
    })
    this.props.history.push(`/`)
  }
}

const UPLOAD_MUTATION = gql`
  mutation uploadFile($file: Upload!) {
    uploadFile(
      file: $file
    ) {
      id
    }
  }
`

const UploadFileWithMutation = graphql(UPLOAD_MUTATION, {
  name: 'uploadMutation', // name of the injected prop: this.props.uploadMutation...
})(UploadFile)

export default withRouter(UploadFileWithMutation)

Finally, in /src/index.js update the contents like so:


import React from 'react'
import ReactDOM from 'react-dom'
import {
  BrowserRouter as Router,
  Route,
  Switch,
} from 'react-router-dom'
import { ApolloProvider } from 'react-apollo'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloLink } from 'apollo-client-preset'
import { createUploadLink } from 'apollo-upload-client'

import UploadFile from './components/UploadFile'

import './index.css'

const authLink = new ApolloLink((operation, forward) => {
  const token = JSON.parse(localStorage.getItem('AUTH_TOKEN'))
  const authorizationHeader = token ? `Bearer ${token}` : null
  operation.setContext({
    headers: {
      authorization: authorizationHeader
    }
  })
  return forward(operation)
})

const client = new ApolloClient({
  link: authLink.concat(createUploadLink({ uri: 'http://localhost:4000' })),
  cache: new InMemoryCache()
})

ReactDOM.render(
  <ApolloProvider client={client}>
    <Router>
      <React.Fragment>
        <div>
          <Switch>
            <Route exact path='/' component={UploadFile} />
          </Switch>
        </div>
      </React.Fragment>
    </Router>
  </ApolloProvider>,
  document.getElementById('root'),
)

Now you’ve wrapped your apollo client with an auth link (optional) and createUploadLink, thus activating apollo-upload-client.

That’s it! You’re ready to delegate everything to the server and send your files along with your mutations using the power of GraphQL. Good luck on your file handling adventures!

Leave a Reply

Your email address will not be published. Required fields are marked *