Uploading a "LOCAL" file from nodejs to rails graphql backend (server to server)

702 views Asked by At

This is not about "client to server" upload.

I am looking for a way to upload a file from nodejs server to rails backend graphql server (server to server).
The rails backend is working fine, and it can take file uploads without a problem if it was from a client.

The problem I am having is to create/build a "File" object in nodejs, so that I can use an Apollo mutation like this to upload the file.

// nodejs server side, NOT react client side
const { data } = await client.mutate<
FileUploadMutation,
FileUploadMutationVariables
>({
  mutation: FileUploadDocument,
  variables: {
    input: {
      file: file // how do I create this file object from a local file?
    },
  }
})

If it was the client side, it's very simple.

<input type="file" onchange={(event) => {
  const file = event.target.files[0]  // this can be the "file" object
}}/>

Is it possible to create an equivalent file object as the client side File object in nodejs?

import fs from "fs"

// giving this "file" object to the mutation doen't upload the file
const file = fs.createReadStream("./local/file/path.jpg")

Things I tried

  • created a new rails graphql backend with apollo_upload_server
  • created a new nextjs
  • posting a file working on the server-end (Altair GraphQL Client)
    • I see the file as <ActionDispatch::Http::UploadedFile:0x00007f9e1ac0e8a0 @tempfile=#<Tempfile:/tmp/RackMultipart20211020-14-156qwls.jpg ...
  • created a Blob object, but I get this error Variable $input of type TestMutationInput! was provided invalid value for file ({} is not a valid upload) on the front-end

package.json

{
  "scripts": {
    "dev": "next",
    "generate": "graphql-codegen -w --config codegen.yml"
  },
  "dependencies": {
    "@apollo/client": "^3.4.16",
    "apollo-upload-client": "^16.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/apollo-upload-client": "^14.1.0",
    "@types/react": "^17.0.30",
    "typescript": "^4.4.4"
  }
}

(nextjs)/api/test.ts (nodejs)

import { ApolloClient, InMemoryCache } from "@apollo/client"
import { createUploadLink } from "apollo-upload-client"
import { Blob } from "buffer"
import type { NextApiRequest, NextApiResponse } from "next"
// import { Blob } from "node:buffer"
import * as Generated from "src/generated/graphql"

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: createUploadLink({
    uri: "http://backend:3000/graphql",
  }),
})

// checking if the apollo connection is good at all ------------
client
  .query<Generated.TestFieldQuery, Generated.TestFieldQueryVariables>({
    query: Generated.TestFieldDocument,
    variables: {},
  })
  .then(({ data }) => {
    // this actually returns data
    console.log("data:", data)
  })
// checking if the apollo connection is good at all ------------


export default async (req: NextApiRequest, res: NextApiResponse) => {
  // const buffer = fs.readFileSync(fs.realpathSync("./test.jpg"))
  // const file = new Blob([buffer], { type: "image/jpg" })
  const file = new Blob(["abc123"], { type: "text/plain" })
  console.log("file:", file)

// this mutation gives me this error
//   graphQLErrors: [
//    {
//      message: 'Variable $input of type TestMutationInput! was provided invalid value for file ({} is not a valid upload)',
//      locations: [Array],
//      extensions: [Object]
//    }
//  ],
  await client.mutate<
    Generated.TestMutationMutation,
    Generated.TestMutationMutationVariables
  >({
    mutation: Generated.TestMutationDocument,
    variables: {
      input: {
        file,
      },
    },
    fetchPolicy: "no-cache",
  })

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

There are 1 answers

0
kukrt On

I nailed it.

https://github.com/jaydenseric/apollo-upload-client/issues/272

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"

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: createUploadLink({
    uri: "https://backend/graphql",
    // https://nextjs.org/docs/basic-features/supported-browsers-features#server-side-polyfills
    // nextjs already polyfills this
    // fetch: ...
    // yarn add form-data or in my case @graphql-codegen had it
    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()
}