Skip to content

Commit

Permalink
Version 1.0.0.
Browse files Browse the repository at this point in the history
  • Loading branch information
jaydenseric committed Feb 24, 2017
0 parents commit 27b1f20
Show file tree
Hide file tree
Showing 9 changed files with 1,975 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# http://editorconfig.org

root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
npm-debug.log
.DS_Store
dist
5 changes: 5 additions & 0 deletions apollo-upload-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 67 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"name": "apollo-upload-client",
"version": "1.0.0",
"license": "MIT",
"author": {
"name": "Jayden Seric",
"email": "me@jaydenseric.com",
"url": "http://jaydenseric.com"
},
"repository": {
"type": "git",
"url": "https://github.com/jaydenseric/apollo-upload-client.git"
},
"dependencies": {
"apollo-client": "^0.8.7",
"object-path": "^0.11.3",
"recursive-iterator": "^2.0.3"
},
"devDependencies": {
"babel-eslint": "^7.1.1",
"babel-preset-env": "^1.1.8",
"babel-preset-stage-0": "^6.22.0",
"eslint": "^3.16.0",
"eslint-config-standard": "^6.2.1",
"eslint-plugin-promise": "^3.4.2",
"eslint-plugin-standard": "^2.0.1",
"rollup": "^0.41.4",
"rollup-plugin-babel": "^2.7.1",
"rollup-watch": "^3.2.2"
},
"scripts": {
"lint": "eslint .",
"prebuild": "npm run lint",
"build": "rollup --config",
"build:watch": "npm run build -- --watch",
"prepublish": "npm run build"
},
"main": "dist/apollo-upload-client.js",
"module": "dist/apollo-upload-client.module.js",
"files": [
"src",
"dist"
],
"babel": {
"presets": [
[
"env",
{
"targets": {
"browsers": [
"> 2%"
]
},
"modules": false,
"loose": true
}
],
"stage-0"
]
},
"eslintConfig": {
"parser": "babel-eslint",
"extends": [
"standard"
]
}
}
129 changes: 129 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# ![Apollo upload client](https://cdn.rawgit.com/jaydenseric/apollo-upload-client/v1.0.0/apollo-upload-logo.svg)

![NPM version](https://img.shields.io/npm/v/apollo-upload-client.svg?style=flat-square) ![Github issues](https://img.shields.io/github/issues/jaydenseric/apollo-upload-client.svg?style=flat-square) ![Github stars](https://img.shields.io/github/stars/jaydenseric/apollo-upload-client.svg?style=flat-square)

In combination with [Apollo upload server](https://github.com/jaydenseric/apollo-upload-server), enhances [Apollo](http://apollodata.com) for intuitive file uploads via GraphQL mutations or queries.

- [> 2%](http://browserl.ist/?q=%3E+2%25) market share browsers supported.
- [MIT license](https://en.wikipedia.org/wiki/MIT_License).

## Setup

Install with [Yarn](https://yarnpkg.com):

```
yarn add apollo-upload-client
```

Create the Apollo client with the special network interface:

```js
import ApolloClient from 'apollo-client'
import createNetworkInterface from 'apollo-upload-client'

const client = new ApolloClient({
networkInterface: createNetworkInterface({
uri: '/graphql'
})
})
```

Also setup [Apollo upload server](https://github.com/jaydenseric/apollo-upload-server).

## Usage

Once setup, you will be able to use [`File`](https://developer.mozilla.org/en/docs/Web/API/File) objects, [`FileList`](https://developer.mozilla.org/en/docs/Web/API/FileList) objects, or `File` arrays within query or mutation input variables.

The files upload to a temp directory; the paths and metadata will be avalable under the variable name in the resolver. See the [server usage](https://github.com/jaydenseric/apollo-upload-server#Usage).

### Single file

See [server usage for this example](https://github.com/jaydenseric/apollo-upload-server#Single-file).

```js
import React, {Component, PropTypes} from 'react'
import {graphql} from 'react-apollo'
import gql from 'graphql-tag'

@graphql(gql`
mutation updateUserAvatar ($userId: String!, $avatar: File!) {
updateUserAvatar (userId: $userId, avatar: $avatar) {
id
}
}
`)
export default class extends Component {
static propTypes = {
userId: PropTypes.string.isRequired,
mutate: PropTypes.func.isRequired
}

onChange = ({target}) => {
if (target.validity.valid) {
this.props
.mutate({
variables: {
userId: this.props.userId,
avatar: target.files[0]
}
})
.then(({data}) => console.log('Mutation response:', data))
}
}

render () {
return <input type='file' accept={'image/jpeg,image/png'} required onChange={this.onChange} />
}
}
```

### Multiple files

See [server usage for this example](https://github.com/jaydenseric/apollo-upload-server#Multiple-files).

```js
import React, {Component, PropTypes} from 'react'
import {graphql} from 'react-apollo'
import gql from 'graphql-tag'

@graphql(gql`
mutation updateGallery ($galleryId: String!, $images: [File!]!) {
updateGallery (galleryId: $galleryId, images: $images) {
id
}
}
`)
export default class extends Component {
static propTypes = {
galleryId: PropTypes.string.isRequired,
mutate: PropTypes.func.isRequired
}

onChange = ({target}) => {
if (target.validity.valid) {
this.props
.mutate({
variables: {
galleryId: this.props.galleryId,
images: target.files
}
})
.then(({data}) => console.log('Mutation response:', data))
}
}

render () {
return <input type='file' accept={'image/jpeg,image/png'} multiple required onChange={this.onChange} />
}
}
```

## Caveats

- Batching is not compatible as only the standard Apollo network interface has been extended yet.

## Inspiration

- [@HriBB](https://github.com/HriBB)’s [apollo-upload-network-interface](https://github.com/HriBB/apollo-upload-network-interface) and [graphql-server-express-upload](https://github.com/HriBB/graphql-server-express-upload) projects.
- [@danielbuechele](https://github.com/danielbuechele)’s [Medium article](https://medium.com/@danielbuechele/file-uploads-with-graphql-and-apollo-5502bbf3941e).
- [@jessedvrs](https://github.com/jessedvrs)’s [example code](https://github.com/HriBB/apollo-upload-network-interface/issues/5#issuecomment-280018715).
20 changes: 20 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import babel from 'rollup-plugin-babel'

const pkg = require('./package.json')

export default {
entry: 'src/index.js',
external: Object.keys(pkg.dependencies),
plugins: [
babel()
],
targets: [{
dest: pkg['main'],
format: 'cjs',
sourceMap: true
}, {
dest: pkg['module'],
format: 'es',
sourceMap: true
}]
}
77 changes: 77 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {printAST} from 'apollo-client'
import {HTTPFetchNetworkInterface} from 'apollo-client/transport/networkInterface'
import RecursiveIterator from 'recursive-iterator'
import objectPath from 'object-path'

export default function createNetworkInterface (opts) {
const {uri} = opts
return new UploadHTTPFetchNetworkInterface(uri, opts)
}

class UploadHTTPFetchNetworkInterface extends HTTPFetchNetworkInterface {
constructor (...args) {
super(...args)

// Store the normal fetch method so it can be used if there are no uploads
const normalFetch = this.fetchFromRemoteEndpoint.bind(this)

this.fetchFromRemoteEndpoint = ({request, options}) => {
let hasFile = false
let variables
let formData

// Recursively search GraphQL input variables for FileList or File objects
for (let {node, path} of new RecursiveIterator(request.variables)) {
const isFileList = node instanceof window.FileList
const isFile = node instanceof window.File

// Only populate certain variables when nessesary
if (isFileList || isFile) {
if (!variables) variables = objectPath(request.variables)
var pathString = path.join('.')
}

if (isFileList) {
// Convert to FileList to File array. This is
// nessesary so items can be manipulated correctly
// by object-path. Either format may be used when
// populating GraphQL variables on the client.
variables.set(pathString, Array.from(node))
} else if (isFile) {
// Check if this is the first file found
if (!hasFile) {
hasFile = true
formData = new window.FormData()
}

// Move the File object to a multipart form field
// with the field name holding the original path
// to the file in the GraphQL input variables.
formData.append(pathString, node)
variables.del(pathString)
}
}

// If there are no uploads use the original fetch method
return hasFile ? this.uploadFetch({request, options}, formData) : normalFetch({request, options})
}
}

uploadFetch ({request, options}, formData) {
// Add Apollo fields to the form
formData.append('operationName', request.operationName)
formData.append('query', printAST(request.query))
formData.append('variables', JSON.stringify(request.variables))

// Send the multipart form
return window.fetch(this._opts.uri, {
...options,
body: formData,
method: 'POST',
headers: {
Accept: '*/*',
...options.headers
}
})
}
}
Loading

0 comments on commit 27b1f20

Please sign in to comment.