This is more of a conceptual question as logically, I can reduce the code below. Per the comments, user information is extracted and put in props. The same user data was then pulled from the database to be returned using done.
I think the original author of the code wanted to make sure it was saved to the database. But I think that is overkill.
Error checking will let us know if anything went wrong and we don't need to pull the data immediately after saving it. We can just return the same data that was stored.
This is passport authentication code.
// the user was not found
// create the user, get the user, and return the user object
function createUser (done, profile) {
let props = obtainProps(profile);
DBM.createUser(props).then(() => {
DBM.getUser(props.id_google).then((res) => {
return done(null, res[0]);
}).catch( error => {
return done(error, null);
});
});
}
to this code:
// the user was not found
// create the user then return props
function createUser (done, profile) {
let props = obtainProps(profile);
DBM.createUser(props).then(() => {
return done(null, props);
}).catch( error => {
return done(error, null);
});
}
I think that you can omit the check too, in addition, as commented, it would make sense to use the Promise and return that, instead of passing a callback and execute it in the promise's then. Promises were made to replace callbacks, your code is going in the other direction. Your createUser function should look like
function createUser (profile) {
let props = obtainProps(profile);
return DBM.createUser(props);
}
and if you need the result somewhere else, chain the function with then
// somewhere else in your code
createUser().then(e => /* e is result of DBM.createUser */)
Related
I have a three-step process that needs to run synchronously. First, the app pulls information from one firebase collection, then using that result, pulls information from a second collection, then using that result, performs a function.
These are not running synchronously, so I'm missing the data assignments.
I believe the solution is working with promise.all, but I cannot structure it correctly:
Original Code:
getUserInfo(user)
.then(() => {
//get data from database from result of getUserInfo
getStyles(userInfo.value);
})
then(() => {
//do things with data from database from result of
getStyles
console.log(info);
});
With Promise.all:
let newUser = getUserInfo(user);
let newStyles = getStyles(userInfo.value)
Promise.all([newUser, newStyles]).then(() => {
console.log("do things")
}
The error is that the value newUser is still undefined.
It seems like you forgot to return anything from your promises so all you get is undefined.
Try like this.
getUserInfo(user)
.then((userInfo) => {
//get data from database from result of getUserInfo
return getStyles(userInfo.value);
})
then((stylesInfo) => {
//do things with data from database from result of getStyles
console.log(stylesInfo);
});
This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 4 years ago.
I need to fetch id of user from collection 'users' by calling a function and return it's value.
fetchId = (name) => {
User.findOne({name: name}, (err, user) => {
return user._id;
});
};
But this implementation returns null. What is the way to fix it?
following your example, if you don't want to use promises, you can simply pass a callback from the caller and invoke the callback when you have the result since the call to mongo is asynchronous.
fetchId = (name, clb) => {
User.findOne({name: name}, (err, user) => {
clb(user._id);
});
};
fetchId("John", id => console.log(id));
Otherwise you can use the promise based mechanism omitting the first callback and return the promise to the caller.
fetchId = name => {
return User.findOne({name: name}).then(user => user.id);
};
fetchId("John")
.then(id => console.log(id));
A third way is a variation of suggestion #2 in #Karim's (perfectly good) answer. If the OP wants to code it as if results are being assigned from async code, an improvement would be to declare fetchId as async and await its result...
fetchId = async (name) => {
return User.findOne({name: name}).then(user => user.id);
};
let someId = await fetchId("John");
console.log(id)
edit
For any method on an object -- including a property getter -- that works asynchronously, the calling code needs to be aware and act accordingly.
This tends to spread upward in your system to anything that depends on caller's callers, and so on. We can't avoid this, and there's no syntactic fix (syntax is what the compiler sees). It's physics: things that take longer, just take longer. We can use syntax to partially conceal the complexity, but we're stuck with the extra complexity.
Applying this to your question, say we have an object representing a User which is stored remotely by mongo. The simplest approach is to think of the in-memory user object as unready until an async fetch (findOne) operation has completed.
Under this approach, the caller has just one extra thing to remember: tell the unready user to get ready before using it. The code below employs async/await style syntax which is the the most modern and does the most to conceal -- but not eliminate :-( -- the async complexity...
class MyMongoUser {
// after new, this in-memory user is not ready
constructor(name) {
this.name = name;
this.mongoUser = null; // optional, see how we're not ready?
}
// callers must understand: before using, tell it to get ready!
async getReady() {
this.mongoUser = await myAsyncMongoGetter();
// if there are other properties that are computed asynchronously, do those here, too
}
async myAsyncMongoGetter() {
// call mongo
const self = this;
return User.findOne({name: self.name}).then(result => {
// grab the whole remote object. see below
self.mongoUser = result;
});
}
// the remaining methods can be synchronous, but callers must
// understand that these won't work until the object is ready
mongoId() {
return (this.mongoUser)? this.mongoUser._id : null;
}
posts() {
return [ { creator_id: this.mongoId() } ];
}
}
Notice, instead of just grabbing the mongo _id from the user, we put away the whole mongo object. Unless this is a huge memory hog, we might as well have it hanging around so we can get any of the remotely stored properties.
Here's what the caller looks like...
let joe = new MyMongoUser('joe');
console.log(joe.posts()) // isn't ready, so this logs [ { creator_id: null } ];
await joe.getReady();
console.log(joe.posts()) // logs [ { creator_id: 'the mongo id' } ];
Currently, I am trying to get the md5 of every value in array. Essentially, I loop over every value and then hash it, as such.
var crypto = require('crypto');
function userHash(userIDstring) {
return crypto.createHash('md5').update(userIDstring).digest('hex');
}
for (var userID in watching) {
refPromises.push(admin.database().ref('notifications/'+ userID).once('value', (snapshot) => {
if (snapshot.exists()) {
const userHashString = userHash(userID)
console.log(userHashString.toUpperCase() + "this is the hashed string")
if (userHashString.toUpperCase() === poster){
return console.log("this is the poster")
}
else {
..
}
}
else {
return null
}
})
)}
However, this leads to two problems. The first is that I am receiving the error warning "Don't make functions within a loop". The second problem is that the hashes are all returning the same. Even though every userID is unique, the userHashString is printing out the same value for every user in the console log, as if it is just using the first userID, getting the hash for it, and then printing it out every time.
Update LATEST :
exports.sendNotificationForPost = functions.firestore
.document('posts/{posts}').onCreate((snap, context) => {
const value = snap.data()
const watching = value.watchedBy
const poster = value.poster
const postContentNotification = value.post
const refPromises = []
var crypto = require('crypto');
function userHash(userIDstring) {
return crypto.createHash('md5').update(userIDstring).digest('hex');
}
for (let userID in watching) {
refPromises.push(admin.database().ref('notifications/'+ userID).once('value', (snapshot) => {
if (snapshot.exists()) {
const userHashString = userHash(userID)
if (userHashString.toUpperCase() === poster){
return null
}
else {
const payload = {
notification: {
title: "Someone posted something!",
body: postContentNotification,
sound: 'default'
}
};
return admin.messaging().sendToDevice(snapshot.val(), payload)
}
}
else {
return null
}
})
)}
return Promise.all(refPromises);
});
You have a couple issues going on here. First, you have a non-blocking asynchronous operation inside a loop. You need to fully understand what that means. Your loop runs to completion starting a bunch of non-blocking, asynchronous operations. Then, when the loop finished, one by one your asynchronous operations finish. That is why your loop variable userID is sitting on the wrong value. It's on the terminal value when all your async callbacks get called.
You can see a discussion of the loop variable issue here with several options for addressing that:
Asynchronous Process inside a javascript for loop
Second, you also need a way to know when all your asynchronous operations are done. It's kind of like you sent off 20 carrier pigeons with no idea when they will all bring you back some message (in any random order), so you need a way to know when all of them have come back.
To know when all your async operations are done, there are a bunch of different approaches. The "modern design" and the future of the Javascript language would be to use promises to represent your asynchronous operations and to use Promise.all() to track them, keep the results in order, notify you when they are all done and propagate any error that might occur.
Here's a cleaned-up version of your code:
const crypto = require('crypto');
exports.sendNotificationForPost = functions.firestore.document('posts/{posts}').onCreate((snap, context) => {
const value = snap.data();
const watching = value.watchedBy;
const poster = value.poster;
const postContentNotification = value.post;
function userHash(userIDstring) {
return crypto.createHash('md5').update(userIDstring).digest('hex');
}
return Promise.all(Object.keys(watching).map(userID => {
return admin.database().ref('notifications/' + userID).once('value').then(snapshot => {
if (snapshot.exists()) {
const userHashString = userHash(userID);
if (userHashString.toUpperCase() === poster) {
// user is same as poster, don't send to them
return {response: null, user: userID, poster: true};
} else {
const payload = {
notification: {
title: "Someone posted something!",
body: postContentNotification,
sound: 'default'
}
};
return admin.messaging().sendToDevice(snapshot.val(), payload).then(response => {
return {response, user: userID};
}).catch(err => {
console.log("err in sendToDevice", err);
// if you want further processing to stop if there's a sendToDevice error, then
// uncomment the throw err line and remove the lines after it.
// Otherwise, the error is logged and returned, but then ignored
// so other processing continues
// throw err
// when return value is an object with err property, caller can see
// that that particular sendToDevice failed, can see the userID and the error
return {err, user: userID};
});
}
} else {
return {response: null, user: userID};
}
});
}));
});
Changes:
Move require() out of the loop. No reason to call it multiple times.
Use .map() to collect the array of promises for Promise.all().
Use Object.keys() to get an array of userIDs from the object keys so we can then use .map() on it.
Use .then() with .once().
Log sendToDevice() error.
Use Promise.all() to track when all the promises are done
Make sure all promise return paths return an object with some common properties so the caller can get a full look at what happened for each user
These are not two problems: the warning you get is trying to help you solve the second problem you noticed.
And the problem is: in Javascript, only functions create separate scopes - every function you define inside a loop - uses the same scope. And that means they don't get their own copies of the relevant loop variables, they share a single reference (which, by the time the first promise is resolved, will be equal to the last element of the array).
Just replace for with .forEach.
I'm trying to send a notification with Firebase Cloud Functions when someone likes a photo. I've coped the Firebase example showing how to send one when someone follows you and tried to modify it.
The problem is that I need to do another additional query in the function to get the key of the person who liked a photo before I can get their token from their User node. The problem is that the getDeviceTokensPromise somehow is not fulfilled and tokensSnapshot.hasChildren can't be read because it is undefined. How can I fix it?
exports.sendPhotoLikeNotification = functions.database.ref(`/photo_like_clusters/{photoID}/{likedUserID}`)
.onWrite((change, context) => {
const photoID = context.params.photoID;
const likedUserID = context.params.likedUserID;
// If un-follow we exit the function
if (!change.after.val()) {
return console.log('User ', likedUserID, 'un-liked photo', photoID);
}
var tripName;
var username = change.after.val()
// The snapshot to the user's tokens.
let tokensSnapshot;
// The array containing all the user's tokens.
let tokens;
const photoInfoPromise = admin.database().ref(`/photos/${photoID}`).once('value')
.then(dataSnapshot => {
tripName = dataSnapshot.val().tripName;
key = dataSnapshot.val().ownerKey;
console.log("the key is", key)
return getDeviceTokensPromise = admin.database()
.ref(`/users/${key}/notificationTokens`).once('value');
///// I need to make sure that this returns the promise used in Promise.all()/////
});
return Promise.all([getDeviceTokensPromise, photoInfoPromise]).then(results => {
console.log(results)
tokensSnapshot = results[0];
// Check if there are any device tokens.
//////This is where I get the error that tokensSnapshot is undefined///////
if (!tokensSnapshot.hasChildren()) {
return console.log('There are no notification tokens to send to.');
}
// Notification details.
const payload = {
notification: {
title: 'Someone liked a photo!',
body: `${username} liked one of your photos in ${tripName}.`,
}
};
// Listing all tokens as an array.
tokens = Object.keys(tokensSnapshot.val());
// Send notifications to all tokens.
return admin.messaging().sendToDevice(tokens, payload);
});
Ok so based on your comment, here's the updated answer.
You need to chain your promises and you don't need to use Promise.all() - that function is used for a collection of asynchronous tasks that all need to complete (in parallel) before you take your next action.
Your solution should look like this:
admin.database().ref(`/photos/${photoID}`).once('value')
.then(dataSnapshot => {
// Extract your data here
const key = dataSnapshot.val().ownerKey;
// Do your other stuff here
// Now perform your next query
return admin.database().ref(`/users/${key}/notificationTokens`).once('value');
})
.then(tokenSnapshot => {
// Here you can use token Snapshot
})
.catch(error => {
console.log(error);
});
Because your second network request depends on the completion of the first, chain your promises by appending a second .then() after your first and don't terminate it with a semi-colon ;. If the new promise created within first .then() scope resolves it will call the second .then() function.
If you need to access variables from the first .then() scope in the second, then you must declare them in the general function, and assign them in the closure scopes.
You can check out more info on promise chaining here.
I have a Cloud Function used to cross reference two lists and find values that match each other across the lists. The function seems to be working properly, however in the logs I keep seeing this Error serializing return value: TypeError: Converting circular structure to JSON . Here is the function...
exports.crossReferenceContacts = functions.database.ref('/cross-ref-contacts/{userId}').onWrite(event => {
if (event.data.previous.exists()) {
return null;
}
const userContacts = event.data.val();
const completionRef = event.data.adminRef.root.child('completed-cross-ref').child(userId);
const removalRef = event.data.ref;
var contactsVerifiedOnDatabase ={};
var matchedContacts= {};
var verifiedNumsRef = event.data.adminRef.root.child('verified-phone-numbers');
return verifiedNumsRef.once('value', function(snapshot) {
contactsVerifiedOnDatabase = snapshot.val();
for (key in userContacts) {
//checks if a value for this key exists in `contactsVerifiedOnDatabase`
//if key dioes exist then add the key:value pair to matchedContacts
};
removalRef.set(null); //remove the data at the node that triggered this onWrite function
completionRef.set(matchedContacts); //write the new data to the completion-node
});
});
I tried putting return in front of completionRef.set(matchedContacts); but that still gives me the error. Not sure what I am doing wrong and how to rid the error. Thanks for your help
I was having the exact same issue when returning multiple promises that were transactions on the Firebase database. At first I was calling:
return Promise.all(promises);
My promises object is an array that I'm using where I'm pushing all jobs that need to be executed by calling promises.push(<add job here>). I guess that this is an effective way of executing the jobs since now the jobs will run in parallel.
The cloud function worked but I was getting the exact same error you describe.
But, as Michael Bleigh suggested on his comment, adding then fixed the issue and I am no longer seeing that error:
return Promise.all(promises).then(() => {
return true;
}).catch(er => {
console.error('...', er);
});
If that doesn't fix your issue, maybe you need to convert your circular object to a JSON format. An example is written here, but I haven't tried that: https://stackoverflow.com/a/42950571/658323 (it's using the circular-json library).
UPDATE December 2017: It appears that in the newest Cloud Functions version, a cloud function will expect a return value (either a Promise or a value), so return; will cause the following error: Function returned undefined, expected Promise or value although the function will be executed. Therefore when you don't return a promise and you want the cloud function to finish, you can return a random value, e.g. return true;
Try:
return verifiedNumsRef.once('value').then(function(snapshot) {
contactsVerifiedOnDatabase = snapshot.val();
for (key in userContacts) {
//checks if a value for this key exists in `contactsVerifiedOnDatabase`
//if key dioes exist then add the key:value pair to matchedContacts
};
return Promise.all([
removalRef.set(null), //remove the data at the node that triggered this onWrite function
completionRef.set(matchedContacts)
]).then(_ => true);
});
I had the same error output with a pretty similar setup and couldn't figure out how to get rid of this error.
I'm not totally sure if every essence has been captured by the previous answers so I'm leaving you my solution, maybe it helps you.
Originally my code looked like this:
return emergencyNotificationInformation.once('value', (data) => {
...
return;
});
But after adding then and catch the error did go away.
return emergencyNotificationInformation.once('value')
.then((data) => {
...
return;
})
.catch((error) => {
...
return:
});
}
We fixed a similar issue with the same error by returning Promise.resolve() at the bottom of the chain, e.g.:
return event.data.ref.parent.child('subject').once('value')
.then(snapshot => {
console.log(snapshot.val());
Promise.resolve();
}).catch(error => {
console.error(error.toString());
});