Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add isExtractableFile, appendFile, FormData options #179

Merged
merged 5 commits into from
Mar 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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