Troubles uploading files with Graphql - node.js

I'm using graphql and trying to upload file to the localhost server from react.
I was following this tutorial from apollo server official documentation: Enabling file uploads in Apollo Server
For some reason, image is corrupt (its empty) and I don't know how to fix it. I've tried number of things with no luck what so ever.
At this point, I'm desperate for any workaround or solution.
typedefs
const {gql} = require('apollo-server')
module.exports = gql`
type File {
url: String!
filename: String!
mimetype: String!
encoding: String!
}
type Mutation {
uploadFile(file: Upload!): File!
}
`
resolvers
const fs = require("fs");
const path = require("path");
module.exports = {
Query: {
test: () => 'Test completed',
files: () => files
},
Mutation: {
uploadFile: async (parent, {file}) => {
const {createReadStream, filename, mimetype, encoding} = await file
const stream = await createReadStream()
const pathName = path.join(__dirname, `../../public/images/${filename}`)
await stream.pipe(fs.createWriteStream(pathName))
return {
filename,
mimetype,
encoding,
url: `http://locahost:5000/images/${filename}`}
},
}
}
Apollo client setup
import React from 'react'
import App from './App'
import ApolloClient from 'apollo-client'
import {InMemoryCache} from 'apollo-cache-inmemory'
import {createUploadLink} from 'apollo-upload-client'
import {ApolloProvider} from '#apollo/react-hooks'
import {setContext} from 'apollo-link-context'
const httpLink = createUploadLink({
uri: 'http://localhost:5000/graphql/'
})
const authLink = setContext(() => {
const token = localStorage.getItem('jwtToken')
return {
headers: {
authorization: token ? `Bearer ${token}` : ''
}
}
})
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
})
export default (
<ApolloProvider client={client}>
<App/>
</ApolloProvider>
)
React code itself
import React from 'react'
import {useMutation, gql} from '#apollo/client'
const UPLOAD_FILE = gql`
mutation uploadFile($file: Upload!) {
uploadFile(file: $file) {
url
mimetype
filename
encoding
}
}
`
function UploadForm() {
const [uploadFile] = useMutation(UPLOAD_FILE, {
onCompleted: data => console.log(data)
})
const handleFileChange = e => {
const file = e.target.files[0]
if(!file) return
uploadFile({variables: {file}})
}
return (
<div>
<h2>Upload file</h2>
<input type='file' onChange={handleFileChange}/>
</div>
)
}
export default UploadForm
console output
uploadFile: {…}
​​
__typename: "File"
​​
encoding: "7bit"
​​
filename: "91f29c99.jpg"
​​
mimetype: "image/jpeg"
​​
url: "http://locahost:5000/images/91f29c99.jpg"
UPD: I've tried this approach. Now sometimes I get non-empty images, but they're still corrupt and unreadable. But most of the times its still empty.
uploadFile: async (_, {file}) => {
const {createReadStream, filename} = await file;
await new Promise((resolve, reject) =>
createReadStream()
.pipe(fs.createWriteStream(path.join(__dirname, `../../public/images/${filename}`))
.on("finish", () => resolve())
.on("error", reject))
)
return {
url
}
},

Related

Generate PDF in NodeJS and open in React with react-pdf

I'm generating a PDF file using html-pdf-node in my NodeJS backend.
exports.generatePDF = async (req, res, next) => {
const data = req.body || req.body.data;
const content = data.html;
let options = {
format: 'letter',
margin: {
right: '40px',
left: '40px'
}
};
let file = { content };
html_to_pdf.generatePdf(file, options).then(pdfBuffer => {
res.setHeader('Content-Length', pdfBuffer.length);
res.setHeader('Content-Type', 'application/pdf');
return res.end(pdfBuffer);
});
}
Then, I'm trying to open the generated PDF in React using react-pdf.
import React, { useState, useEffect } from 'react';
import { Document } from 'react-pdf';
import * as helperAPI from '../../api/Helper'; // This is just a helper for the axios request to NodeJS
const PDFViewer = (props) => {
//const html = props.html;
const html = "<html><body><div><p>This is a Test</p></div></body></html>";
const [ pdfLink, setPDFLink ] = useState(null);
useEffect(() => {
async function fetchData() {
const res = await helperAPI.generatePDF({html});
const file = new Blob(
[res],
{type: 'application/pdf'}
);
const fileURL = URL.createObjectURL(file);
setPDFLink(fileURL);
}
fetchData();
}, [html]);
const onDocumentLoadSuccess = () => {
console.log('onLoad');
console.log(pdfLink);
}
console.log('Link: ' + pdfLink);
return (
<div>
<Document
file={{
url: pdfLink,
}}
onLoadSuccess={onDocumentLoadSuccess}
/>
</div>
);
}
export default PDFViewer;
The axios request inside the helperAPI:
import axios from 'axios';
import * as url from '../Constants/Custom';
export const generatePDF = async (data) => {
let output = "";
await axios.post(`${url.REQ_URL}/helper/generatePDF`, data)
.then((res) => {
output = res.data;
});
return output;
};
When I run the code, the browser returns: Failed to load PDF file.
How can I load a blob generated file from NodeJS in react-pdf?

File upload from React Native ( expo ) to Node ( multer )

How can I upload a file( pdf, docs etc) from React Native using expo to the server using node. I've seen many examples for images using the expo image-picker api but I've come across none that uses document-picker or filesystem apis from expo. The expo file system documentation was a little hard to interpret for a beginner like me.
Thanks for the help. I was able to come up with a solution and I'll post it below so it can be of some use to whoever comes here in the future.
React Native
import React, { useState } from 'react';
import { Button, View } from 'react-native';
import * as DocumentPicker from 'expo-document-picker';
import * as FileSystem from 'expo-file-system';
const DocPicker = () => {
const [ doc, setDoc ] = useState();
const pickDocument = async () => {
let result = await DocumentPicker.getDocumentAsync({ type: "*/*", copyToCacheDirectory: true }).then(response => {
if (response.type == 'success') {
let { name, size, uri } = response;
let nameParts = name.split('.');
let fileType = nameParts[nameParts.length - 1];
var fileToUpload = {
name: name,
size: size,
uri: uri,
type: "application/" + fileType
};
console.log(fileToUpload, '...............file')
setDoc(fileToUpload);
}
});
// console.log(result);
console.log("Doc: " + doc.uri);
}
const postDocument = () => {
const url = "http://192.168.10.107:8000/upload";
const fileUri = doc.uri;
const formData = new FormData();
formData.append('document', doc);
const options = {
method: 'POST',
body: formData,
headers: {
Accept: 'application/json',
'Content-Type': 'multipart/form-data',
},
};
console.log(formData);
fetch(url, options).catch((error) => console.log(error));
}
return (
<View>
<Button title="Select Document" onPress={pickDocument} />
<Button title="Upload" onPress={postDocument} />
</View>
)
};
export default DocPicker;
Node.js
const express = require('express')
const bodyParser = require('body-parser')
var multer = require('multer')
var upload = multer({ dest: 'uploads/' })
const app = express()
const fs = require('fs')
const http = require('http')
const port = 8000
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.get('/', (req,res) => {
res.json({
success: true
})
})
app.post('/', (req, res) => {
console.log(req.body)
res.status(200)
})
app.post('/upload', upload.single('document'),(req , res) => {
console.log(req.file, req.body)
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
Cheers!!!
If the solution given by #Anandhu doesn't work then try the above code like this.
import React, { useState } from 'react';
import { Button, View } from 'react-native';
import * as DocumentPicker from 'expo-document-picker';
import * as FileSystem from 'expo-file-system';
const DocPicker = () => {
const [ doc, setDoc ] = useState();
const pickDocument = async () => {
let result = await DocumentPicker.getDocumentAsync({
type: "*/*",
copyToCacheDirectory: true })
.then(response => {
if (response.type == 'success') {
let { name, size, uri } = response;
/ ------------------------/
if (Platform.OS === "android" && uri[0] === "/") {
uri = `file://${uri}`;
uri = uri.replace(/%/g, "%25");
}
/ ------------------------/
let nameParts = name.split('.');
let fileType = nameParts[nameParts.length - 1];
var fileToUpload = {
name: name,
size: size,
uri: uri,
type: "application/" + fileType
};
console.log(fileToUpload, '...............file')
setDoc(fileToUpload);
}
});
// console.log(result);
console.log("Doc: " + doc.uri);
}
const postDocument = () => {
const url = "http://192.168.10.107:8000/upload";
const fileUri = doc.uri;
const formData = new FormData();
formData.append('document', doc);
const options = {
method: 'POST',
body: formData,
headers: {
Accept: 'application/json',
'Content-Type': 'multipart/form-data',
},
};
console.log(formData);
fetch(url, options).catch((error) => console.log(error));
}
return (
<View>
<Button title="Select Document" onPress={pickDocument} />
<Button title="Upload" onPress={postDocument} />
</View>
)
};
export default DocPicker;
There is a bug in the way the path was encoded, and the file:// scheme is missing.
This bug may be fixed in next release.
Try this Uploading pictures,documents and videos from your phone in your app with React Native, Expo
Here is an example, which also uses multer and express on the backend: https://github.com/expo/examples/tree/master/with-formdata-image-upload
That said, I'd recommend using FileSystem.uploadAsync instead of fetch and the background sessionType in order to support uploads while the app is backgrounded on iOS.

Variable "$picture" of required type "Upload!" was not provided

I am trying to upload file with apollo-upload-client from Next.js app to Node.js back end using graphql.
My Apollo client config
const createClient = (ctx: NextPageContext) =>
new ApolloClient({
credentials: "include",
headers: {
cookie:
(typeof window === "undefined"
? ctx?.req?.headers.cookie
: undefined) || "",
},
cache: new InMemoryCache(),
link: createUploadLink({uri:'http://localhost:4000/graphql', credentials:"include"})
});
My express resolver with TypeORM
#Resolver()
export class ProfilePictureResolver {
#Mutation(() => Boolean)
async addProfilePicture(#Arg("picture", () => GraphQLUpload)
{
createReadStream,
filename
}: Upload): Promise<boolean> {
return new Promise(async (resolve, reject) =>
createReadStream()
.pipe(createWriteStream(__dirname + `/../../../images/${filename}`))
.on("finish", () => resolve(true))
.on("error", () => reject(false))
);
}
}
Page
const Profile:React.FC<IProps> = () => {
const user = useSelector(state => state.user);
const [file, setFileToUpload] = useState(null);
const [mutate, {loading}] = useMutation(UPLOAD_IMAGE_MUTATION);
function onChange({
target: {
validity,
files: [file],
},
}) {
if (validity.valid) mutate({ variables: { picture: file } });
}
const onSubmit = async (e) =>{
e.preventDefault();
console.log(file)
const response = await mutate({
variables: {picture: file}
});
}
return (
<Box mt={20} pl={30} pr={30}>
<Header>
Edit Profile
</Header>
<input onChange={onChange} type="file" placeholder="photo" />
<button onClick={(e)=>onSubmit(e)}>Submit</button>
</Box>
)
};
FormData
------WebKitFormBoundary3bcoEmOQWM0eUhCG Content-Disposition: form-data; name="operations"
{"operationName":"addProfilePicture","variables":{"picture":null},"query":"mutation
addProfilePicture($picture: Upload!) {\n addProfilePicture(picture:
$picture)\n}\n"}
------WebKitFormBoundary3bcoEmOQWM0eUhCG Content-Disposition: form-data; name="map"
{"1":["variables.picture"]}
------WebKitFormBoundary3bcoEmOQWM0eUhCG Content-Disposition: form-data; name="1";
filename="73387406_149357266327075_4919835380817576193_n.jpg"
Content-Type: image/jpeg
------WebKitFormBoundary3bcoEmOQWM0eUhCG--
Before call mutation in console I see that file is present. What am I doing wrong?
Update:
I fixed mutation on the client side, changed file on picture:file.
Now I have another error:
Variable "$picture" got invalid value {}; Upload value invalid.
Update 2:
Here is what my query looks like, might be of help
js export const UPLOAD_IMAGE_MUTATION = gql` mutation addProfilePicture($picture: Upload!) { addProfilePicture(picture: $picture) } `;
SOLUTION
I figured that indeed there was something wrong with my apollo server resolver
I've change the old resolver (see above) on this one:
uploadFile: async (_, { file }) => {
const { createReadStream, filename } = await file;
await new Promise(res =>
createReadStream()
.pipe(createWriteStream(path.join(__dirname, "../images", filename)))
.on("close", res)
);
files.push(filename);
return true;
},
So as you see I dont use graphql upload anymore, and also I've changed my client mutation back to
mutate({ variables: { file } }
And new mutation query:
mutation UploadFile($file: Upload!) {
uploadFile(file: $file)
}
Now it works like a charm. Case closed.
As the error indicates, your operation includes a variable named $picture, but you have not provided such a variable. When calling mutate, the only variable you're providing is one named $file. You should instead do:
const response = await mutate({
variables: { picture: file },
});
I figured that indeed there was something wrong with my apollo server resolver
I've change the old resolver from the question on this one:
uploadFile: async (_, { file }) => {
const { createReadStream, filename } = await file;
await new Promise(res =>
createReadStream()
.pipe(createWriteStream(path.join(__dirname, "../images", filename)))
.on("close", res)
);
files.push(filename);
return true;
},
So as you see I dont use graphql upload anymore, and also I've changed my client mutation back to
mutate({ variables: { file } }
And new mutation query:
mutation UploadFile($file: Upload!) {
uploadFile(file: $file)
}
Now it works like a charm.

NodeJS create and POST file with GQL

I've been unable to figure out in NodeJS how to:
create a "file" in memory from a raw string; and,
how to POST that data to another server that expects a multipart/form-data payload.
Seems you cannot use the Blob or File classes in NodeJS.
I've read the pattern should be to use the Buffer class.
I still cannot get it to work with Buffers.
My GQL Datasoruce class looks something like:
const { RESTDataSource } = require('apollo-datasource-rest');
const FormData = require('form-data');
export default class MyDatasource extends RESTDataSource {
async postFileToServer({ string }) {
const inMemoryFile = Buffer.from(string, 'utf-8');
const myForm = new FormData();
myForm.append('file', inMemoryFile, 'file.txt');
const url = 'http://examnple.com';
const opts = { headers: { 'Content-Type': 'multipart/form-data' } };
return await this.post(url, myForm, opts);
}
}
The endpoint I want to hit works fine when I use Postman to make the API call with a file from my local machine. However, I need the GQL server to create the file from a raw string to afterwards call the example.com endpoint that is expecting a multipart/form-data.
The above example code always gives me an error of Status 400 and SyntaxError: Unexpected token - in JSON at position 0
File upload works for me using the apollo-datasource-rest package. Here is an example:
server.ts:
import { ApolloServer, gql } from 'apollo-server';
import MyDatasource from './datasource';
const typeDefs = gql`
type Query {
dummy: String
}
type Mutation {
upload: String
}
`;
const resolvers = {
Mutation: {
upload(_, __, { dataSources }) {
return dataSources.uploadAPI.postFileToServer({ str: '1234' });
},
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => {
return {
uploadAPI: new MyDatasource(),
};
},
});
const port = 3001;
server.listen(port).then(({ url }) => console.log(`🚀 Server ready at ${url}`));
datasource.ts:
import { RESTDataSource } from 'apollo-datasource-rest';
import FormData from 'form-data';
export default class MyDatasource extends RESTDataSource {
public async postFileToServer({ str }) {
const inMemoryFile = Buffer.from(str, 'utf-8');
const myForm = new FormData();
myForm.append('file', inMemoryFile, 'file.txt');
const url = 'http://localhost:3000/upload';
return this.post(url, myForm);
}
}
uploadServer.ts:
import multer from 'multer';
import express from 'express';
import path from 'path';
const upload = multer({ dest: path.resolve(__dirname, 'uploads/') });
const app = express();
const port = 3000;
app.post('/upload', upload.single('file'), (req, res) => {
console.log(req.file);
console.log(req.body);
res.sendStatus(200);
});
app.listen(port, () => {
console.log(`upload server is listening on http://localhost:${port}`);
});
The logs printed in the controller of /upload API:
{
fieldname: 'file',
originalname: 'file.txt',
encoding: '7bit',
mimetype: 'text/plain',
destination: '/Users/ldu020/workspace/github.com/mrdulin/apollo-graphql-tutorial/src/stackoverflow/63181608/uploads',
filename: '3cba4dded6089479ad495e2fb2daac21',
path: '/Users/ldu020/workspace/github.com/mrdulin/apollo-graphql-tutorial/src/stackoverflow/63181608/uploads/3cba4dded6089479ad495e2fb2daac21',
size: 4
}
[Object: null prototype] {}
source code: https://github.com/mrdulin/apollo-graphql-tutorial/tree/master/src/stackoverflow/63181608

Using loopback4 and graphQL together

import {Lb4Application} from './application';
import {ApplicationConfig} from '#loopback/core';
import graphqlHTTP from 'express-graphql';
import {createGraphQLSchema} from 'openapi-to-graphql';
import {Oas3} from 'openapi-to-graphql/lib/types/oas3';
export {Lb4Application};
export async function main(options: ApplicationConfig = {}) {
const app = new Lb4Application(options);
await app.boot();
await app.start();
const url: string = <string>app.restServer.url;
console.log(`REST API Server is running at ${url}`);
const graphqlPath = '/graphql';
const oas: Oas3 = <Oas3>(<unknown>app.restServer.getApiSpec());
const {schema} = await createGraphQLSchema(oas, {
strict: false,
viewer: false,
baseUrl: url,
headers: {
'X-Origin': 'GraphQL',
},
tokenJSONpath: '$.jwt',
});
const handler: graphqlHTTP.Middleware = graphqlHTTP(
(request, response, graphQLParams) => ({
schema,
pretty: true,
graphiql: true,
context: {jwt: getJwt(request)},
}),
);
// Get the jwt from the Authorization header and place in context.jwt, which is then referenced in tokenJSONpath
function getJwt(req: any) {
if (req.headers && req.headers.authorization) {
return req.headers.authorization.replace(/^Bearer /, '');
}
}
app.mountExpressRouter(graphqlPath, handler);
console.log(`Graphql API: ${url}${graphqlPath}`);
return app;
}
I have taken this code from this github issue, and I still cannot seem to get it to run.
The error is get is
Error: Invalid specification provided
When i just use an express server, and run npx openapi-to-graphql --port=3001 http://localhost:3000/openapi.json --fillEmptyResponses The graphql is served correctly.
I need to get the example code running, as it seems to be the only way to pass JWT token headers correctly when using loopback4 and graphql together
This is how i solved it for anyone that needs help:
/* eslint-disable #typescript-eslint/no-explicit-any */
import {Lb4GraphqlPocApplication} from './application';
import {ApplicationConfig} from '#loopback/core';
const graphqlHTTP = require('express-graphql');
const {createGraphQLSchema} = require('openapi-to-graphql');
const fetch = require('node-fetch');
export {Lb4GraphqlPocApplication};
export async function main(options: ApplicationConfig = {}) {
console.log('hello world!')
const app = new Lb4GraphqlPocApplication(options);
await app.boot();
await app.start();
const url = app.restServer.url;
const graphqlPath = '/graphql';
console.log(`REST Server is running at ${url}`);
console.log(`Try ${url}/ping`);
// replace with process.env.{active-environment} once deployments setup
const openApiSchema = 'http://localhost:3000/openapi.json';
const oas = await fetch(openApiSchema)
.then((res: any) => {
console.log(`JSON schema loaded successfully from ${openApiSchema}`);
return res.json();
})
.catch((err: any) => {
console.error('ERROR: ', err);
throw err;
});
const {schema} = await createGraphQLSchema(oas, {
strict: false,
viewer: true,
baseUrl: url,
headers: {
'X-Origin': 'GraphQL',
},
tokenJSONpath: '$.jwt',
});
const handler = graphqlHTTP(
(request: any, response: any, graphQLParams: any) => ({
schema,
pretty: true,
graphiql: true,
context: {jwt: getJwt(request)},
}),
);
// Get the jwt from the Authorization header and place in context.jwt, which is then referenced in tokenJSONpath
function getJwt(req: any) {
if (req.headers && req.headers.authorization) {
return req.headers.authorization.replace(/^Bearer /, '');
}
}
app.mountExpressRouter(graphqlPath, handler);
console.log(`Graphql API: ${url}${graphqlPath}`);
return app;
}

Resources