Skip to content

Commit

Permalink
Merge pull request #22 from jaydenseric/spec-v2
Browse files Browse the repository at this point in the history
New API to support the GraphQL multipart request spec v2. Fixes [#13](jaydenseric/graphql-upload#13).
  • Loading branch information
krasivyy3954 committed Nov 19, 2017
2 parents 8d8985f + ca90246 commit 439b4c0
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 60 deletions.
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Next

* New API to support the [GraphQL multipart request spec v2.0.0-alpha.2](https://github.com/jaydenseric/graphql-multipart-request-spec/releases/tag/v2.0.0-alpha.2). Files no longer upload to the filesystem; [readable streams](https://nodejs.org/api/stream.html#stream_readable_streams) are used in resolvers instead.
* Export a new `Upload` scalar type to use in place of the old `Upload` input type. It represents a file upload promise that resolves an object containing `stream`, `filename`, `mimetype` and `encoding`.
* Deprecated the `uploadDir` middleware option.
* Added new `maxFieldSize`, `maxFileSize` and `maxFiles` middleware options.
* `graphql` is now a peer dependency.
* Middleware are now arrow functions.

## 3.0.0
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,19 @@
"node": ">=7.6"
},
"dependencies": {
"formidable": "^1.1.1",
"mkdirp": "^0.5.1",
"busboy": "^0.2.14",
"object-path": "^0.11.4"
},
"peerDependencies": {
"graphql": "^0.11.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0-beta.32",
"@babel/core": "^7.0.0-beta.32",
"@babel/preset-env": "^7.0.0-beta.32",
"eslint": "^4.11.0",
"eslint-plugin-prettier": "^2.3.1",
"graphql": "^0.11.7",
"husky": "^0.14.3",
"lint-staged": "^5.0.0",
"prettier": "^1.8.2"
Expand Down
58 changes: 34 additions & 24 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,25 @@

[![npm version](https://img.shields.io/npm/v/apollo-upload-server.svg)](https://npm.im/apollo-upload-server) ![Licence](https://img.shields.io/npm/l/apollo-upload-server.svg) [![Github issues](https://img.shields.io/github/issues/jaydenseric/apollo-upload-server.svg)](https://github.com/jaydenseric/apollo-upload-server/issues) [![Github stars](https://img.shields.io/github/stars/jaydenseric/apollo-upload-server.svg)](https://github.com/jaydenseric/apollo-upload-server/stargazers)

Enhances [Apollo](https://apollographql.com) for intuitive file uploads via GraphQL mutations or queries. Use with [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client).
Enhances [Apollo](https://apollographql.com) for intuitive file uploads via GraphQL queries or mutations. Use with [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client).

## Setup

Install with [npm](https://www.npmjs.com):
Install with peer dependencies using [npm](https://www.npmjs.com):

```
npm install apollo-upload-server
npm install apollo-upload-server graphql
```

### Middleware

Add the server middleware just before [graphql-server](https://github.com/apollographql/graphql-server).
Add the middleware just before [graphql-server](https://github.com/apollographql/graphql-server).

#### Options

* `maxFieldSize` (integer): Max allowed non-file multipart form field size in bytes; enough for your queries (default: 1 MB).
* `maxFileSize` (integer): Max allowed file size in bytes (default: Infinity).
* `maxFiles` (integer): Max allowed number of files (default: Infinity).

#### [Koa](http://koajs.com)

Expand All @@ -28,10 +34,7 @@ import { apolloUploadKoa } from 'apollo-upload-server'
router.post(
'/graphql',
koaBody(),
apolloUploadKoa({
// Defaults to OS temp directory
uploadDir: './uploads'
}),
apolloUploadKoa(/* Options */),
graphqlKoa(/**/)
)
```
Expand All @@ -46,33 +49,40 @@ import { apolloUploadExpress } from 'apollo-upload-server'
app.use(
'/graphql',
bodyParser.json(),
apolloUploadExpress({
// Defaults to OS temp directory
uploadDir: './uploads'
}),
apolloUploadExpress(/* Options */),
graphqlExpress(/**/)
)
```

#### Custom middleware

If the middleware you need is not available, import the async `processRequest` function to make your own:
To make your own middleware import the `processRequest` async function:

```js
import { processRequest } from 'apollo-upload-server'
```

### GraphQL schema
### `Upload` scalar

A file upload promise that resolves an object containing:

Add an input type for uploads to your schema. You can name it anything but it must have this shape:
* `stream`
* `filename`
* `mimetype`
* `encoding`

```graphql
input Upload {
name: String!
type: String!
size: Int!
path: String!
}
It must be added to your types and resolvers:

```js
import { makeExecutableSchema } from 'graphql-tools'
import { GraphQLUpload } from 'apollo-upload-server'

const schema = makeExecutableSchema({
typeDefs: [`scalar Upload`],
resolvers: {
Upload: GraphQLUpload
}
})
```

### Client
Expand All @@ -83,9 +93,9 @@ Also setup [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-c

Once setup, on the client use [`FileList`](https://developer.mozilla.org/en/docs/Web/API/FileList), [`File`](https://developer.mozilla.org/en/docs/Web/API/File) and [`ReactNativeFile`](https://github.com/jaydenseric/apollo-upload-client#react-native) instances anywhere within query or mutation input variables. See the [client usage](https://github.com/jaydenseric/apollo-upload-client#usage).

The files upload to a configurable temp directory on the GraphQL server. `Upload` input type metadata replaces file instances in the arguments received by the resolver.
Files upload via a [GraphQL multipart request](https://github.com/jaydenseric/graphql-multipart-request-spec) and appear as [`Upload` scalars](#upload-scalar) in resolver arguments.

See the [example API and client](https://github.com/jaydenseric/apollo-upload-examples)
See the [example API and client](https://github.com/jaydenseric/apollo-upload-examples).

## Support

Expand Down
95 changes: 61 additions & 34 deletions src/index.mjs
Original file line number Diff line number Diff line change
@@ -1,52 +1,79 @@
import mkdirp from 'mkdirp'
import formidable from 'formidable'
import { GraphQLScalarType } from 'graphql'
import Busboy from 'busboy'
import objectPath from 'object-path'

export function processRequest(request, { uploadDir } = {}) {
// Ensure provided upload directory exists
if (uploadDir) mkdirp.sync(uploadDir)
export const GraphQLUpload = new GraphQLScalarType({
name: 'Upload',
description:
'The `Upload` scalar type represents a file upload promise that resolves ' +
'an object containing `stream`, `filename`, `mimetype` and `encoding`.',
parseValue: value => value,
parseLiteral() {
throw new Error('Upload scalar literal unsupported')
},
serialize() {
throw new Error('Upload scalar serialization unsupported')
}
})

const form = formidable.IncomingForm({
// Defaults to the OS temp directory
uploadDir
})

// Parse the multipart form request
return new Promise((resolve, reject) => {
form.parse(request, (error, { operations }, files) => {
if (error) reject(new Error(error))

// Decode the GraphQL operation(s). This is an array if batching is
// enabled.
operations = JSON.parse(operations)

// Check if files were uploaded
if (Object.keys(files).length) {
// File field names contain the original path to the File object in the
// GraphQL operation input variables. Relevent data for each uploaded
// file now gets placed back in the variables.
const operationsPath = objectPath(operations)
Object.keys(files).forEach(variablesPath => {
const { name, type, size, path } = files[variablesPath]
operationsPath.set(variablesPath, { name, type, size, path })
})
export const processRequest = (
request,
{ maxFieldSize, maxFileSize, maxFiles } = {}
) =>
new Promise(resolve => {
const busboy = new Busboy({
headers: request.headers,
limits: {
fieldSize: maxFieldSize,
fields: 2, // Only operations and map
fileSize: maxFileSize,
files: maxFiles
}
})

// GraphQL multipart request spec:
// https://github.com/jaydenseric/graphql-multipart-request-spec

let operations
let operationsPath

// Provide fields for replacement request body
resolve(operations)
busboy.on('field', (fieldName, value) => {
switch (fieldName) {
case 'operations':
operations = JSON.parse(value)
operationsPath = objectPath(operations)
break
case 'map': {
for (const [mapFieldName, paths] of Object.entries(
JSON.parse(value)
)) {
// Upload scalar
const upload = new Promise(resolve =>
busboy.on(
'file',
(fieldName, stream, filename, encoding, mimetype) =>
fieldName === mapFieldName &&
resolve({ stream, filename, mimetype, encoding })
)
)

for (const path of paths) operationsPath.set(path, upload)
}
resolve(operations)
}
}
})

request.pipe(busboy)
})
}

export const apolloUploadKoa = options => async (ctx, next) => {
// Skip if there are no uploads
if (ctx.request.is('multipart/form-data'))
ctx.request.body = await processRequest(ctx.req, options)
await next()
}

export const apolloUploadExpress = options => (request, response, next) => {
// Skip if there are no uploads
if (!request.is('multipart/form-data')) return next()
processRequest(request, options)
.then(body => {
Expand Down

0 comments on commit 439b4c0

Please sign in to comment.