Firebase Functions read and add data in same onCall function - javascript

I have a function that is triggered when I click a button. Based on the docId I can retrieve the right download URL in Firestore. But I also want to update a specific field (availableDownloads) in my "customers" collection. I can't get it to work.
This code works fine. It returns the download url.
exports.getDownloadUrl = functions.https.onCall(async(data, context) => {
var docRef= await db.collection('projects').doc(data.docId);
return docRef.get().then(function(doc){
const downloadURL = doc.data().downloadURL;
return downloadURL;
}).catch(function(error) {
// Handle error
});
});
However this doesn't. It returns null
exports.getDownloadUrl = functions.https.onCall(async(data, context) => {
var docRef= await db.collection('projects').doc(data.docId);
return docRef.get()
.then(async function(doc){
const downloadURL = doc.data().downloadURL;
const userRef = await db.collection('customers').doc(context.auth.uid);
return userRef.update({
availableDownloads: admin.firestore.FieldValue.increment(-1)
}).then(()=> {
return downloadURL;
}).catch((error)=> {
})
}).catch(function(error) {
// Handle error
});
});

The first thing that jumps out at me is that .update() is an asynchronous function so you need an await in front of userRef.update(...).
You should also consider sticking to either arrow functions (() => {}) or anonymous functions (function () {}), but that's just a stylistic note. :)
ETA: as Frank van Puffelen pointed out, you don't need awaits for simply declaring a DocumentReference since those are not async functions. (looking at the line var docRef= await db.collection('projects').doc(data.docId);).

In addition to the potential problems due to the mix of async/await with then/catch, your main problem is due to variable scoping. The downloadURL variable is a local variable declared inside the first then() block and is therefore not accessible in the second then() block.
You can easily see the problem with the following code:
function later(delay, value) {
return new Promise((resolve) => setTimeout(resolve, delay, value));
}
later(500, 'value1')
.then((value) => {
console.log('First then block: ' + value);
const downloadURL = value;
return later(1000, 'value2');
})
.then((value) => {
console.log('Second then block: ' + value);
console.log('Second then block: ' + downloadURL); // Here it is going to throw an error
})
.catch((error) => {
console.log(error);
});
You need to declare the downloadURL variable in the scope of the Cloud Function, as follows:
exports.getDownloadUrl = functions.https.onCall((data, context) => {
let downloadURL;
const docRef = db.collection('projects').doc(data.docId);
return docRef.get()
.then((doc) => {
downloadURL = doc.data().downloadURL;
const userRef = db.collection('customers').doc(context.auth.uid);
return userRef.update({
availableDownloads: admin.firestore.FieldValue.increment(-1)
});
})
.then(() => {
return downloadURL;
}).catch((error) => {
// Handle error
})
});
Note that the above code does not use async/await. If you want to use async/await, do as follows:
exports.getDownloadUrl = functions.https.onCall(async (data, context) => {
try {
const docRef = db.collection('projects').doc(data.docId);
const doc = await docRef.get();
const downloadURL = doc.data().downloadURL;
const userRef = db.collection('customers').doc(context.auth.uid);
await userRef.update({
availableDownloads: admin.firestore.FieldValue.increment(-1)
})
return downloadURL;
} catch (error) {
// Handle error
}
});
You can see that with this code, due to the fact that it is simplified by using async/await, there is no more complexity due to variable scoping.

Related

Firebase Promise Returning Undefined Data Javascript

I've followed several guides on how to correctly wait for the data from my ListFile() function. I have logged the output to the console in ListFile() so I know the data is correct. When I try and await for the function to complete and log the data, I get one of two outputs when experimenting with awaits and promises:
getData error: TypeError: undefined is not an object (evaluating 'response.json')
or
Promise {
"_U": 0,
"_V": 0,
"_W": null,
"_X": null,
}
I'm creating a React Native app and the function that calls ListFile() contains a bunch of other components that are rendered so I can't make the entire function an async function. Not sure what else to do here.
Calls ListFile()
const getData = async () => {
try {
const response = await ListFile();
const data = response.json();
console.log(data);
} catch (err) {
console.log("getData error: " + err);
}
}
getData(); //always returns getData error: TypeError: undefined is not an object (evaluating 'response.json')
ListFile()
async function ListFile() {
// Reference Firebase Container
var filesInStorageList = [];
let content = [];
const listRef = ref(fbStorage, filePath);
console.log("Listing Files");
// List all files in container
listAll(listRef)
.then((res) => {
res.prefixes.forEach((folderRef) => {
// All the prefixes under listRef.
// You may call listAll() recursively on them.
console.log(res);
});
res.items.forEach((itemRef) => {
// All the items under listRef.
let fileInStorage = itemRef["_location"]["path_"].slice(filePath.length);
filesInStorageList.push(fileInStorage);
// CORRECT DATA IS LOGGED console.log("Pushing " + fileInStorage);
// CORRECT DATA IS LOGGED console.log(filesInStorageList);
});
return filesInStorageList;
}).catch((error) => {
// Uh-oh, an error occurred!
console.log("ListFile - ERROR");
});
}
You're mixing async/await and Promises. Although they serve similar purposes, generally they are used separately to avoid situations like this where they are used together and then create unexpected results if one doesn't know what to look for.
You annotate your ListFile function with async, which means that it's expecting to find await somewhere. Instead, you use a Promise-based .then and .catch.
To convert your code, you could change it to this:
async function ListFile() {
// Reference Firebase Container
var filesInStorageList = [];
let content = [];
const listRef = ref(fbStorage, filePath);
console.log("Listing Files");
// List all files in container
try {
const res = await listAll(listRef);
//...do your forEach loops
return filesInStorageList;
} catch(err) {
//handle your error
}
}
You should add a return on the listAll method to return its value when calling it from getData(). See sample code below:
const getData = async () => {
try {
ListFile()
.then((response) => {
// do something here.
console.log(response);
});
} catch (err) {
console.log("getData error: " + err);
}
}
getData();
async function ListFile() {
// Reference Firebase Container
var filesInStorageList = [];
let content = [];
const filePath = "test/";
const listRef = ref(storage, filePath);
// List all files in container
return listAll(listRef)
.then((res) => {
res.prefixes.forEach((folderRef) => {
// All the prefixes under listRef.
// You may call listAll() recursively on them.
console.log(res);
});
res.items.forEach((itemRef) => {
// All the items under listRef.
let fileInStorage = itemRef["_location"]["path_"].slice(filePath.length);
filesInStorageList.push(fileInStorage);
// CORRECT DATA IS LOGGED console.log("Pushing " + fileInStorage);
// CORRECT DATA IS LOGGED console.log(filesInStorageList);
// console.log(filesInStorageList);
});
return filesInStorageList;
}).catch((error) => {
// Uh-oh, an error occurred!
console.log(error);
});
}
Also, as #jnpdx stated, you have to use a Promise-based .then and .catch on your ListFile()

Promises not getting resolved

I am running this asynchronous function in my React app -
const getMetaData = async (hashes: any) => {
console.log({ hashes });
try {
const data = hashes.map(async (hash: any) => {
const url = `http://localhost:3003/user/pinata/getmetadata/${hash}`;
const metadata = await axios.get(url);
return metadata.data.response;
});
console.log("data1", data);
const metadata = await Promise.all(data);
console.log('data2', metadata);
} catch (error) {
console.log('getMetaData Error', error);
}
};
console.log("data1", data) gives me -
data1 (12) [Promise, Promise, Promise, Promise, Promise, Promise, Promise, Promise, Promise, Promise, Promise, Promise]
The problem here is after I do a await Promise.all(data) I don't get data2 anywhere in the console. Maybe because the Promises are not even getting resolved?
Any idea what might be wrong?
Thanks in advance.
It seems that your code works fine when using SWAPI API so it can be that the API you use does not deliver data appropriately. I run the below code to test. Here's a link to codebox to play around with it if you want.
import axios from "axios";
const data = ["people", "planets", "starships"];
const getMetaData = async (hashes) => {
console.log({ hashes });
try {
const data = hashes.map(async (hash) => {
const url = `https://swapi.dev/api/${hash}`;
const metadata = await axios.get(url);
return metadata.data.results;
});
console.log("data1", data);
const metadata = await Promise.all(data);
console.log("data2", metadata);
} catch (error) {
console.log("getMetaData Error", error);
}
};
getMetaData(data);
With this code, it appears the most likely situation is that one of the promises in the loop is not resolving or rejecting. To confirm that, you can log every possible path with more local error handling so you can see exactly what happens to each request. I also added a timeout to the request so you can definitely find out if it's just not giving a response, but you can also see that by just looking at the logging for begin and end of each request in the loop:
function delay(msg, t) {
return new Promise((resolve, reject)) => {
setTimeout(() => {
reject(new Error(msg));
}), t);
});
}
const getMetaData = async (hashes: any) => {
console.log({ hashes });
try {
const data = hashes.map(async (hash: any, index: number) => {
try {
console.log(`Request: ${index}, hash: ${hash}`);
const url = `http://localhost:3003/user/pinata/getmetadata/${hash}`;
const metadata = await axios.get(url);
console.log(`Request: ${index}, result: ${metadata.data.response}`);
return metadata.data.response;
} catch (e) {
console.log(`Request: ${index} error: `, e);
throw e;
}
});
console.log("data1", data);
const metadata = await Promise.all(data.map((p: any, index: number) => {
return Promise.race(p, delay(`Timeout on request #${index}`, 5000));
});
console.log('data2', metadata);
} catch (error) {
console.log('getMetaData Error', error);
}
};
FYI, I don't really know Typescript syntax so if I've made any Typescript mistakes here, you can hopefully see the general idea and fix the syntax.

Cannot read property 'code' of undefined from promise, context api (react hook)

I am making my first hybrid web app using react hook.
I am faced with a timing problem with promise and context api.
Here are the logic I am facing with.
A function fetchApplications gets data from firebase firestore, it is defined in globalcontext.js (called in tableView)
tableView.js calls fetchApplications in useEffect.
fetchApplications is defined as promise function, I expect it will resolve(return) data until data fetched, it will resolove(return) object like {code:200, data:data}
problem
in the fetchData.code,
Cannot read property 'code' of undefined
Here is my code
In the tableview.js
React.useEffect(() => {
context.fetchApplications(auth.userId, "")
.then(function (fetchData) {
console.log("Fetched data: ", fetchData); ///this is undefined
if (fetchData.code !== 200) { /// error part
alert(fetchData.msg);
}
if (!fetchData.data) {
alert("No applications");
}
setsData(fetchData.data);
});
}, []);
In the GlobalContext.js
async function fetchApplications(userId, role) {
return new Promise((resolve, reject) => {
// resolve({ code: 200, data: "data" }); //If I add this code, it will be alright.
let dataArray = [];
let applicationRef = db
.collection("Users")
.doc(userId)
.collection("Applications");
applicationRef
.get()
.then(function (qs) {
qs.forEach(function (doc) { //doesn't work
console.log(doc.id, " => ", doc.data());
console.log(doc.size, " => ", typeof doc);
dataArray.push(doc.data());
});
return Promise.resolve(dataArray);
})
.then((data) => {
console.log("Global, Fetched Data", dataArray);
if (data) {
resolve({ code: 200, data: data });
}
return;
});
}).catch(function (error) {
reject({ code: 400, msg: "시스템에러 고객센터에 문의해주세요" });
});
}
wrote in codesendbox
If I was write the wrong way of promise, please let me know.
thanks
You're implementing a couple of bad practices and have some major issues. For starters, fetchApplications is marked as async but you're returning a manually created promise which is quite overkill as your fetching code actually generates a promise which you should directly return. Furthermore:
.then(function (qs) {
qs.forEach(function (doc) { //doesn't work
console.log(doc.id, " => ", doc.data());
console.log(doc.size, " => ", typeof doc);
dataArray.push(doc.data());
});
return Promise.resolve(dataArray);
})
I am not sure what exactly "//doesn't work" should mean but return Promise.resolve(dataArray); won't cut it for you. You're already in the then chain, so you can't resolve anything from the main promise at this point. You should just pass the data to the next then callback as return dataArray;.
All in all, I will suggest ditching the then-ables syntax all together and migrate to async/await altogether:
async function fetchApplications(userId, role) {
try {
const dataArray = [];
const applicationRef = db
.collection("Users")
.doc(userId)
.collection("Applications");
const querySnapshot = await applicationRef.get();
querySnapshot.forEach(doc => {
dataArray.push(doc.data());
});
return {
code: 200,
data: dataArray
};
}
catch (error) {
return {
code: 400,
msg: '시스템에러 고객센터에 문의해주세요'
};
}
}
Then, in your react component/hook:
React.useEffect(() => {
const fetchApplicationDataAsync = async () => {
const result = await context.fetchApplications(auth.userId, "");
if (result.code !== 200) {
alert(result.msg);
}
if (!result.data) {
alert("No applications");
}
setsData(result.data);
}
fetchApplicationDataAsync();
}, [auth.userId]);
Another problem and bad practice is that you're not specifying your dependencies for the useEffect hook. You have 2 external dependencies: the auth.userId paramater and the ontext.fetchApplications function. We alleviate one of the problem by creating the fetch function in the body of useEffect itself. However, the auth.userId should go into the dependency array as stated above.
You have to check for fetchData to be defined before accessing its properties.
A short form would be
if (fetchData && fetchData.code !== 200){...}
Applied to your code:
React.useEffect(() => {
context.fetchApplications(auth.userId, "")
.then(function (fetchData) {
console.log("Fetched data: ", fetchData); ///this is undefined
if (fetchData && fetchData.code !== 200) { /// error part
alert(fetchData.msg);
}else {
alert("No applications");
}
setsData(fetchData.data);
});
}, []);
By calling then() on the fetchApplications() function, as follows, you pass to the callback the fullfilment value from the Promise returned by fetchApplications() (i.e. fetchData gets the value returned by fetchApplications()).
context.fetchApplications(auth.userId, "")
.then(function (fetchData) {...}
However, fetchApplications() returns a Promise that resolves with undefined because, actually, you don't return the Promises chain. This is why you get an error on fetchData.code.
Adapting fetchApplications() as follows (using await, since you use async) should do the trick (untested!):
async function fetchApplications(userId, role) {
try {
let dataArray = [];
let applicationRef = db
.collection('Users')
.doc(userId)
.collection('Applications');
const qs = await applicationRef.get();
qs.forEach(doc => {
console.log(doc.id, ' => ', doc.data());
console.log(doc.size, ' => ', typeof doc);
dataArray.push(doc.data());
});
return { code: 200, data: dataArray };
} catch (error) {
return { code: 400, msg: '시스템에러 고객센터에 문의해주세요' };
}
}
Note that in any case you return an object with a code property, so no more problem when doing fetchData.code.

this.setToken is not a function

I am trying to call a function in my action, but get the error this.setToken is not a function:
async function setToken() {
const {
credentials: { year, group, student }
} = this.props;
const fcmToken = await firebase.messaging().getToken();
if (fcmToken) {
firebase
.firestore()
.collection("users")
.doc(fcmToken)
.set({
year
})
.then(function() {
return true;
})
.catch(function(error) {
return false;
});
}
}
export function fetchEvents(id) {
const currentDateString =
moment().format("YYYY-MM-DD") + "T" + "07:00:00.000Z";
const url = xxx;
return async dispatch => {
dispatch(isLoading(true));
const setToken = await setToken(); // call here
fetch(url)
.then(response => {
return response;
})
.then(response => response.json())
.then(data => {
const { error } = data;
if (error) {
dispatch(hasErrored(error.message));
} else {
dispatch(fetchSuccessEvents(data.items));
}
navigate("Month");
dispatch(isLoading(false));
});
};
}
Any ideas?
setToken is defined as a standalone function, not a property of an instance, or of the current this, or anything like that:
async function setToken() {
So, call it like you would call any other function, without putting this in front of it.
You also cannot use the same variable name in the inner scope, else you'll refer to the inner variable (which will start out as undefined / uninitialized); assign the result to a different variable name instead:
const token = await setToken();
But your current code doesn't have setToken resolve to anything, in which case, you may simply await it, without assigning the result to anything:
await setToken();
Use
setToken()
instead of
this.setToken()
You are not in a class environment, as far as I can tell, so setToken() is not an instance or a property, but a standalone function.
If you prefer to use a class notation, you could use window.setToken().

Save Async/Await response on a variable

I am trying to understand async calls using async/await and try/catch.
In the example below, how can I save my successful response to a variable that can be utilized throughout the rest of the code?
const axios = require('axios');
const users = 'http://localhost:3000/users';
const asyncExample = async () =>{
try {
const data = await axios(users);
console.log(data); //200
}
catch (err) {
console.log(err);
}
};
//Save response on a variable
const globalData = asyncExample();
console.log(globalData) //Promise { <pending> }
1) Return something from your asyncExample function
const asyncExample = async () => {
const result = await axios(users)
return result
}
2) Call that function and handle its returned Promise:
;(async () => {
const users = await asyncExample()
console.log(users)
})()
Here's why should you handle it like this:
You can't do top-level await (there's a proposal for it though);
await must exist within an async function.
However I must point out that your original example doesn't need async/await
at all; Since axios already returns a Promise you can simply do:
const asyncExample = () => {
return axios(users)
}
const users = await asyncExample()
try..catch creates a new block scope. Use let to define data before try..catch instead of const, return data from asyncExample function call
(async() => {
const users = 123;
const asyncExample = async() => {
let data;
try {
data = await Promise.resolve(users);
} catch (err) {
console.log(err);
}
return data;
};
//Save response on a variable
const globalData = await asyncExample();
console.log(globalData);
// return globalData;
})();
I had same issue with you and found this post. After 2 days of trying I finally found a simple solution.
According to the document of JS, an async function will only return a Promise object instead of value. To access the response of Promise, you have to use .then()method or await which can return the resulting object of Promise is instead of Promise itself.
To change variables from await, you have access and change the variable you want to assign within the async function instead of return from it.
//Save response on a variable
var globalData;
const asyncExample = async () =>{
try {
const data = await axios(users);
globalData = data; // this will change globalData
console.log(data); //200
}
catch (err) {
console.log(err);
}
};
asyncExample();
But if you do this, you may get an undefined output.
asyncExample();
console.log(globalData) //undefined
Since asyncExample() is an async function, when console.log is called, asyncExample() has not finished yet, so globalData is still not assigned. The following code will call console.log after asyncExample() was done.
const show = async () => {
await asyncExample();
console.log(globalData);
}
show();
Because the events are happening asynchronously you need to tie in a callback/promise. I'm going to assume it returns a promise.
const axios = require('axios');
const users = 'http://localhost:3000/users';
const asyncExample = async () =>{
try {
const data = await axios(users);
console.log(data); //200
}
catch (err) {
console.log(err);
}
};
//Save response on a variable
const globalData = asyncExample().then( (success, err) => {
if (err) { console.error(err); }
console.log(success)
}
Just use a callback/promise (cascading programming):
axios(users).then(function(response) {
const globalData = response;
console.log(globalData)
});

Categories

Resources