Nexus: How to implement mutations using GraphQL multipart request spec

Created on 2 May 2019  Â·  4Comments  Â·  Source: graphql-nexus/nexus

Please add some examples on how create mutations using GraphQL multipart request spec and apollo-upload-server with Nexus?

Most helpful comment

In our project we're are doing this way:

First we create a scalar:

import {scalarType} from 'nexus';
const GraphQLUpload = require('graphql-upload').GraphQLUpload;

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,
});

And I use it in a mutation like this:

  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))),
    );
  };

  const Mutation = objectType({
    name: 'Mutation',
    definition(t) {
      t.field('importProfiles', {
        type: 'String', // 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 NotValidError(filename, ['filename'], 'Invalid file Stream');
          }
          const ext = filename.split('.').pop();
          if (ext !== 'csv') {
            throw NotValidError(
              filename,
              ['filename'],
              'File not valid, must be a .csv file',
            );
          }
          const buf = await readFS(createReadStream());
          const fileParsed = parse(buf.toString());
          const fileData = fileParsed.data;

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

In our case we are expecting a .csv file.
I think that we still need some more error handling but it's working so far.

Hope that helps :)

All 4 comments

In our project we're are doing this way:

First we create a scalar:

import {scalarType} from 'nexus';
const GraphQLUpload = require('graphql-upload').GraphQLUpload;

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,
});

And I use it in a mutation like this:

  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))),
    );
  };

  const Mutation = objectType({
    name: 'Mutation',
    definition(t) {
      t.field('importProfiles', {
        type: 'String', // 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 NotValidError(filename, ['filename'], 'Invalid file Stream');
          }
          const ext = filename.split('.').pop();
          if (ext !== 'csv') {
            throw NotValidError(
              filename,
              ['filename'],
              'File not valid, must be a .csv file',
            );
          }
          const buf = await readFS(createReadStream());
          const fileParsed = parse(buf.toString());
          const fileData = fileParsed.data;

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

In our case we are expecting a .csv file.
I think that we still need some more error handling but it's working so far.

Hope that helps :)

If you're using the latest version of apollo-server-express, then it comes with GraphQLUpload scalar type — no need to include a separate package.

Also, Nexus provides a helper called asNexusMethod that you can use to implement something like:

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

That export adds the Upload scalar that you can use as an argument type.

Thanks @israelglar, @delianides ! Your comments helped me a lot

I realize this is a closed issue, but since I borrowed so heavily from @bizmedia, @delianides and @israelglar I figured I'd post here. I'll make a new issue as well.
But 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 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).
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;
```

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ryands17 picture ryands17  Â·  4Comments

MathiasKandelborg picture MathiasKandelborg  Â·  5Comments

capaj picture capaj  Â·  5Comments

chrisdrackett picture chrisdrackett  Â·  3Comments

StevenLangbroek picture StevenLangbroek  Â·  6Comments