Using wildcards in firestore get query - node.js

I want to create a cloud function in firebase that gets triggered whenever a user logs in for the first time. The function needs to add the UID from the authentication of the specific user to a specific, already existing document in firestore. The problem is that the UID needs to be added to a document of which I do not know the location. The code I have right now doesn't completely do that, but this is the part where it goes wrong. The database looks like this when simplified
organisations
[randomly generated id]
people
[randomly generated id] (in here, a specific document needs to be found based on known email
adress)
There are multiple different organisations and it is unknown to which organisation the user belongs. I thought of using a wildcard, something like the following:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
console.log('function ready');
//Detect first login from user
//if(firebase.auth.UserCredential.isNewUser()){
if(true){
//User is logged in for the first time
//const userID = firebase.auth().currentUser.UID;
//const userEmail = firebase.auth().currentUser.email;
const userID = '1234567890';
const userEmail = 'example#example.com';
//Get email, either personal or work
console.log('Taking a snapshot...');
const snapshot = db.collection('organisations/{orgID}/people').get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
console.log(doc.data());
});
});
}
I commented out some authentication-based lines for testing purposes. I know the code still runs, because hardcoding the orgID does return the right values. Also, looping trough every organisation is not an option, because I need to have the possibility of having a lot of organisations.
A lot of solutions are based on firestore triggers, like onWrite, where you can use wildcards like this.
However, I don't think that's possible in this case
The solution to the problem above:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
//Add UID to document in DB[FMIS-94]
//Detect first login from user
//if(firebase.auth.UserCredential.isNewUser()){
if(true){
//User is logged in for the first time
//const userID = firebase.auth().currentUser.UID;
//const userEmail = firebase.auth().currentUser.email;
const userID = '1234567890';
const userEmail = 'example#example.com';
var docFound = false;
//Get email, either personal or work
console.log('Taking a snapshot...');
//Test for work email
const snapshot = db.collectionGroup('people').where('email.work', '==', userEmail).get()
.then(function(querySnapshot){
querySnapshot.forEach(function(doc){
//work email found
console.log('work email found');
console.log(doc.data());
docFound = true;
const organisationID = doc.ref.parent.parent.id;
writeUID(doc.id, userID, organisationID);
});
});
if(!docFound){
//Test for personal email
const snapshot = db.collectionGroup('people').where('email.personal', '==', userEmail).get()
.then(function(querySnapshot){
querySnapshot.forEach(function(doc){
//personal email found
console.log('personal email found');
console.log(doc.data());
const organisationID = doc.ref.parent.parent.id;
writeUID(doc.id, userID, organisationID);
});
});
}
}
async function writeUID(doc, uid, organisationID){
const res = db.collection(`organisations/${organisationID}/people`).doc(doc).set({
userId: uid
}, { merge: true });
}
This was exactly what I needed, thanks for all your help everyone!

It is not possible to trigger a Cloud Function when a user logs in to your frontend application. There is no such trigger among the Firebase Authentication triggers.
If you want to update a document based on some characteristics of the user (uid or email), you can do that from the app, after the user has logged in.
You mention, in your question, "in here, a specific document needs to be found based on known email address". You should first build a query to find this document and then update it, all of that from the app.
Another classical approach is to create, for each user, a specific document which uses the user uid as document ID, for example in a users collection. It is then very easy to identify/find this document, since, as soon the user is logged in you know his uid.

I'm not sure I understand you correctly, but if you want to search across all people collections not matter what organizations document they're under, the solution is to use a collection group query for that.
db.collectionGroup('people').get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
console.log("user: "+doc.id+" in organization: "+doc.ref.parent.parent.id);
});
});
This will return a snapshot across all people collections in your entire Firestore database.

First setup Cloud Functions according to the official Documentation.
Then after setting up create functions like this:
exports.YOURFUNCTIONNAME= functions.firestore
.document('organisations/[randomly generated id]/people/[randomly generated id]')
.oncreate(res => {
const data = res.data();
const email = data.email;/----Your field name goes here-----/
/-----------------Then apply your logic here---------/
)}
This will triggers the function whenever you create the People -> Random ID

Related

Are "get" functions in Google cloud functions for firebase reading each document everytime?

I observed a huge amount of read on my firebase console and I was wondering if this might come from my "referral function".
This function works perfectly fine but I was wondering whether or not this function could end up with a crazy load of read in case of app scaling.
My question: does this function imply that every time a user comes in, it will account for a number of read equivalent to the number of users in my collection ?
Thus, as this function is an onUpdate, will it redo the job every time a document is updated ?
I would not mind some resources on the topic because I found it unclear on Firebase's website.
I hope my questions are clear!
Thank you very much!
export const onReferralInfoUpdate = functions.
firestore.document('users/{userUid}')
.onUpdate(async (change, context) => {
const before = change.before.data();
const after = change.after.data();
const currentUserUid = after["uid"];
if (before.godfather_code == after.godfather_code){
console.log('Text did not change')
return null
}
const godfatherUserSnapshot = await db.collection('users').where("referral_code", "==", after.godfather_code).get();
const godfather = godfatherUserSnapshot.docs[0].data();
const godfatherUid = godfather["uid"];
const userRef = db.collection('users').doc(after.uid);
const godfather_code = after.godfather_code
await userRef.update({godfather_code})
console.log(`the text before was >> ${before.godfather_code} << and after is ${after.godfather_code}` )
let batch = db.batch();
const updateGodfather = db.collection('users').doc(godfatherUid);
batch.update(updateGodfather, {
reward: admin.firestore.FieldValue.increment(100),
godChildUid: admin.firestore.FieldValue.arrayUnion(currentUserUid),
});
return batch.commit();
});
Yes, the where("referral_code", "==", after.godfather_code).get() will fetch all the documents matching the query every time onUpdate() function triggers and you'll be charged N reads (N = number of matched documents). The Admin SDK doesn't have any caching like Client SDKs.
Does this function imply that every time a user comes in, it will account for a number of read equivalent to the number of users in my collection ?
Not numbers of documents in the users collection, only the documents matching your query as mentioned.

Sending Notification With Firebase Cloud Functions to Specific users

I am using firebase cloud function in my firebase group chat app, Setup is already done but problem is when some one send message in any group then all user get notification for that message including non members of group.
I want to send notification to group specific users only, below is my code for firebase cloud function -
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const _ = require('lodash');
admin.initializeApp(functions.config().firebase);
exports.sendNewMessageNotification = functions.database.ref('/{pushId}').onWrite(event => {
const getValuePromise = admin.database()
.ref('messages')
.orderByKey()
.limitToLast(1)
.once('value');
return getValuePromise.then(snapshot => {
const { text, author } = _.values(snapshot.val())[0];
const payload = {
notification: {
title: text,
body: author,
icon: ''
}
};
return admin.messaging()
.sendToTopic('my-groupchat', payload);
});
});
This will be really help full, if anyway some one can suggest on this.
As per our conversation on the comments I believe that the issue is that you are using a topic that contains all the users, which is the my-groupchat topic. The solution would be to create a new topic with the users that are part of this subtopic.
As for how to create such topics, there are a couple of examples in this documentation, in there you can see that you could do it server side, or client side. In your case, you could follow the examples for the server side, since you would need to add them in bulk, but as new users are added it could be interesting to implement the client side approach.

How to delete a user and their Firestore document in a callable cloud function

I'm creating a cloud function in firebase and need some help,I'm trying to delete a user form firebase and delete his document from firestore in one cloud function.
How can I make a batch job / transaction for both auth and firestore, lets say the user tries to delete his account but for some reason the user.delete() function doesn't work (lets say it's down on firebases side for that moment). The user would get en error message that we couldn't delete his account but when he tries to login again he would also get an error because his document doesn't exist.
I looked at the firebase extension to delete user data but it doesn't delete the user account and it seems to have the same problem.
Do I need to handle such edge case in the app/cloud-functions, is it something firebase should take care of or am I just getting something wrong?
Here is my code, if it would help:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
exports.deleteUser = functions.https.onCall(async (data, context) => {
try {
const uid = context.auth.uid;
const db = admin.firestore();
const collection = db.collection("users");
await collection.doc(uid).delete();
await admin.auth.deleteUser(uid); // what if this line fails?
return "success";
} catch (err) {
console.error(err);
return "error";
}
});
This line isn't doing what you think it's doing:
const user = await admin.auth().currentUser;
user is going to be undefined, because admin.auth() doesn't have a property called currentUser (it's an Auth object). The concept of "current user" doesn't exist on backend SDKs. It's a frontend concept only. What you do have, however, is a string uid which is the UID of the authenticated user that invoked the function.
If you want to use the Firebase Admin SDK to delete the user identified by the uid string, then you just need to call deleteUser(uid):
await admin.auth().deleteUser(uid);
By the way, the Delete User Data extension doesn't have to delete the user, because it works by responding to the user deleting their own account using the client SDK. That should actually be enough to make this work.

How to update a quantity in another document when creating a new document in the firebase firestore collection?

When I create a new document in the note collection, I want to update the quantity in the info document. What am I doing wrong?
exports.addNote = functions.region('europe-west1').firestore
.collection('users/{userId}/notes').onCreate((snap,context) => {
const uid = admin.user.uid.toString();
var t;
db.collection('users').doc('{userId}').collection('info').doc('info').get((querySnapshot) => {
querySnapshot.forEach((doc) => {
t = doc.get("countMutable").toString();
});
});
let data = {
countMutable: t+1;
};
db.collection("users").doc(uid).collection("info").doc("info").update({countMutable: data.get("countMutable")});
});
You have... a lot going on here. A few problems:
You can't trigger firestore functions on collections, you have to supply a document.
It isn't clear you're being consistent about how to treat the user id.
You aren't using promises properly (you need to chain them, and return them out of the function if you want them to execute properly).
I'm not clear about the relationship between the userId context parameter and the uid you are getting from the auth object. As far as I can tell, admin.user isn't actually part of the Admin SDK.
You risk multiple function calls doing an increment at the same time giving inconsistent results, since you aren't using a transaction or the increment operation. (Learn More Here)
The document won't be created if it doesn't already exist. Maybe this is ok?
In short, this all means you can do this a lot more simply.
This should do you though. I'm assuming that the uid you actually want is actually the one on the document that is triggering the update. If not, adjust as necessary.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
exports.addNote = functions.firestore.document('users/{userId}/notes/{noteId}').onCreate((snap,context) => {
const uid = context.params.userId;
return db.collection("users").doc(uid).collection("info").doc("info").set({
countMutable: admin.firestore.FieldValue.increment(1)
}, { merge: true });
});
If you don't want to create the info document if it doesn't exist, and instead you want to get an error, you can use update instead of set:
return db.collection("users").doc(uid).collection("info").doc("info").update({
countMutable: admin.firestore.FieldValue.increment(1)
});

Get a auth user by its uid in Firestore cloud function

I have cloud function triggered when a new object is added to a collection. It looks like this:
exports.emailAdmin = functions.firestore
.document('user-books/{userId}/books/{ASIN}')
.onWrite(event => {
That event.data.data() is an object added to the sub-collection ("books"). The userId comes from the Firebase authentication system. I.e. a user signed in, and added an object to the collection "user-books" with his/her "uid".
I tried:
firestore
.collection('users')
.doc(uid)
.get()
But that, "of course", fails because I don't have a collection called "users". How do I get to the "authentication database"?
The purpose is to convert the "uid" to that person's "email".
Import firebase-admin:
import * as admin from 'firebase-admin';
Then fetch the auth user:
const uid = ...;
const authUser = await admin.auth().getUser(uid); // this returns a promise, so use await or .then()
console.log(authUser.email);
You have no access to the user collection from firebase. What I did was creating a separate User's Table where I store all the users Data. I add the new user to my collection on the onCreate Hook like this:
exports.addNewUserToUserTable = functions.auth.user()
.onCreate(event => {
let user = event.data;
let database = admin.firestore();
return database.collection('user').doc(user.uid).set({
email: user.email
});
Another problem is, that at the moment it is not possible to get the logged in user's ID on Firestore Database Triggers (It works for the real time db). I'm still waiting for this feature to be released...
It seems like your question might be incomplete- you reference an event.data.data() line that didn't appear in your code snippet. Could you review it and add it if it's missing?
Also, just to clarify your purpose- you want to be able to access the email address of a user who has been authenticated, and tag it to the book that has been added by that user?
Thanks.
Since context.auth.uid is not available for firestore. You can assign userId to document and read it in the rules.
match /collectionA/{docId} {
allow update: if request.resource.data.userId == request.auth.uid;
}
exports.createProfile = functions.firestore
.document('collectionA/{docId}')
.onCreate((snap, context) => { // or onUpdate
const newValue = snap.data();
const {userId} = newValue;
});

Resources