Skip to content

Commit

Permalink
Merge pull request #179 from yaacovCR/master
Browse files Browse the repository at this point in the history
Support uploading files from a server environment. Fixes #172 .
  • Loading branch information
jaydenseric authored Mar 15, 2020
2 parents 0797dfb + 9f2efd1 commit e1bb6e7
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 24 deletions.
11 changes: 11 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@
- Updated Node.js support from v8.10+ to v10+.
- Updated dependencies, some of which require Node.js v10+.

### Minor

- Support uploading files from a server environment, fixing [#172](https://github.com/jaydenseric/apollo-upload-client/issues/172) via [#179](https://github.com/jaydenseric/apollo-upload-client/pull/179).
- Added `createUploadLink` options:
- `isExtractableFile`
- `FormData`
- `formDataAppendFile`
- Added exports for the new `createUploadLink` option defaults:
- `isExtractableFile`
- `formDataAppendFile`

### Patch

- Removed the now redundant [`eslint-plugin-import-order-alphabetical`](https://npm.im/eslint-plugin-import-order-alphabetical) dev dependency.
Expand Down
108 changes: 101 additions & 7 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,24 @@ Consider polyfilling:

- [class ReactNativeFile](#class-reactnativefile)
- [function createUploadLink](#function-createuploadlink)
- [function formDataAppendFile](#function-formdataappendfile)
- [function isExtractableFile](#function-isextractablefile)
- [type ExtractableFileMatcher](#type-extractablefilematcher)
- [type FetchOptions](#type-fetchoptions)
- [type FormDataFileAppender](#type-formdatafileappender)
- [type ReactNativeFileSubstitute](#type-reactnativefilesubstitute)

### class ReactNativeFile

Used to mark a [React Native `File` substitute](#type-reactnativefilesubstitute). It’s too risky to assume all objects with `uri`, `type` and `name` properties are files to extract. Re-exported from [`extract-files`](https://npm.im/extract-files) for convenience.
Used to mark [React Native `File` substitutes](#type-reactnativefilesubstitute) as it’s too risky to assume all objects with `uri`, `type` and `name` properties are extractable files.

| Parameter | Type | Description |
| :-- | :-- | :-- |
| `file` | [ReactNativeFileSubstitute](#type-reactnativefilesubstitute) | A React Native [`File`](https://developer.mozilla.org/docs/web/api/file) substitute. |
| `file` | [ReactNativeFileSubstitute](#type-reactnativefilesubstitute) | A React Native [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) substitute. |

#### See

- [`extract-files` `ReactNativeFile` docs](https://github.com/jaydenseric/extract-files#class-reactnativefile).

#### Examples

Expand All @@ -150,14 +158,21 @@ _A React Native file that can be used in query or mutation variables._
### function createUploadLink
Creates a terminating [Apollo Link](https://apollographql.com/docs/link) capable of file uploads. Options match [`createHttpLink`](https://apollographql.com/docs/link/links/http#options).
Creates a terminating [Apollo Link](https://apollographql.com/docs/link) capable of file uploads.
The link matches and extracts files in the GraphQL operation. If there are files it uses a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) instance as the [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) `options.body` to make a [GraphQL multipart request](https://github.com/jaydenseric/graphql-multipart-request-spec), otherwise it sends a regular POST request.
Some of the options are similar to the [`createHttpLink` options](https://www.apollographql.com/docs/link/links/http/#options).
| Parameter | Type | Description |
| :-- | :-- | :-- |
| `options` | object | Options. |
| `options.uri` | string? = /graphql | GraphQL endpoint URI. |
| `options.fetch` | Function? | [`fetch`](https://fetch.spec.whatwg.org) implementation to use, defaulting to the `fetch` global. |
| `options.fetchOptions` | [FetchOptions](#type-fetchoptions)? | `fetch` options; overridden by upload requirements. |
| `options.isExtractableFile` | [ExtractableFileMatcher](#type-extractablefilematcher)? = [isExtractableFile](#function-isextractablefile) | Customizes how files are matched in the GraphQL operation for extraction. |
| `options.FormData` | class? | [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) implementation to use, defaulting to the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) global. |
| `options.formDataAppendFile` | [FormDataFileAppender](#type-formdatafileappender)? = [formDataAppendFile](#function-formdataappendfile) | Customizes how extracted files are appended to the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) instance. |
| `options.fetch` | Function? | [`fetch`](https://fetch.spec.whatwg.org) implementation to use, defaulting to the [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) global. |
| `options.fetchOptions` | [FetchOptions](#type-fetchoptions)? | [`fetch` options](#type-fetchoptions); overridden by upload requirements. |
| `options.credentials` | string? | Overrides `options.fetchOptions.credentials`. |
| `options.headers` | object? | Merges with and overrides `options.fetchOptions.headers`. |
| `options.includeExtensions` | boolean? = `false` | Toggles sending `extensions` fields to the GraphQL server. |
Expand Down Expand Up @@ -186,6 +201,68 @@ _A basic Apollo Client setup._
---
### function formDataAppendFile
The default implementation for [`createUploadLink`](#function-createuploadlink) `options.formDataAppendFile` that uses the standard [`FormData.append`](https://developer.mozilla.org/en-US/docs/Web/API/FormData/append) method.
**Type:** [FormDataFileAppender](#type-formdatafileappender)
| Parameter | Type | Description |
| :-- | :-- | :-- |
| `formData` | FormData | [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) instance to append the specified file to. |
| `fieldName` | string | Field name for the file. |
| `file` | \* | File to append. |
---
### function isExtractableFile
The default implementation for [`createUploadLink`](#function-createuploadlink) `options.isExtractableFile`.
**Type:** [ExtractableFileMatcher](#type-extractablefilematcher)
| Parameter | Type | Description |
| :-------- | :--- | :-------------- |
| `value` | \* | Value to check. |
**Returns:** boolean — Is the value an extractable file.
#### See
- [`extract-files` `isExtractableFile` docs](https://github.com/jaydenseric/extract-files#function-isextractablefile).
---
### type ExtractableFileMatcher
A function that checks if a value is an extractable file.
**Type:** Function
| Parameter | Type | Description |
| :-------- | :--- | :-------------- |
| `value` | \* | Value to check. |
**Returns:** boolean — Is the value an extractable file.
#### See
- [`isExtractableFile`](#function-isextractablefile) has this type.
#### Examples
_How to check for the default exactable files, as well as a custom type of file._
> ```js
> const { isExtractableFile } = require('apollo-upload-client')
>
> const isExtractableFileEnhanced = value =>
> isExtractableFile(value) ||
> (typeof CustomFile !== 'undefined' && value instanceof CustomFile)
> ```
---
### type FetchOptions
GraphQL request `fetch` options.
Expand All @@ -203,9 +280,26 @@ GraphQL request `fetch` options.
---
### type FormDataFileAppender
Appends a file extracted from the GraphQL operation to the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) instance used as the [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) `options.body` for the [GraphQL multipart request](https://github.com/jaydenseric/graphql-multipart-request-spec).
| Parameter | Type | Description |
| :-- | :-- | :-- |
| `formData` | FormData | [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) instance to append the specified file to. |
| `fieldName` | string | Field name for the file. |
| `file` | \* | File to append. The file type depends on what the [`ExtractableFileMatcher`](#type-extractablefilematcher) extracts. |
#### See
- [`formDataAppendFile`](#function-formdataappendfile) has this type.
- [`createUploadLink`](#function-createuploadlink) accepts this type in `options.formDataAppendFile`.
---
### type ReactNativeFileSubstitute
A React Native [`File`](https://developer.mozilla.org/docs/web/api/file) substitute.
A React Native [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) substitute.
Be aware that inspecting network requests with Chrome dev tools interferes with the React Native `FormData` implementation, causing network errors.
Expand All @@ -219,7 +313,7 @@ Be aware that inspecting network requests with Chrome dev tools interferes with
#### See
- [`extract-files` docs](https://github.com/jaydenseric/extract-files#type-reactnativefilesubstitute).
- [`extract-files` `ReactNativeFileSubstitute` docs](https://github.com/jaydenseric/extract-files#type-reactnativefilesubstitute).
- [React Native `FormData` polyfill source](https://github.com/facebook/react-native/blob/v0.45.1/Libraries/Network/FormData.js#L34).
#### Examples
Expand Down
113 changes: 96 additions & 17 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,22 @@ const {
createSignalIfSupported,
parseAndCheckHttpResponse
} = require('apollo-link-http-common')
const { extractFiles, ReactNativeFile } = require('extract-files')
const {
extractFiles,
isExtractableFile,
ReactNativeFile
} = require('extract-files')

/**
* A React Native [`File`](https://developer.mozilla.org/docs/web/api/file)
* A React Native [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File)
* substitute.
*
* Be aware that inspecting network requests with Chrome dev tools interferes
* with the React Native `FormData` implementation, causing network errors.
* @kind typedef
* @name ReactNativeFileSubstitute
* @type {object}
* @see [`extract-files` docs](https://github.com/jaydenseric/extract-files#type-reactnativefilesubstitute).
* @see [`extract-files` `ReactNativeFileSubstitute` docs](https://github.com/jaydenseric/extract-files#type-reactnativefilesubstitute).
* @see [React Native `FormData` polyfill source](https://github.com/facebook/react-native/blob/v0.45.1/Libraries/Network/FormData.js#L34).
* @prop {string} uri Filesystem path.
* @prop {string} [name] File name.
Expand All @@ -36,14 +40,13 @@ const { extractFiles, ReactNativeFile } = require('extract-files')
*/

/**
* Used to mark a
* [React Native `File` substitute]{@link ReactNativeFileSubstitute}.
* It’s too risky to assume all objects with `uri`, `type` and `name` properties
* are files to extract. Re-exported from [`extract-files`](https://npm.im/extract-files)
* for convenience.
* Used to mark [React Native `File` substitutes]{@link ReactNativeFileSubstitute}
* as it’s too risky to assume all objects with `uri`, `type` and `name`
* properties are extractable files.
* @kind class
* @name ReactNativeFile
* @param {ReactNativeFileSubstitute} file A React Native [`File`](https://developer.mozilla.org/docs/web/api/file) substitute.
* @param {ReactNativeFileSubstitute} file A React Native [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) substitute.
* @see [`extract-files` `ReactNativeFile` docs](https://github.com/jaydenseric/extract-files#class-reactnativefile).
* @example <caption>A React Native file that can be used in query or mutation variables.</caption>
* ```js
* const { ReactNativeFile } = require('apollo-upload-client')
Expand All @@ -67,17 +70,90 @@ exports.ReactNativeFile = ReactNativeFile
* @prop {string} [credentials] Authentication credentials mode.
*/

/**
* A function that checks if a value is an extractable file.
* @kind typedef
* @name ExtractableFileMatcher
* @type {Function}
* @param {*} value Value to check.
* @returns {boolean} Is the value an extractable file.
* @see [`isExtractableFile`]{@link isExtractableFile} has this type.
* @example <caption>How to check for the default exactable files, as well as a custom type of file.</caption>
* ```js
* const { isExtractableFile } = require('apollo-upload-client')
*
* const isExtractableFileEnhanced = value =>
* isExtractableFile(value) ||
* (typeof CustomFile !== 'undefined' && value instanceof CustomFile)
* ```
*/

/**
* The default implementation for [`createUploadLink`]{@link createUploadLink}
* `options.isExtractableFile`.
* @kind function
* @name isExtractableFile
* @type {ExtractableFileMatcher}
* @param {*} value Value to check.
* @returns {boolean} Is the value an extractable file.
* @see [`extract-files` `isExtractableFile` docs](https://github.com/jaydenseric/extract-files#function-isextractablefile).
*/
exports.isExtractableFile = isExtractableFile

/**
* Appends a file extracted from the GraphQL operation to the
* [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
* instance used as the [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch)
* `options.body` for the [GraphQL multipart request](https://github.com/jaydenseric/graphql-multipart-request-spec).
* @kind typedef
* @name FormDataFileAppender
* @param {FormData} formData [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) instance to append the specified file to.
* @param {string} fieldName Field name for the file.
* @param {*} file File to append. The file type depends on what the [`ExtractableFileMatcher`]{@link ExtractableFileMatcher} extracts.
* @see [`formDataAppendFile`]{@link formDataAppendFile} has this type.
* @see [`createUploadLink`]{@link createUploadLink} accepts this type in `options.formDataAppendFile`.
*/

/**
* The default implementation for [`createUploadLink`]{@link createUploadLink}
* `options.formDataAppendFile` that uses the standard
* [`FormData.append`](https://developer.mozilla.org/en-US/docs/Web/API/FormData/append)
* method.
* @kind function
* @name formDataAppendFile
* @type {FormDataFileAppender}
* @param {FormData} formData [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) instance to append the specified file to.
* @param {string} fieldName Field name for the file.
* @param {*} file File to append.
*/
function formDataAppendFile(formData, fieldName, file) {
formData.append(fieldName, file, file.name)
}

exports.formDataAppendFile = formDataAppendFile

/**
* Creates a terminating [Apollo Link](https://apollographql.com/docs/link)
* capable of file uploads. Options match [`createHttpLink`](https://apollographql.com/docs/link/links/http#options).
* capable of file uploads.
*
* The link matches and extracts files in the GraphQL operation. If there are
* files it uses a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
* instance as the [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch)
* `options.body` to make a [GraphQL multipart request](https://github.com/jaydenseric/graphql-multipart-request-spec),
* otherwise it sends a regular POST request.
*
* Some of the options are similar to the [`createHttpLink` options](https://www.apollographql.com/docs/link/links/http/#options).
* @see [GraphQL multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
* @see [apollo-link on GitHub](https://github.com/apollographql/apollo-link).
* @kind function
* @name createUploadLink
* @param {object} options Options.
* @param {string} [options.uri=/graphql] GraphQL endpoint URI.
* @param {Function} [options.fetch] [`fetch`](https://fetch.spec.whatwg.org) implementation to use, defaulting to the `fetch` global.
* @param {FetchOptions} [options.fetchOptions] `fetch` options; overridden by upload requirements.
* @param {ExtractableFileMatcher} [options.isExtractableFile=isExtractableFile] Customizes how files are matched in the GraphQL operation for extraction.
* @param {class} [options.FormData] [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) implementation to use, defaulting to the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) global.
* @param {FormDataFileAppender} [options.formDataAppendFile=formDataAppendFile] Customizes how extracted files are appended to the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) instance.
* @param {Function} [options.fetch] [`fetch`](https://fetch.spec.whatwg.org) implementation to use, defaulting to the [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) global.
* @param {FetchOptions} [options.fetchOptions] [`fetch` options]{@link FetchOptions}; overridden by upload requirements.
* @param {string} [options.credentials] Overrides `options.fetchOptions.credentials`.
* @param {object} [options.headers] Merges with and overrides `options.fetchOptions.headers`.
* @param {boolean} [options.includeExtensions=false] Toggles sending `extensions` fields to the GraphQL server.
Expand All @@ -96,7 +172,10 @@ exports.ReactNativeFile = ReactNativeFile
*/
exports.createUploadLink = ({
uri: fetchUri = '/graphql',
fetch: linkFetch = fetch,
isExtractableFile: customIsExtractableFile = isExtractableFile,
FormData: CustomFormData = FormData,
formDataAppendFile: customFormDataAppendFile = formDataAppendFile,
fetch: customFetch = fetch,
fetchOptions,
credentials,
headers,
Expand Down Expand Up @@ -141,7 +220,7 @@ exports.createUploadLink = ({
contextConfig
)

const { clone, files } = extractFiles(body)
const { clone, files } = extractFiles(body, '', customIsExtractableFile)
const payload = serializeFetchParameter(clone, 'Payload')

if (files.size) {
Expand All @@ -151,7 +230,7 @@ exports.createUploadLink = ({
// GraphQL multipart request spec:
// https://github.com/jaydenseric/graphql-multipart-request-spec

const form = new FormData()
const form = new CustomFormData()

form.append('operations', payload)

Expand All @@ -164,7 +243,7 @@ exports.createUploadLink = ({

i = 0
files.forEach((paths, file) => {
form.append(++i, file, file.name)
customFormDataAppendFile(form, ++i, file)
})

options.body = form
Expand All @@ -183,7 +262,7 @@ exports.createUploadLink = ({
}
}

linkFetch(uri, options)
customFetch(uri, options)
.then(response => {
// Forward the response on the context.
operation.setContext({ response })
Expand Down

0 comments on commit e1bb6e7

Please sign in to comment.