Wait for multiple Promises in Firebase Functions - firebase

I try to figure out how I can wait for several promises to be executed before the function ends.
Essentially, this is what I try to do:
if a user deletes his/her account, I want to clean up all data which is associated with him
direct data can be deleted
if a user is part of a group, the group shall still exists, if other users are in that group; otherwise the group shall be deleted as well
Here is what I tried so far:
A) Main function (starts the first level of promises):
export function cleanUpAllData(user) {
const userId = user.uid;
const promises = [];
promises.push(deleteCategoriesData(userId)); // this works fine
promises.push(deleteUserAndGroupData(userId)); // this one has other promises which still run when Promise.all() is finished
Promise.all(promises)
.then(() => {
return "ok"; // this works so far, but not all promises are resolved
})
.catch(errPromises => {
console.log("an error occured during the processing of main promises");
console.log(errPromises, errPromises.code);
return "error";
})
}
B) deleteUserAndGroupData function (the other promise is working fine): each group found in the user data starts another level of promises and also triggers a thrid level of promises (deleteGroup) - the rest is working fine
function deleteUserAndGroupData(userId) {
const promisesUserData = [];
return admin.firestore().collection('users').doc(userId).collection('groups').get()
.then(userGroups => {
userGroups.forEach(userGroupData => {
// delete group data
promisesUserData.push(deleteGroups(userId, userGroupData.id)); // here are other promises that need to be resolved - somewhere is a problem
// delete all Groups in Users (subcollection)
promisesUserData.push(deleteGroupInUser(userId, userGroupData.id)); // this works fine
});
Promise.all(promisesUserData)
.then(() => {
admin.firestore().collection('users').doc(userId).delete()
.then(() => {
return "user data deleted"; // works fine
})
.catch(() => {
console.log("an error occured during deleting of user");
return "error";
});
})
.catch(errPromises => {
console.log("an error occured during the processing of promisesUserData");
console.log(errPromises, errPromises.code);
return "error";
})
})
.catch(errUserGroups => {
console.log(errUserGroups, errUserGroups.code);
return "no groups in User";
});
}
C) deleteGroups functions: deletes the sub collections in the group (works fine) and after that the group shall be deleted if there is no other user (which does not work)
function deleteGroups(userId,groupId) {
const promisesDeleteGroups = [];
// delete groups subcollection data
promisesDeleteGroups.push(deleteGroupRole(userId, groupId));
promisesDeleteGroups.push(deleteGroupUser(userId, groupId));
return Promise.all(promisesDeleteGroups).then(() => {
checkForOthers(groupId);
}).catch(errDeleteGroupSubcollections => {
console.log("an error occured during the processing of promisesDeleteGroups");
console.log(errDeleteGroupSubcollections, errDeleteGroupSubcollections.code);
return "error";
});
}
D) checkForOthers function - checks if there is any entry in the subcollection and shall start to delete the group (but does not)
function checkForOthers(groupId) {
return admin.firestore().collection('groups').doc(groupId).collection('users').get()
.then(groupUsers => {
return "other users exist - group should not be deleted";
})
.catch(errGroupUsers => {
// console.log("no other group members - group can be deleted");
// return "no other group members - group can be deleted";
console.log(errGroupUsers, errGroupUsers.code);
checkForInvitesAndDelete(groupId);
});;
}
E) checkForInvitesAndDelete: first I want to delete another subcollection which might or might not exists, if another user has been invited to the group; then it should trigger the final group delete (which seems not to work)
function checkForInvitesAndDelete(groupId) {
const promisesInvitesAndDelete = [];
return admin.firestore().collection('groups').doc(groupId).collection('invitations').get()
.then(groupInvitations => {
console.log("delete open Group Invitations");
groupInvitations.forEach(groupInvite => {
promisesInvitesAndDelete.push(deleteGroupInvite(groupId, groupInvite.id));
});
Promise.all(promisesInvitesAndDelete)
.then(() => {
deleteGroup(groupId)
})
.catch(errPromisesInvitesAndDelete => {
console.log("an error occured during the processing of deleting group invites");
console.log(errPromisesInvitesAndDelete, errPromisesInvitesAndDelete.code);
return "error";
});
})
.catch(() => {
console.log("no open invitations");
deleteGroup(groupId);
});
}
F) deleteGroup function
function deleteGroup(groupId) {
return admin.firestore().collection('groups').doc(groupId).delete();
}
I am relatively new to programming, especially Firebase Functions, so any help would be appreciated!!
Thank you!

You are not using the return keyword everywhere, where it should be. If you do a async task, you must 'return' it someway.
Some examples:
example A: add return before Promise.all(promises)
... B: add return before Promise.all(promisesUserData)
... C: add return before checkForOthers(groupId)
... D: add return before checkForInvitesAndDelete(groupId)
... E: add return before Promise.all(promisesInvitesAndDelete) and deleteGroup(groupId)

I added the 'return' statements which helped a lot, but that was not the full answer.
In (D) I thought that if my collection has no data, it would run into the 'catch' phase, but it is not. So, I needed to check for an empty result set in my 'then' phase.
if (groupUsers.empty) {
return checkForInvitesAndDelete(groupId);
} else {
return "other users exist - group should not be deleted";
}
Same with the function (E) when there is no open invitation.

Related

How to return boolean in Angular

I have a backend method called LikeExists() to verify if a certain user has liked a certain post.
public async Task<bool> LikeExists(int postId)
{
var post = await _postRepository.GetPostByIdAsync(postId);
var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername());
if (_context.Likes.Where(i => i.PostId == post.Id && i.UserId == user.Id).FirstOrDefault() != null) return true;
return false;
}
The method works fine in Postman, but it does not do the job in Angular. If a user presses a like button I first want to check if this user has already liked this post. If he has, he will unlike it and the like will be deleted from the database. If he hasn't liked it, he will like it and the like will be saved in the database.
likeExists(){
this.postService.likeExists(this.post.id).subscribe((response: boolean) =>{
this.like = response;
});
}
likePost() {
if(this.likeExists){
this.postService.likePost(this.post.id, this.model).subscribe((response: Like) => {
this.likee = response;
console.log(response);
this.toastr.success('Liked');
}, error => {
console.log(error);
this.toastr.error(error.error);
});
} else {
this.postService.deleteLike(this.post.id).subscribe(() => {
this.toastr.success('Unliked');
}, error => {
console.log(error);
})
}
}
The problem is it always enters the if{} clause and never the else{} clause. The method below returns an Observable. I think the problem is that it must return a boolean. How can I make this work?
This is the method in the postService:
likeExists(postId: number) {
return this.http.get(this.baseUrl + 'like/exists/' + postId);
}
Try avoiding nested subscription since it will result in an unreadable and hard to maintain code, use rxjs pipes with operators instead, try something like this:
let id= this.post.id;
let likeExists$ = this.postService.likeExists(id);
likeExists$
.pipe(
switchMap(likeExists => {
if (likeExists) {
// delete like
return this.postService.deleteLike(id);
}
// otherwise addlike
return this.postService.addLike(id);
})
).subscribe(
res=> this.toastr.success('Success'),
err=> this.toastr.error('Failed')
);
or even shorter
let id= this.post.id;
let likeExists$ = this.postService.likeExists(id);
likeExists$
.pipe(switchMap(
liked => liked ? this.postService.deleteLike(id) : this.postService.addLike(id)}))
.subscribe(
res=> this.toastr.success('Success'),
err=> this.toastr.error('Failed')
);
The problem is that this.http.get is asynchronous, which means that likeExists returns before this.like is being set. You need to wait for the value to be returned in your observable. Refactor your code to something along these lines:
likePost() {
// Check to see if like exists and wait for response from server
this.postService.likeExists(this.post.id).subscribe((response: boolean) => {
this.like = response;
if (this.like) {
this.postService.likePost(this.post.id, this.model).subscribe((response: Like) => {
this.likee = response;
console.log(response);
this.toastr.success('Liked');
}, error => {
console.log(error);
this.toastr.error(error.error);
});
} else {
this.postService.deleteLike(this.post.id).subscribe(() => {
this.toastr.success('Unliked');
}, error => {
console.log(error);
})
}
});
}
Also, this is a nice guide to asynchronous concepts in general. And the RxJS docs have a bunch of helpful information to get started.

Create documents in different firestore collections, with same reference ID

My question is actually twofold, so I m not sure I should ask both in one post or create another post. Anyway, here it is:
I am creating users in firestore database. I do not want to put all details in a single document because it will be requested a lot, and all details will be retrieved, even if not needed. So I decided to create a collection members_full with all details of users I may not need often, and another collection called members_header to keep the few most important details. On creation of a new user, I want reference ID in both collections to be the same for a specific user.
- members_full -+
|
+ --- abnGMbre --- +
|
+ --- mother : 'His mom'
+ --- Father: 'daddy'
- members_header+
|
+ ---- abnGMbre -- +
|
+ ---- fullname: 'john Doe'
+ ---- pictURL: 'path to his profile pic'
I want something looking like the above.
So this is what I did in the cloud function:
/** Create / Update a member
* ------------------------- */
exports.updateMember = functions.https.onCall( (data, context) =>{
// root member and secretaries are allowed to update members
const authParams:any = {
uid: context.auth.uid,
email: context.auth.token.email,
};
// Check if user is allowed to perform operation
return checkPermission(authParams, ['root', 'secretary']).then(res => {
if(res==false){
return { // Permission denied
status: STATUS.permission_denied,
}
}
// set object to add/ update
const member:any = data;
// Check if uid of member object is present (true:update, false: create)
var fullRef : admin.firestore.DocumentReference;
var headRef : admin.firestore.DocumentReference;
var countRef: admin.firestore.DocumentReference;
var createNewMember = false;
if(member.uid!==undefined && member.uid!==null){ // update
fullRef = fsDB.collection('members_full').doc(member.uid);
headRef = fsDB.collection('members_header').doc(member.uid);
} else {
fullRef = fsDB.collection('members_full').doc();
headRef = fsDB.collection('members_header').doc(fullRef.id);
countRef = fsDB.collection('counters').doc('members');
createNewMember = true;
}
return fsDB.runTransaction(t => {
return t.get(fullRef).then(doc => {
// Update full details
t.set(fullRef, {
surname : member.surname ,
firstName : member.firstName ,
birthDate : member.birthDate ,
birthPlace : member.birthPlace ,
email : member.email ,
phone : member.phone ,
occupation : member.occupation ,
father : member.father ,
mother : member.mother ,
spouse : member.spouse ,
children : member.children ,
addressHome : member.addressHome ,
addressLocal: member.addressLocal,
contactHome : member.contactHome ,
contactLocal: member.contactLocal,
comment : member.comment ,
regDate : member.regDate ,
});
// Update header details
t.set(headRef, {
fullName : member.fullName ,
gender : member.gender ,
active : member.active ,
picURL : member.picURL ,
});
// Increment number of members
if(createNewMember ){
t.update(countRef, {count: admin.firestore.FieldValue.increment(1)});
}
}).then(() => {
return { status : STATUS.ok }
}).catch(err => {
return {
status: STATUS.fail,
message: err.message,
error: err
}
});
}).then(() => {
return { status : STATUS.ok }
}).catch(error =>{
return {
status: STATUS.fail,
message: error.message,
debug: 'run transaction err',
error: error
}
});
}).catch(err => {
return {
status: STATUS.fail,
message: err.message,
debug: 'check permission err',
error: err
}
});
});
/** Check if authenticated user's roles are among the ones allowed
* --------------------------------------------------------------- */
function checkPermission(authParams:any, allowedRoles:any[]):Promise<boolean>{
// Check if authenticated user as any of the roles in array 'allowedRoles'
return new Promise((resolve, reject) => {
// If one of allowed roles is root, check against global variables
if(allowedRoles.indexOf('root')>=0 &&
( root_auth.email.localeCompare(authParams.email)==0 ||
root_auth.uid.localeCompare(authParams.uid)==0)){
resolve(true);
}
// Get autID
const uid = authParams.uid;
// Get corresponding user in collection roles
admin.firestore().collection('userRoles').doc(uid).get().then(snap => {
// Get roles of user and compare against all roles in array 'allowedRoles'
const memRoles = snap.data().roles;
var found = false;
var zz = memRoles.length;
for(let z=0; z<zz; z++){
if(allowedRoles.indexOf(memRoles[z])){
found = true;
break;
}
}
resolve(found);
}).catch(err => {
reject(err);
});
});
}
When I call this cloud function, it only writes in document members_full, and increment number of members. It does not create entry in members_header.
My first question: where did I go wrong? the way I' m getting ID from the first document to create second document, isn't it valid?
The second question, will it be better to create subcollections rather than having 2 collections? if yes, how to do I do that in a transaction?
Help much appreciated
You need to chain the method calls in the Transaction. It is not extremely clear in the documentation, but if you look at the reference document for a Transaction (https://firebase.google.com/docs/reference/node/firebase.firestore.Transaction) you will see that the update() and set() methods return a Transaction, which is
the "Transaction instance. [and is] used for chaining method calls".
So you should adapt your code along these lines:
return fsDB.runTransaction(t => {
return t.get(fullRef)
.then(doc => {
t.set(fullRef, {
surname : member.surname ,
firstName : member.firstName
//....
})
.set(headRef, {
//....
gender : member.gender
//....
})
.update(countRef, {count: admin.firestore.FieldValue.increment(1)});
});
});
You also need to correctly chain all the different promises, as follows:
return checkPermission(authParams, ['root', 'secretary'])
.then(res => {
//...
return fsDB.runTransaction(t => {
//.....
});
.then(t => {
return { status : STATUS.ok }
})
.catch(error => {...})
However, you may use a batched write instead of a transaction, since it appears that you don't use the document returned by t.get(fullRef) in the transaction.
For your second question, IMHO there is no reason to use sub-collections instead of two (root) collections.

socket.io get value that promise returns instead of promise { pending }

in socket.io i am trying to check if a user exist easily enough to where i can just call
if(checkUserExist(uid) == 'true'){
success();
}else{
failure();
};
so i figured out i need to use promises because the function i use to get info from the database is async so i do this
function checkUserExist(uid){
return new Promise(resolve => {
webUser.findOne({ _id: uid }, function(err, uid) {
if(uid){
console.log("USER EXISTS")
resolve('true')
}if(!uid){
console.log("USER NO REAL")
resolve('false')
}
})
});
and when i'm trying to use the function like this
socket.on('getAgents',function(uid){
console.log(checkUserExist(uid))
if(checkUserExist(uid) == 'true'){
console.log('user does exist getting agents')
agentList.find({}, function(err, docs) {
docs.forEach(function(d) {
socket.emit('newAgent', d.agentName)
});
});
}else if(checkUserExist(uid) == 'false'){
console.log('invalid uid ' + uid)
socket.emit('serverError', 'Invalid UID '+ uid)
}
})
the returned value is Promise { <pending> }
i am not sure what to do i thought that it was a simple enough task but obviously i don't yet know how to do it. is there anybody out there that can help me out.
promises is a fairly new concept to me and i still don't fully understand how they work should maybe use a library like promisify?
Thanks a ton :)
So, checkUserExist() returns a promise, not a value. That's how you coded it!
And, since the value is obtained asynchronously, you can't return the value directly anyway. See the canonical answer on that issue for more explanation on that topic.
To make your code work properly, you would have to actually use the promise that your function returns:
socket.on('getAgents',function(uid){
console.log(checkUserExist(uid))
checkUserExist(uid).then(result => {
if (result) {
agentList.find({}, function(err, docs) {
if (err) {
// decide what to do if there's a DB error here
return;
}
docs.forEach(function(d) {
socket.emit('newAgent', d.agentName)
});
});
} else {
socket.emit('serverError', 'Invalid UID ' + uid)
}
}).catch(err => {
socket.emit('serverError', 'Error processing UID ' + uid);
});
});

Firebase trigger on delete

Im trying to make a function where I can identify who delete, the problem is Im not getting any answer from the firebase server, someone could help me ? Thanks
exports.deleteFunction = functions.database.ref('/clientes')
.onDelete((context) => {
// Grab the current value of what was written to the Realtime Database.
console.log("delete");
console.log(context);
});
Here is the function who is deleting
confirm = (e) => {
if (id_deleta) {
firebaseDatabase.ref('/clientes/categorias/').child(id_deleta)
.remove();
notification('success', 'Excluido com sucesso');
this.callCategoria();
} else {
notification('error', 'Ocorreu um erro, tente mais tarde');
}
}
Code correction
The onDelete event handler is defined as
function(non-null functions.database.DataSnapshot, optional non-null functions.EventContext)
So in your code above, .onDelete((context) => { should be .onDelete((snapshot, context) => {.
Getting the deleted ID
Next, if you are trying to get the value of id_deleta from the onDelete event, you can use var id_deleta = snapshot.key.
exports.deleteFunction = functions.database.ref('/clientes')
.onDelete((snapshot, context) => {
var id_deleta = snapshot.key;
console.log("deleted ID %s", id_deleta); // logs "deleted ID 1234", etc.
console.log(snapshot.val()); // logs the deleted data, no need for this
console.log(context); // logs the event context
});

Firebase: Get notified just after the firebase-function trigger promise completed

In my app I pushed some object to my firebase-database and immediately after that (after the then-promise fully filled) I fetch the object (with the returned key) from the database (with the on-value method).
In addition, I make some changes on the pushed object using the firebase-functions.
How can I receive the object (in the app) just after the changes and not before? (like other ordinary backend services)
I hope this helps you, I have not tested this piece of code but it should help you in the right direction.
Also dont use this exact code in production, there is plenty room for improvement, this is just an example code.
exports.testFunction = functions.https.onRequest((req, res) => {
if (req && req.body) {
if (
req.body.hasOwnProperty('name') &&
req.body.hasOwnProperty('age')
) {
const person = {
name: req.body['name'],
age: req.body['age']
}
// Make some changes to the person object
person['hobby'] = 'Programmer';
// Add object to FireStore
admin
.firestore()
.collection('/persons')
.add(person)
.then((success) => {
// Return the added & changed person
res.status(201).send(JSON.stringify(person));
})
.catch((error) => {
// Error
console.error('Something broke', error)
res.status(500).send();
});
}
else {
// Error
res.status(500).send({err: 'Missing property'});
}
}
else {
// Error
res.status(500).send({err: 'Missing something'});
}
});

Resources