Nexus: Basic File Uploading

Created on 26 May 2020  路  5Comments  路  Source: graphql-nexus/nexus

As there is no plugin for this, I've been trying to learn from other peoples comments and issues and such. But to no avail...

Basically I have no idea how this is working for everyone. Typescript is throwing errors everywhere. I am just looking to upload files to an s3 bucket. I have written the function for uploading to the s3 bucket, but I can't figure out how to receive the file on the server side to upload. I cannot believe how not straightforward this is - you would think this is a pretty basic use case...
Also, parse is not a function, so I'm not sure what I'm supposed to be using (as @israelglar did in the issue here).
Any help from anyone as to what exactly I'm missing would be GREATLY appreciated! I'm at a complete loss...

This is the error I'm getting when I upload the file:
In the network tab, this appears to be what's being sent:
operations: {"variables":{"file":null},"query":"mutation ($file: Upload) {\n uploadFile(file: $file)\n}\n"} map: {"1":["variables.file"]} 1: (binary)
And this is the response (error):
GraphQLError: Variable "$file" got invalid value {}; Upload value invalid.

BACKEND
server.ts

import './config'
import { GraphQLServer } from 'graphql-yoga'
import { permissions } from './permissions'
import { schema } from './schema'
import { createContext } from './context'

import * as jwt from 'jsonwebtoken'

const localdev = 'http://localhost:3000'

const admindev = 'http://localhost:3001'

var whitelist = [localdev, process.env.MYSQL_URL_FCA, admindev]

const options = {
  endpoint: '/',
  subscriptions: '/subscriptions',
  playground: '/playground',
  debug: true,
  tracing: true,
  allowExternalErrors: true,
  cors: {
    credentials: true,
    // origin: process.env.HATCHLI_URL,
    origin: whitelist,
  },
}

const server = new GraphQLServer({
  schema,
  context: createContext,
  middlewares: [permissions],
}).start(options, ({ port }) => {
  console.log(
    `Server started, listening on port ${port} for incoming requests.`,
  )
})

Upload.ts
```import { schema } from 'nexus'
import { objectType, extendType, scalarType } from '@nexus/schema'
import { GraphQLUpload } from 'graphql-upload'

export const Upload = scalarType({
name: GraphQLUpload.name,
asNexusMethod: 'upload', // We set this to be used as a method later as t.upload() if needed
description: GraphQLUpload.description,
serialize: GraphQLUpload.serialize,
parseValue: GraphQLUpload.parseValue,
parseLiteral: GraphQLUpload.parseLiteral,
})

`Mutation.ts`

import { schema } from 'nexus'
import { arg } from '@nexus/schema'

const readFS = (stream: {
on: (
arg0: string,
arg1: (data: any) => number,
) => { on: (arg0: string, arg1: () => void) => void }
}) => {
let chunkList: any[] | Uint8Array[] = []
return new Promise((resolve, reject) =>
stream
.on('data', (data) => chunkList.push(data))
.on('end', () => resolve(Buffer.concat(chunkList))),
)
}

export const Mutation = schema.mutationType({
definition(t) {
t.field('uploadFile', {
type: 'Boolean', // Or any other type that your resolver returns
args: {
file: arg({ type: 'Upload' }),
},
resolve: async (root, args, context) => {
const {
createReadStream,
filename,
mimetype,
encoding,
} = await args.file
if (!filename) {
throw Error('Invalid file Stream')
}
const ext = filename.split('.').pop()
const buf = await readFS(createReadStream())
const fileParsed = parse(buf.toString())
const fileData = fileParsed.data

    console.log(fileData)
    return fileData

    // Do things to fileData and return
  },
}),

... rest of my mutations
}})

FRONT END
`index.tsx`

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { theme } from "./theme";
import Routes from "./routes";
import * as serviceWorker from "./serviceWorker";
import "./theme/global.css";

import { setContext } from "@apollo/link-context";
import { Client as Styletron } from "styletron-engine-atomic";
import { Provider as StyletronProvider } from "styletron-react";
import { BaseProvider } from "baseui";
import { ApolloProvider, ApolloClient, InMemoryCache } from "@apollo/client";
const { createUploadLink } = require("apollo-upload-client");

const dev = "http://localhost:4000/";
const prod = "https://prisma2-graphql-yoga-shield.now.sh/";

const httpLink = createUploadLink({
uri: dev,
credentials: "include",
fetch,
});

const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem("token");
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? Bearer ${token} : "",
},
};
});

const client = new ApolloClient({
uri: process.env.REACT_APP_API_URL,
cache: new InMemoryCache(),
link: authLink.concat(httpLink),
});

function App() {
const engine = new Styletron();

return (






);
}

ReactDOM.render(, document.getElementById("root"));

`Customer Form.tsx`

import React, { useState, useCallback } from "react";
import { gql, useMutation, useQuery } from "@apollo/client";

const SINGLE_UPLOAD = gql mutation($file: Upload) { uploadFile(file: $file) } ;
const AddCustomer: React.FC = (props) => {
const [uploadNewFile, { loading, error }] = useMutation(SINGLE_UPLOAD);
function onChange({
target: {
validity,
files: [file],
},
}) {
if (validity.valid) uploadNewFile({ variables: { file } });
}
return (
type="file"
name="file"
// disabled={!uploadFileName}
inputRef={register}
onChange={onChange}
/>
)
}
export default AddCustomer;
```

notuser-resolved typdiscussion

Most helpful comment

@Maetes

This was the most ridiculous shit I had dealt with. Parse was a red herring. The biggest thing was getting off of the Yoga server and using Apollo server express. Then figuring out how to make the Upload type from Nexus horrific documentation. But it works now.

My mutation (from Mutation.ts):

definition(t) {
    t.field('uploadFile', {
      type: 'UploadFile',
      args: {
        file: arg({ type: 'Upload' }),
      },
      resolve: async (_: any, { file }, {}: any) => {
        const { createReadStream, filename, mimetype, encoding } = await file
        const uploadedFile = await uploadFile(createReadStream, filename)
        return {
          filename,
          uri: uploadedFile,
        }
      },
    }),

the uploadFile function:

export const uploadFile = async (createReadStream: any, filename: any) => {
  aws.config.update({
    region: 'us-east-1',
    accessKeyId: process.env.AWS_ACCESS_KEY_ID
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  })

  // Create S3 service object
  const s3 = new aws.S3({ apiVersion: '2006-03-01' })

  const { Location } = await s3
    .upload({
      Body: createReadStream(),
      Key: `${uuidv4()}${extname(filename)}`,
      Bucket: process.env.AWS_S3_BUCKET as string,
      ACL: 'public-read',
    })
    .promise()

  console.log(Location)

  return Location
}

And the Upload.ts type:

// @ts-nocheck
import { asNexusMethod, objectType } from '@nexus/schema'
import { GraphQLUpload } from 'apollo-server-express'

export const Upload = asNexusMethod(GraphQLUpload, 'upload')

export const UploadFile = objectType({
  name: 'UploadFile',
  definition(t) {
    t.string('uri')
    t.string('filename')
  },
})

All 5 comments

I was searching this problem for days now. Firstly I was sure, that its frontend problem, but it seems something wrong with scalar type declaration.

Solution from this stackoverflow post worked:

import { objectType, scalarType } from "@nexus/schema";
import { GraphQLError } from "graphql";
import * as FileType from "file-type";

export const Upload = scalarType({
  name: "Upload",
  asNexusMethod: "upload", // We set this to be used as a method later as `t.upload()` if needed
  description: "desc",
  serialize: () => {
    throw new GraphQLError("Upload serialization unsupported.");
  },
  parseValue: async (value) => {
    const upload = await value;
    const stream = upload.createReadStream();
    const fileType = await FileType.fromStream(stream);

    if (fileType?.mime !== upload.mimetype)
      throw new GraphQLError("Mime type does not match file content.");

    return upload;
  },
  parseLiteral: (ast) => {
    throw new GraphQLError("Upload literal unsupported.", ast);
  },
});

But personally I have no idea why your code didn't it worked >.<

I'm also looking to solve this issue for me but i'm stucking on the "parse". @hatchli do you figgured out what this function is or what needs to be imported?

@Maetes

This was the most ridiculous shit I had dealt with. Parse was a red herring. The biggest thing was getting off of the Yoga server and using Apollo server express. Then figuring out how to make the Upload type from Nexus horrific documentation. But it works now.

My mutation (from Mutation.ts):

definition(t) {
    t.field('uploadFile', {
      type: 'UploadFile',
      args: {
        file: arg({ type: 'Upload' }),
      },
      resolve: async (_: any, { file }, {}: any) => {
        const { createReadStream, filename, mimetype, encoding } = await file
        const uploadedFile = await uploadFile(createReadStream, filename)
        return {
          filename,
          uri: uploadedFile,
        }
      },
    }),

the uploadFile function:

export const uploadFile = async (createReadStream: any, filename: any) => {
  aws.config.update({
    region: 'us-east-1',
    accessKeyId: process.env.AWS_ACCESS_KEY_ID
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  })

  // Create S3 service object
  const s3 = new aws.S3({ apiVersion: '2006-03-01' })

  const { Location } = await s3
    .upload({
      Body: createReadStream(),
      Key: `${uuidv4()}${extname(filename)}`,
      Bucket: process.env.AWS_S3_BUCKET as string,
      ACL: 'public-read',
    })
    .promise()

  console.log(Location)

  return Location
}

And the Upload.ts type:

// @ts-nocheck
import { asNexusMethod, objectType } from '@nexus/schema'
import { GraphQLUpload } from 'apollo-server-express'

export const Upload = asNexusMethod(GraphQLUpload, 'upload')

export const UploadFile = objectType({
  name: 'UploadFile',
  definition(t) {
    t.string('uri')
    t.string('filename')
  },
})

@hatchli thank you very much this helps beside of the aws implementation because i want to implement it with local file system storage. But no problem thank you very much. I want to mention that the nexus framework as of comments in this repo is supporting file uploads now by default cause of switchtig to apollo-server. Would also be happy to see an implementation there.

I feel this is out of scope for this package, please feel free to open the issue on the nexus framework package.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

saostad picture saostad  路  3Comments

santialbo picture santialbo  路  5Comments

ecwyne picture ecwyne  路  6Comments

MathiasKandelborg picture MathiasKandelborg  路  5Comments

DregondRahl picture DregondRahl  路  3Comments