多分絶対誰も必要ないけど、node環境からapollo clientを使ってfileをアップロードする方法

# 注意

fetchのpolyfillとして[node-fetch][3]を使っていると（next.jsのapi routesは使ってる）

> Note: when body is a Stream, Content-Length is not set automatically.

なので、`fs.createReadStream`なファイルを渡すと、結果としてcontent-lengthがheaders情報としてサーバー側に渡されない。 サーバーによってはエラーの原因になると思う。

puma v4.3.7未満はcontent-lengthが無いとPOSTデータを上手く処理できない様子なので、うまくサーバーにデータが行かない場合はpumaのバージョンを確認したほうが良い。

[Backport set CONTENT_LENGTH for chunked requests][4]

# 問題

- apollo clientを使っている
- apollo-upload-clientを使っている
- node環境から使っている（nextjsのapi routesとか）

この場合、clientと同じようなコードを書いてもアップロードが機能しない。

# 解決方法

node環境では

- Fileがない
- Blobがない
- FormDataがない
- fetchがない（[nextjsならpolyfillしてる][1]けど）

なので、apollo-upload-clientのcreateUploadLinkを使う際にこれらのnode版を指定してやる。

そしてFile、Blobがないのでfsでファイルをstreamとして読み込み、それをアップロード対象のオブジェクトとするが、デフォルトではFileかBlobしか対象になっていないので、ReadStreamが対象になるように`isExtractableFile`で設定し直す。

```tsx
import FormData from "form-data"  // yarn add form-data
import fetch from "isomorphic-unfetch" // yarn add isomorphic-unfetch
import { ReadStream } from "fs"
import { createUploadLink, isExtractableFile } from "apollo-upload-client"

createUploadLink({
  uri: "https://backend/graphql",
  fetch,
  FormData,  
  isExtractableFile: (value) =>
    isExtractableFile(value) ||
    (typeof ReadStream !== "undefined" && value instanceof ReadStream),
})
```


```tsx
import { ApolloClient, InMemoryCache } from "@apollo/client"
import { createUploadLink, isExtractableFile } from "apollo-upload-client"
import FormData from "form-data"
import fs, { ReadStream } from "fs"
import type { NextApiRequest, NextApiResponse } from "next"
import * as Generated from "src/generated/graphql" // @graphql-codegenを使っているので...

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: createUploadLink({
    uri: "https://backend/graphql", // 他のgraphqlサーバー
    // fetch, nextjsを使っているので必要ない
    FormData,  
    isExtractableFile: (value) =>
      isExtractableFile(value) ||
      (typeof ReadStream !== "undefined" && value instanceof ReadStream),
  }),
})

export default async (req: NextApiRequest, res: NextApiResponse) => {
  const file = fs.createReadStream(fs.realpathSync("./test.jpg"))

  await client.mutate<
    Generated.TestMutationMutation,
    Generated.TestMutationMutationVariables
  >({
    mutation: Generated.TestMutationDocument,
    variables: {
      input: {
        file,
      },
    },
    fetchPolicy: "no-cache",
  })

  res.status(200).end()
}
```

```json:package.json
{
  "scripts": {
    "dev": "next",
    "generate": "graphql-codegen -w --config codegen.yml"
  },
  "dependencies": {
    "@apollo/client": "^3.4.16",
    "apollo-upload-client": "^16.0.0",
    "form-data": "^4.0.0",
    "graphql": "^15.6.1",
    "next": "^11.1.2",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  },
  "devDependencies": {
    "@graphql-codegen/cli": "^2.2.1",
    "@graphql-codegen/typescript": "^2.2.4",
    "@graphql-codegen/typescript-operations": "^2.1.8",
    "@graphql-codegen/typescript-react-apollo": "^3.1.6",
    "@types/react": "^17.0.30",
    "typescript": "^4.4.4"
  }
}
```

[1]: https://nextjs.org/docs/basic-features/supported-browsers-features#server-side-polyfills
[2]: https://github.com/jaydenseric/extract-files/blob/6d8f725e9b9973fd564bfdd784cd9b2ff58f8470/public/isExtractableFile.js#L31
[3]: https://github.com/node-fetch/node-fetch
[4]: https://github.com/puma/puma/blob/master/History.md#437--2020-11-30