Please add some examples on how create mutations using GraphQL multipart request spec and apollo-upload-server with Nexus?
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(
`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
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;
```
Most helpful comment
In our project we're are doing this way:
First we create a scalar:
And I use it in a mutation like this:
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 :)