Access a cloud firestore document within a cloud function - firebase

Within my Google Firebase Firstore database I would like to gather aggregative data such as how many documents a collection has. Since Firestore does not provide aggregative queries I'm attempting to write a cloud function that will increment a field every time a document is added to the database which will contain the number of documents a collection has.
The problem I'm having is I cannot for the life of me figure out how to grab documents from Firestore within a cloud function using nodejs.
Here's what I'm doing:
At the top of my index.js file I configure the admin SDK and what have you like this:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
Then for my cloud function I do this:
exports.createPost = functions.firestore
.document('posts/{post_id}')
.onCreate(event => {
// Get the post object
var post = event.data.data();
var senderID = post["sender_id"]; // This is not null
// Access to user's document and the main ledger document
const userDocRef = admin.database().ref('/users').orderByChild("user_id").equalTo(senderID).once('value')
const ledgerDocRef = admin.database().ref('/posts/ledger').once('value');
return Promise.all([userDocRef, ledgerDocRef]).then(snapshot => {
const user = snapshot[0].val();
const ledger = snapshot[1].val();
console.log("user => "+user); // Logs: user => null
console.log("ledger => "+ledger); // Logs: ledger => null
const userPostCount = user["user_name"];
const globalPostCount = ledger["global_post_count"] + 1;
const userUpdate = user.update({"post_count" : userPostCount});
const ledgerUpdate = ledger.update({"global_post_count" : globalPostCount});
return Promise.all([userUpdate, ledgerUpdate]);
});
});
I end up with the error:
TypeError: Cannot read property 'global_post_count' of null
at Promise.all.then.snapshot
Which I figure means something is wrong with my query but I don't know what. Both the users and posts are root level collections.
Im also getting a warning that says:
Billing account not configured. External network is not accessible and
quotas are severely limited.
From what I've read online, I don't think that effects it but I thought it was worth noting.
Please help.

Looks like you've written a Firestore trigger, but are then reaching into Realtime Database for queries:
const userDocRef = admin.database().ref('/users').orderByChild("user_id").equalTo(senderID).once('value')
const ledgerDocRef = admin.database().ref('/posts/ledger').once('value');
If your RTDB is empty, these queries will also end up empty.
To query Firestore, you need to be using admin.firestore() instead of admin.database(). Firestore has a mostly different API (via the Cloud SDK I just linked) than RTDB, but they are similar in some ways.

Related

How to check if client's contacts are using my app?

I'm currently developing an app using Firebase.
My Firestore Database looks like below:
Once the user passes the Firebase authentication procedure, I'm creating a user document with a field "Phone:" which contains his phone number. Basically, everyone who is gonna using the app will be listed in the database.
And here is my challenge:
I'm using the plugin easy_contact_picker to store all the contacts of the users device to a List.
How can I find out whether the users contacts are using the app or whether they are listed in the database?
My goal is create a contact List Widget which shows me all my contacts. But those contacts which are using the app or which are listed in the database, should be highlighted or marked particularly.
Which is the best way to realize that if we consider that millions of users (to minimize computing power)are listed in the database?
Anyone has an idea?
Thanks a lot
First of all try to awoid giving everyone access to read all users. That is something most ppl do when handling such a problem. The do it because the query over all users won't work if you don't give the rights to read all of them.
Because of security reasons I would move the logic for checking if a user exists into callable function (not a http function!). That way you can call it inside of your app and check for a single user or multiple of them in an array. That would depend how your frontend would handle it.
Very importand would be to store all phone numbers in the absolute same format. That way you could query for them. Regardless of the number of users you could always find a specific one like here:
var citiesRef = db.collection("users");
var query = citiesRef.where("Phone", "==", "+4912345679");
The numbers need to be absolutely the same without any emtpy spaces - chars and the +49 or 0049 also needs to be the same.
You could create two callable funcitons. One to check if a single user exists in your app and another where you send an array of phone numbers and you get an array back. The cloud function can use Promise.all to performe such queries in parallel so you get your responce quite fast.
I'm using a similar approach to add users in my app as admins to specific groups where you just enter the email of the user and if he is in the app he will be added. I not he get's an invitation on the email to join the App.
With the help of Tarik's answer, Ayrix and I came up with the following solution.
Important: Read Tarik's answer for more information.
Client: callable_compare_contacts.dart
import 'package:cloud_functions/cloud_functions.dart';
Future<List<Object>> getMembersByPhoneNumber(List<String> allPhoneNumbers) async {
HttpsCallable callable = FirebaseFunctions.instance.httpsCallable('membersByPhoneNumber');
final results = await callable.call(<String, dynamic>{'allPhoneNumbers': allPhoneNumbers});
return results.data;
}
Server: index.js
const functions = require("firebase-functions");
const admin = require("firebase-admin");
if (admin.apps.length === 0) {
admin.initializeApp({
credential: admin.credential.applicationDefault(),
});
}
exports.membersByPhoneNumber = functions.https.onCall((data, context) => {
return new Promise((resolve, reject) => {
if (!data || !data.allPhoneNumbers.length) return resolve([]);
const phoneNumbers = data.allPhoneNumbers;
// TODO: different scope? move vars for future use
const db = admin.firestore();
const collectionRef = db.collection("User");
let batches = [];
// because of wrong eslint parsing (dirty)
batches = [];
while (phoneNumbers.length) {
// firestore limits batches to 10
const batch = phoneNumbers.splice(0, 10);
// add the batch request to to a queue
batches.push(
new Promise((response) => {
collectionRef.where("Phone", "in", [...batch]).get()
.then((results) =>
response(results.docs.map(function(result) {
return result.data().Phone;
} )));
})
);
}
// response / return to client
Promise.all(batches).then(function(content) {
// console.log("content.flat()");
// console.log(content.flat());
return resolve(content.flat());
});
});
});
Note: This is our first callable/cloud function .. so Suggestions for changes are welcome.

Firebase Functions - Function not being called

I have created the following functions:
It has been deployed and is there on the firebase hosting, but it just doesn't ever get called (usage is 0)...
It should be called when a user document is changed and then update a field to null if it wasn't already null.
Can anyone see why this is not running?
exports.deleteField = functions.database.ref('/Users/{userID}')
.onUpdate((change, context) => {
const overrideTag = change.after.data().overrideTag
if (overrideTag !== null) {
const db = admin.firestore()
db.collection('Users').doc(userID).set({ overrideTag: null })
}
})
Kind Regards,
Josh
Your function is configured to trigger on changes to a node called "Users" in Realtime Database. Realtime Database doesn't have "documents". However, Firestore does have documents. If you meant to trigger when a document is changed in Firestore, you will have to write a Firestore trigger instead. It will use functions.firestore instead of functions.database.

Cloud Functions Update Sub-collections

I'm trying to create a cloud function to trigger every time a product on my project gets updated. Here is the idea.
I have 2 collections, stores and products.
Inside the stores collection, there is a sub-collection called products that contains all the products that the store sells. The data 'gets fed' by copying specific items from the main products root collection
The idea is the my project gets a good performance as well as cost effective.
In order for this to work, I need to create a cloud function to be triggered every time a product gets modified and query all the stores that has that same product id and update the data.
I'm having a really hard time with that. Can anybody shine a light here for me? This is my cloud function.
// Exporting the function
export const onProductChange = functions.firestore
.document('products/{productId}')
// Call the update method
.onUpdate(async (snap, context) => {
// Get the product ID
const productID = context.params.productID;
// Query for all the collections with the specific product ID.
const resultSnapshot = await db.collectionGroup('products')
.where('id', '==', productID).get();
// Filter for the collections with the 'products' root and return an array.
const snaphotsInStoreSubcollection = resultSnapshot.docs.filter(
(snapshot: any) => {
return snapshot.ref.parent === 'products';
});
const batch = db.batch();
// Takes each product and update
snaphotsInStoreSubcollection.forEach((el: any) => {
batch.set(el.ref, snaphotsInStoreSubcollection.product);
});
await batch.commit();
});
error on cloud function console
Error: Value for argument "value" is not a valid query constraint. Cannot use "undefined" as a Firestore value. at Object.validateUserInput (/srv/node_modules/#google-cloud/firestore/build/src/serializer.js:273:15) at validateQueryValue (/srv/node_modules/#google-cloud/firestore/build/src/reference.js:1844:18) at Query.where (/srv/node_modules/#google-cloud/firestore/build/src/reference.js:956:9) at exports.onProductChange.functions.firestore.document.onUpdate (/srv/lib/product-update.js:29:10) at cloudFunction (/srv/node_modules/firebase-functions/lib/cloud-functions.js:131:23) at /worker/worker.js:825:24 at at process._tickDomainCallback (internal/process/next_tick.js:229:7)
I would suggest you take a look at this documentation and specially in Event Triggers.
Let me know if this helps.
I think the snapshotsInStoreSubcollection.product is undefined
batch.set(el.ref, snaphotsInStoreSubcollection.product);
A snapshot is a document and its data is snapshot.data()
You cannot set undefined as a value in firestore and you are attempting to

minimize time operation in firebase/firestore

I build react native app with firebase & firestore.
what I'm looking to do is, when user open app, to insert/update his status to 'online' (kind of presence system), when user close app, his status 'offline'.
I did it with firebase.database.onDisconnect(), it works fine.
this is the function
async signupAnonymous() {
const user = await firebase.auth().signInAnonymouslyAndRetrieveData();
this.uid = firebase.auth().currentUser.uid
this.userStatusDatabaseRef = firebase.database().ref(`UserStatus/${this.uid}`);
this.userStatusFirestoreRef = firebase.firestore().doc(`UserStatus/${this.uid}`);
firebase.database().ref('.info/connected').on('value', async connected => {
if (connected.val() === false) {
// this.userStatusFirestoreRef.set({ state: 'offline', last_changed: firebase.firestore.FieldValue.serverTimestamp()},{merge:true});
return;
}
await firebase.database().ref(`UserStatus/${this.uid}`).onDisconnect().set({ state: 'offline', last_changed: firebase.firestore.FieldValue.serverTimestamp() },{merge:true});
this.userStatusDatabaseRef.set({ state: 'online', last_changed: firebase.firestore.FieldValue.serverTimestamp() },{merge:true});
// this.userStatusFirestoreRef.set({ state: 'online',last_changed: firebase.firestore.FieldValue.serverTimestamp() },{merge:true});
});
}
after that, I did trigger to insert data into firestore(because I want to work with firestore), this is the function(works fine, BUT it takes 3-4 sec)
module.exports.onUserStatusChanged = functions.database
.ref('/UserStatus/{uid}').onUpdate((change,context) => {
const eventStatus = change.after.val();
const userStatusFirestoreRef = firestore.doc(`UserStatus/${context.params.uid}`);
return change.after.ref.once("value").then((statusSnapshot) => {
return statusSnapshot.val();
}).then((status) => {
console.log(status, eventStatus);
if (status.last_changed > eventStatus.last_changed) return status;
eventStatus.last_changed = new Date(eventStatus.last_changed);
//return userStatusFirestoreRef.set(eventStatus);
return userStatusFirestoreRef.set(eventStatus,{merge:true});
});
});
then after that, I want to calculate the online users in app, so I did trigger when write new data to node of firestore so it calculate the size of online users by query.(it works fine but takes 4-7 sec)
module.exports.countOnlineUsers = functions.firestore.document('/UserStatus/{uid}').onWrite((change,context) => {
console.log('userStatus')
const userOnlineCounterRef = firestore.doc('Counters/onlineUsersCounter');
const docRef = firestore.collection('UserStatus').where('state','==','online').get().then(e=>{
let count = e.size;
console.log('count',count)
return userOnlineCounterRef.update({count})
})
return Promise.resolve({success:'added'})
})
then into my react native app
I get the count of online users
this.unsubscribe = firebase.firestore().doc(`Counters/onlineUsersCounter`).onSnapshot(doc=>{
console.log('count',doc.data().count)
})
All the operations takes about 12 sec. it's too much for me, it's online app
my firebase structure
what I'm doing wrong? maybe there is unnecessary function or something?
My final goals:
minimize time operation.
get online users count (with listener-each
change, it will update in app)
update user status.
if there are other way to do that, I would love to know.
Cloud Functions go into a 'cold start' mode, where they take some time to boot up. This is the only reason I can think of that it would take that long. Stack Overflow: Firebase Cloud Functions Is Very Slow
But your cloud function only needs to write to Firestore on log out to
catch the case where your user closes the app. You can write to it directly on log in from your client
with auth().onAuthStateChange().
You could also just always read who is logged in or out directly from the
realtime database and use Firestore for the rest of your data.
You can rearrange your data so that instead of a 'UserStatus' collection you have an 'OnlineUsers' collection containing only online users, kept in sync by deleting the documents on log out. Then it won't take a query operation to get them. The query's impact on your performance is likely minimal, but this would perform better with a large number of users.
The documentation also has a guide that may be useful: Firebase Docs: Build Presence in Cloud Firestore

How to count documents in cloud firestore?

I'm having issues implementing a counter using the firestore DB.
I have a collection of documents where the document has a field that is updated at some point.
Collection
Object
Field:true
Object
Field:false
Object
Field:true
I need to count all the fields where the value is equals to true. The app is updated from multiple devices at the same time.
How can I accomplish this? Do I need to use the distributed counters approach o just with a cloud function would be enough
Cloud Firestore has a limit of one write per document/second. I would not advise having multiple users writing to the same document at the same time. You should definitely use the Distributed Counters method.
Use cloud functions. simple example:
// database in the 'posts' collection
{
title: "My great post",
categories: {
"technology": true,
"opinion": false,
"cats": true
}
Cloud functions script:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp({
credential: admin.credential.applicationDefault()
});
const docRef = admin.firestore().collection('post')
.where('categories', '==', true)
.get()
.then((result) => {
res.send(result.size()); //return the count
}).catch(function(error){
console.log("got an error",error);
})
exports.countFunction = functions.https.onRequest(...);

Resources