I'm trying to upload a file using apollo-server-express and apollo-client. However, when the file object is passed to the resolver it is always empty. I can see the file on the client, but not the server side. How can I resolve this ?
My Schema
type File {
id: ID
path: String
filename: String
mimetype: String
}
extend type Query {
getFiles: [File]
}
extend type Mutation {
uploadSingleFile(file: Upload!): File
}
My Resolver
Mutation: {
uploadSingleFile: combineResolvers(
isAuthenticated,
async (parent, { file }, { models, user, storeUpload }, info) => {
console.log('Resolver-> uploadSingleFile')
console.log(file) // Will return empty, { }
const x = await file
console.log(x) // Will also return empty, { }
const storedFile = storeUpload(file)
return storedFile
}
),
},
My Client-side queries file
export const UPLOAD_SINGLE_FILE = gql`
mutation uploadSingleFile($file: Upload!) {
uploadSingleFile(file: $file) {
id
}
}
`
My Client-side interface
import React from 'react'
// GQL
import { useApolloClient, useMutation } from '#apollo/react-hooks'
import { UPLOAD_SINGLE_FILE } from '../../queries'
const FileUpload = props => {
const [uploadSingleFile, uploadSingleFileResult] = useMutation(UPLOAD_SINGLE_FILE, {
onCompleted(uploadSingleFile) {
console.log('Completed uploadSingleFile')
}
})
const apolloClient = useApolloClient()
const handleUploadFile = ({
target: {
validity,
files: [file]
}
}) => {
console.log('Uploading file...')
if(validity.valid) {
console.log('Valid')
console.log(file.name)
uploadSingleFile({ variables: { file } })
.then(() => {
apolloClient.resetStore()
})
}
else console.log('Invalid file')
}
return(
<input type="file" required onChange={handleUploadFile} />
)
}
export default FileUpload
UPDATED
My front-end set-up is:
const httpLink = createHttpLink({
uri: 'http://localhost:4000/graphql',
})
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token')
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
}
})
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
})
You need to utilize the appropriate Link with your Apollo Client in order to enable file uploads. The easiest way to do that is by using createUploadLink from apollo-upload-client. It functions as a drop-in replacement for createHttpLink, so just swap out the functions and you'll be good to go.
const httpLink = createUploadLink({
uri: 'http://localhost:4000/graphql',
})
const authLink = ...
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
})
Assuming you have the proper link set up and in use (using createUploadLink as Daniel mentions in his post), you should be able to destructure the props from file once you await the promise in your resolver on the server.
const { filename, mimetype, createReadStream } = await file.promise;
console.log(filename, mimetype);
// to get a stream to use of the data
const stream = createReadStream();
UPDATE: in more recent versions of graphql-upload you can just await the file like you do in the OP, rather than the file.promise. I was using an older version of the lib it seems.
Related
I am trying to post a file object to my nuxt 3 api route
Problem is:
Data from client has my file object
Data from server returns empty object
Screenshot of the issue
Where did my file object go?
const handleImageUpload = async (evt: Event) => {
const target = evt.target as HTMLInputElement
if (target.files) {
const file = target.files[0]
const upload: iUpload = {
name: file.name,
type: file.type,
file
}
console.log("data from client", upload)
try {
const { data, error } = await useFetch(constants.imageUploadApiUrl, {
headers: { "Content-type": "application/json" },
method: 'POST',
body: upload
})
console.log("data from server", data.value)
} catch (error) {
console.log(error)
}
}
}
constants.imageUploadApiUrl (api route) has the following
import { getQuery, readBody } from "h3"
import { iUpload } from "~~/helpers/interfaces"
export default defineEventHandler(async (event) => {
try {
const query = getQuery(event)
const body = await readBody(event) as iUpload
return { body }
} catch (error: any) {
return { error: error.message }
}
})
iUpload interface is this
export interface iUpload {
name: string;
type: string;
file: File;
}
I eventually got it working. Meanwhile it's using supabase as it's backend (forgot to mention that).
But here are the changes I made.
#1 - I added a utility function to convert the file to base64 string
export const getBase64 = (file: File) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
#2 - I updated the handleImageUpload function like below. The only change being in the file key
const handleImageUpload = async (evt: Event) => {
const target = evt.target as HTMLInputElement
if (target.files) {
const fileObj = target.files[0]
const upload: iUpload = {
path: id(memberName(store.selected), '-'),
name: fileObj.name,
file: await getBase64(fileObj) as string, // <**=**
type: fileObj.type
}
console.log("data from client", upload)
try {
const { data, error } = await useFetch(constants.imageUploadApiUrl, {
headers: { "Content-type": "multipart/form-data" },
method: 'POST',
body: upload
})
console.log("data from server", data.value)
} catch (error) {
console.log(error)
}
}
}
#3 - Furthermore I updated the server route as follows:
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event) as iUpload
const filePath = `${body.path}/${body.name}`
const res = await fetch(body.file)
const blob = await res.blob()
const response = await supabase.storage
.from("pictures")
.upload(filePath, blob, {
contentType: body.type,
upsert: true,
})
return {
data: response.data,
error: response.error?.message,
}
} catch (error: any) {
return { error: error.message }
}
})
#4 - Lastly I updated the policies on supabase storage bucket and storage object to the following:
supabase storage policy update
Below I try to respond with a stream when I receive ticker updates.
+page.server.js:
import YahooFinanceTicker from "yahoo-finance-ticker";
const ticker = new YahooFinanceTicker();
const tickerListener = await ticker.subscribe(["BTC-USD"])
const stream = new ReadableStream({
start(controller) {
tickerListener.on("ticker", (ticker) => {
console.log(ticker.price);
controller.enqueue(ticker.price);
});
}
});
export async function load() {
return response????
};
Note: The YahooFinanceTicker can't run in the browser.
How to handle / set the response in the Sveltekit load function.
To my knowledge, the load functions cannot be used for this as their responses are JS/JSON serialized. You can use an endpoint in +server to return a Response object which can be constructed from a ReadableStream.
Solution: H.B. comment showed me the right direction to push unsollicited price ticker updates the client.
api route: yahoo-finance-ticker +server.js
import YahooFinanceTicker from "yahoo-finance-ticker";
const ticker = new YahooFinanceTicker();
const tickerListener = await ticker.subscribe(["BTC-USD"])
/** #type {import('./$types').RequestHandler} */
export function GET({ request }) {
const ac = new AbortController();
console.log("GET api: yahoo-finance-ticker")
const stream = new ReadableStream({
start(controller) {
tickerListener.on("ticker", (ticker) => {
console.log(ticker.price);
controller.enqueue(String(ticker.price));
}, { signal: ac.signal });
},
cancel() {
console.log("cancel and abort");
ac.abort();
},
})
return new Response(stream, {
headers: {
'content-type': 'text/event-stream',
}
});
}
page route: +page.svelte
<script>
let result = "";
async function getStream() {
const response = await fetch("/api/yahoo-finance-ticker");
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
while (true) {
const { value, done } = await reader.read();
console.log("resp", done, value);
if (done) break;
result += `${value}<br>`;
}
}
getStream();
</script>
<section>
<p>{#html result}</p>
</section>
I would like my uploadFormPage() function to be able to take jpegs and pdf's. Is it possible for me to have 2 file types for the same FormData() const?`
export function uploadFormPage(documentId, formId, file, callback) {
return async dispatch => {
try {
const formData = new FormData();
formData.append('page', {
name: `document-${documentId}-${formId}-${Date.now()}.jpg`,
type: 'image/jpeg',
uri: file,
});
const result = await Api.uploadFiles(formData);
const entity = {
id: formId,
resourceKey: result.page,
};
const rsp = await Api.uploadFormPage(documentId, entity);
dispatch({type: LOAD_DOCUMENTS, data: rsp});
callback(null, rsp);
} catch (e) {
callback(e, null);
}
};
}
I have a problem creating a hook calling a stored procedure.
My custom service hooks (customservice.hooks.js) are :
const callstored = require('../../hooks/callstored')
module.exports = {
before: {
all: [],
find: [callstored()],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
...
My service class (customservice.class.js):
const mysql = require('mysql')
class Service {
constructor (options) {
this.options = options || {};
}
find (params) {
return Promise.resolve( [] );
}
get (id, params) {
return Promise.resolve({
id, text: `A new message with ID: ${id}!`
});
}
create (data, params) {
if (Array.isArray(data)) {
return Promise.all(data.map(current => this.create(current)));
}
return Promise.resolve(data);
}
update (id, data, params) {
return Promise.resolve(data);
}
patch (id, data, params) {
return Promise.resolve(data);
}
remove (id, params) {
return Promise.resolve({ id });
}
}
module.exports = function (options) {
return new Service(options);
};
module.exports.Service = Service;
And my service (customservice.service.js) :
const createService = require('./rankingvotes.class.js');
const hooks = require('./rankingvotes.hooks');
module.exports = function (app) {
const paginate = app.get('paginate');
const options = {
name: 'rankingvotes',
paginate
};
// Initialize our service with any options it requires
app.use('/rankingvotes', createService(options));
// Get our initialized service so that we can register hooks and filters
const service = app.service('rankingvotes');
service.hooks(hooks);
app.publish(() => {
});
};
And finally my hook is (callstored.js)
const Sequelize = require('sequelize');
module.exports = function () {
return function (hook) {
sequelize = hook.app.get('sequelizeClient');
let result=[];
return sequelize.query('CALL RANKING();',{
nest: true,
raw: true }).then(function(response){
console.log(response[0]) //data are correct
hook.data=response[0];
return hook;
}).error(function(err){
console.log(err);
return hook;
});
}
}
If I check the console I got data correctly, but nothing calling from a REST client like Postman.
Any idea? Thank you.
If you want to change the response you have to set hook.result.
const Sequelize = require('sequelize');
module.exports = function () {
return async hook => {
try {
const sequelize = hook.app.get('sequelizeClient');
const response = await sequelize.query('CALL RANKING();',{
nest: true,
raw: true
});
console.log(response[0]) //data are correct
context.result = response[0];
} catch(error) {
console.error(error);
}
return hook;
}
}
hook.data is the request data and is only available for create, update and patch.
Keep in mind that setting hook.result will skip your custom service find if it is set in a before hook.
I'm having trouble moving my Angular 1 JavaScript service to a Angular 2 TypeScript service using http to make a CORS request (this is using Ionic version 2). In Angular 1, I do something like this.
angular.module('app.services',[])
.factory('LoginService', ['$http', function($http) {
var service = {};
service.isUserLoggedIn = function() {
var restUrl = 'http://10.10.10.25:8080/api/user/isloggedin';
var options = {
method: 'GET', url: restUrl, withCredentials: true,
headers: {
'x-request-with': 'XMLHttpRequest',
'x-access-token': 'sometoken'
}
};
return $http(options);
};
return service;
}])
In Angular 2 using TypeScript, a lot has changed (Promises/Observables). My attempt so far looks like the following.
import { Injectable } from '#angular/core';
import { Headers, Http, Response, RequestMethod, RequestOptionsArgs } from '#angular/http';
import { Observable } from 'rxjs/Observable';
#Injectable()
export class LoginService {
constructor(private _http:Http) {
}
isLoggedIn():boolean {
var r:any;
var e:any;
let url = 'http://10.10.10.25:8080/api/user/isloggedin';
let options:RequestOptionsArgs = {
url: url,
method: RequestMethod.Get,
search: null,
headers: new Headers({
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'x-access-token' : 'sometoken'
}),
body: null
};
this._http.get(url, options).map(this.extractData).catch(this.handleError)
.subscribe(
response => { r = <any>response; console.log(r); },
error => { e = <any>error; console.log(e); });
return false;
}
private extractData(response:Response) {
let body = response.json();
return body.data || { };
}
private handleError(error:any) {
let errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.log('error = ' + errMsg);
return Observable.throw(errMsg);
}
}
I am no longer sure how to address in Angular 2 what I had in Angular 1
withCredentials (note that https://angular.io/docs/js/latest/api/http/index/RequestOptionsArgs-interface.html#!#withCredentials-anchor says the RequestOptionsArgs should have a field withCredentials but I get an error in the IDE Visual Studio Code saying that it does not exists in the interface; ionic is probably using an older angular 2 version?)
headers (x-request-with and x-access-token)
the actual response
I did a little bit more searching, and I was able to understand how to insert headers and properly handle the subscription. However, the withCredentials is still a problem.
I found this SO post angular2 xhrfields withcredentials true and modified my constructor as follows. It works now.
constructor(#Inject(Http) private _http:Http) {
let _build = (<any> _http)._backend._browserXHR.build;
(<any> _http)._backend._browserXHR.build = () => {
let _xhr = _build();
_xhr.withCredentails = true;
return _xhr;
}
}
The support of withCredentails will be present in the RC2 version of Angular2 that will be released soon. It's not part of the RC1 version... You need to wait a bit.
With RC2, you will be able to use this property directly in the request options:
this.get('...', { withCredentials: true })
See this question for more details:
Angular 2 - http get withCredentials