-
-
Notifications
You must be signed in to change notification settings - Fork 156
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 27b1f20
Showing
9 changed files
with
1,975 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
node_modules | ||
npm-debug.log | ||
.DS_Store | ||
dist |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.