├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '4' 10 | before_install: 11 | - npm i -g npm@^2.0.0 12 | before_script: 13 | - npm prune 14 | after_success: 15 | - npm run semantic-release 16 | branches: 17 | except: 18 | - /^v\d+\.\d+\.\d+$/ 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Bojan Hribernik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-server-express-upload 2 | 3 | Graphql Server Express file upload middleware. Used together with [apollo-upload-network-interface](https://github.com/HriBB/apollo-upload-network-interface). 4 | 5 | ## Usage 6 | 7 | #### 1. Add `graphqlExpressUpload` middleware to your Express GraphQL endpoint 8 | 9 | ``` 10 | import { graphqlExpress, graphiqlExpress } from 'graphql-server-express' 11 | import bodyParser from 'body-parser' 12 | import graphqlExpressUpload from 'graphql-server-express-upload' 13 | import multer from 'multer' 14 | 15 | import schema from './schema' 16 | 17 | const upload = multer({ 18 | dest: config.tmp.path, 19 | }) 20 | 21 | app.use('/graphql', 22 | upload.array('files'), 23 | bodyParser.json(), 24 | graphqlExpressUpload({ endpointURL: '/graphql' }), // after multer and before graphqlExpress 25 | graphqlExpress((req) => { 26 | return { 27 | schema, 28 | context: {} 29 | } 30 | }) 31 | ) 32 | 33 | app.use('/graphiql', graphiqlExpress({ 34 | endpointURL: '/graphql', 35 | })) 36 | ``` 37 | 38 | #### 2. Add `UploadedFile` scalar to your schema 39 | 40 | ``` 41 | scalar UploadedFile 42 | ``` 43 | 44 | #### 3. Add `UploadedFile` resolver 45 | 46 | For now we simply use JSON. In the future we should improve this. 47 | 48 | ``` 49 | const resolvers = { 50 | UploadedFile: { 51 | __parseLiteral: parseJSONLiteral, 52 | __serialize: value => value, 53 | __parseValue: value => value, 54 | } 55 | ... 56 | } 57 | 58 | function parseJSONLiteral(ast) { 59 | switch (ast.kind) { 60 | case Kind.STRING: 61 | case Kind.BOOLEAN: 62 | return ast.value; 63 | case Kind.INT: 64 | case Kind.FLOAT: 65 | return parseFloat(ast.value); 66 | case Kind.OBJECT: { 67 | const value = Object.create(null); 68 | ast.fields.forEach(field => { 69 | value[field.name.value] = parseJSONLiteral(field.value); 70 | }); 71 | 72 | return value; 73 | } 74 | case Kind.LIST: 75 | return ast.values.map(parseJSONLiteral); 76 | default: 77 | return null; 78 | } 79 | } 80 | ``` 81 | 82 | #### 4. Add mutation on the server 83 | 84 | Schema definition 85 | 86 | ``` 87 | uploadProfilePicture(id: Int!, files: [UploadedFile!]!): ProfilePicture 88 | ``` 89 | 90 | And the mutation function 91 | 92 | ``` 93 | async uploadProfilePicture(root, { id, files }, context) { 94 | // you can now access files parameter from variables 95 | console.log('uploadProfilePicture', { id, files }) 96 | //... 97 | } 98 | ``` 99 | 100 | #### 5. Add mutation on the client 101 | 102 | Example using `react-apollo`. Don't forget that you need to be using [UploadNetworkInterface](https://github.com/HriBB/apollo-upload-network-interface/releases), because `apollo-client` does not support `multipart/form-data` out of the box. 103 | 104 | ``` 105 | import React, { Component, PropTypes } from 'react' 106 | import { graphql } from 'react-apollo' 107 | import gql from 'graphql-tag' 108 | 109 | class UploadProfilePicture extends Component { 110 | 111 | onSubmit = (fields) => { 112 | const { user, uploadProfilePicture } = this.props 113 | // fields.files is an instance of FileList 114 | uploadProfilePicture(user.id, fields.files) 115 | .then(({ data }) => { 116 | console.log('data', data); 117 | }) 118 | .catch(error => { 119 | console.log('error', error.message); 120 | }) 121 | } 122 | 123 | render() { 124 | return ( 125 | //... 126 | ) 127 | } 128 | 129 | } 130 | 131 | const UPLOAD_PROFILE_PICTURE = gql` 132 | mutation uploadProfilePicture($id: Int!, $files: [UploadedFile!]!) { 133 | uploadProfilePicture(id: $id, files: $files) { 134 | id url thumb square small medium large full 135 | } 136 | }` 137 | 138 | const withFileUpload = graphql(UPLOAD_PROFILE_PICTURE, { 139 | props: ({ ownProps, mutate }) => ({ 140 | uploadProfilePicture: (id, files) => mutate({ 141 | variables: { id, files }, 142 | }), 143 | }), 144 | }) 145 | 146 | export default withFileUpload(UploadProfilePicture) 147 | ``` 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-server-express-upload", 3 | "description": "GraphQL Server Express file upload middleware", 4 | "main": "src/index.js", 5 | "scripts": { 6 | "test": "echo \"No test specified\" && exit 0", 7 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/HriBB/graphql-server-express-upload.git" 12 | }, 13 | "keywords": [ 14 | "apollo", 15 | "graphql", 16 | "server", 17 | "express", 18 | "file", 19 | "upload", 20 | "middleware" 21 | ], 22 | "author": "Bojan Hribernik", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/HriBB/graphql-server-express-upload/issues" 26 | }, 27 | "homepage": "https://github.com/HriBB/graphql-server-express-upload#readme", 28 | "dependencies": { 29 | "body-parser": "^1.15.2", 30 | "graphql": "^0.7.2", 31 | "multer": "^1.2.0" 32 | }, 33 | "devDependencies": { 34 | "semantic-release": "^6.3.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function graphqlServerExpressUpload(options) { 2 | function isUpload(req) { 3 | return Boolean( 4 | req.baseUrl === options.endpointURL && 5 | req.method === 'POST' && 6 | req.is('multipart/form-data') 7 | ); 8 | } 9 | return function(req, res, next) { 10 | if (!isUpload(req)) { 11 | return next(); 12 | } 13 | var files = req.files; 14 | var body = req.body; 15 | var variables = JSON.parse(body.variables); 16 | // append files to variables 17 | files.forEach(file => { 18 | if (!variables[file.fieldname]) { 19 | variables[file.fieldname] = []; 20 | } 21 | variables[file.fieldname].push(file); 22 | }) 23 | req.body = { 24 | operationName: body.operationName, 25 | query: body.query, 26 | variables: variables 27 | }; 28 | return next(); 29 | } 30 | } 31 | --------------------------------------------------------------------------------