Firebase function - query firestore - firebase

Im trying to retrieve some data from firestore within a cloud function, but get nothing back. The same query on the client-side gives me the correct results. It's probably something small but I don't see the issue. What am I doing wrong?
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
db.settings({ timestampsInSnapshots: true });
exports.myFunction = functions.https.onCall((data, context) => {
const info = getInfo();
//do some stuff with the info
return info;
}
function getInfo() {
const query = db
.collection('info')
.where('table_nr', '==', 1)
.where('number', '<=', 25)
.orderBy('number', 'desc')
.limit(1);
const info = query.get().then(snapshot => {
snapshot.forEach(doc => {
return doc.data();
})
})
return info;
}
When I make a call to this function I get: "data: null"
let info = functions.httpsCallable('myFunction')
info().then(res => { console.log(res) })
I tried a lot of different options, like when I change the last part to:
const info = query.get().then(snapshot => {
snapshot.docs;
})
I get an array with 1 object. So I'm sure there is a document in the query with data. The console.log gives me:
{data: Array(1)}
data: Array(1)
0: {_ref: {…}, _fieldsProto: {…}, _serializer: {…}, _validator: {…},
_readTime: {…}, …}
length: 1
__proto__: Array(0)
__proto__: Object
And:
return query.get().then(querySnapshot => {
if (querySnapshot.empty) {
return { exists: false }
} else {
return { exists: true }
}
})
The console.log:
{data: {…}}
data:
exists: true
__proto__: Object
__proto__: Object
Mabye good to add that I created an (working) index for the query.

In both cases, you're returning a promise for an object that isn't what you really want to send to the client. When you write a callable, you need to return a promise that resolves to the exact JavaScript object that you want to send. You can't just return anything. What you'll have to do is convert that querySnapshot into plain old JavaScript objects that describe what you want the client to know. A querySnapshot object itself is not serializable - it is a complex object that describes many things about the query results.
First define this: What exactly do you want the client to receive? Define what the actual JavaScript object should look like. Now, convert the query results to look like that. At a minimum, you can send the entire set of documents as plain JS objects like this:
return query.get().then(querySnapshot => {
return querySnapshot.docs.map(doc => doc.data());
})
This will return an array to the client with raw document objects. But it's not clear to me that's what you want to send (since you didn't define your expectations). But it's a start.

So to give a clear example of using the .where in the Firebase Functions library for Firebase Cloud Functions
admin.firestore().collection("fruit")
.where("color", "==", "purple")
With ordering
admin.firestore().collection("fruit").orderBy("size")
.where("color", "==", "purple")
See demo doc if you want more details about how admin functions
Furthermore, the list of all query functions are found in the Query class can be used from the Firestore Functions library by using method chaining like my two examples in case you'd like things like "limit", "offset", etc.

Related

Firebase Cloud Functions: TypeError snapshot.forEach is not a function

I've been struggling to understand why my Firebase cloud function isn't working.
I'm deleting a reserved number in a collection called 'anglerNumbers' when a new user has registered and when that users' document has been created. I use this on the client to make sure a reserved number can't be used twice. I'm following the documentation here: https://firebase.google.com/docs/firestore/query-data/queries?authuser=0 (Using Node.js)
But I keep getting the Error: TypeError: snapshot.forEach is not a function
Here's the function:
exports.newUser = functions.firestore.document('users/{userId}')
.onCreate((snap, context) => {
const newUserNumber = snap.data().anglerNumber;
const anglersRef = admin.firestore().collection('anglerNumbers');
const snapshot = anglersRef.where('anglerNumber', '==', newUserNumber).get();
if (snapshot.empty) {
console.log('No matching documents.');
return;
}
snapshot.forEach(doc => {
console.log(doc.id, '=>', doc.data());
doc.delete();
});
})
It does not console log 'No matching documents'. So there are documents but I can't perform the forEach as indicated by the documentation. What am I missing? Thanks!
in this line in your code:
const snapshot = anglersRef.where('anglerNumber', '==', newUserNumber).get();
You assume that get resolves immediately to a snapshot but in fact get() returns a promise that will resolve into a snap shot. You need to wait for this async function.
Either use await if that is possible in your context or use:
anglersRef.where('anglerNumber', '==', newUserNumber).get().then((snapshot)=>{
//do you processing
});

How to load 2 different Firestore docs in one 'onUpdate' Cloud Function?

I am trying to make an "onUpdate" function that loads the document that has been updated. Then I want to load another document using the data received by the wildcards. So to summarize I want to access the document that was updated and one more that is in the same collection.
I want : /userProfiles/{doc1}/employees/{doc2} AND /userProfiles/{doc1}.
I can get them both but when I try to use the data from one, it doesn't read the previous data and gives me a ReferenceError.
The end goal is to use both these docs to send an email with nodemailer. Thanks for any help.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const nodemailer = require('nodemailer');
admin.initializeApp();
exports.testLog = functions.firestore
.document('/userProfiles/{doc1}/employees/{doc2}')
.onUpdate((change, context) => {
var info = [];
const doc1 = context.params.doc1;
const doc2 = context.params.doc2;
const db = admin.firestore();
return (
db
.collection("userProfiles")
.doc(`${doc1}`)
.get()
.then(doc => {
var email = doc.data().email;
var phone = doc.data().phone;
info.push(doc.data());
console.log(email, phone); // sees and gets info
return email, phone;
}),
db
.collection("userProfiles")
.doc(`${doc1}`)
.collection(`employees`)
.doc(`${doc2}`)
.get()
.then(doc => {
info.push(doc.data());
var Status = doc.data().Status;
console.log(phone, `${Status}`); //phone is undefined
if (`${Status}` === "Alarm") {
// replace with variables from the users settings page
console.log(`${info.phone}`); // phone is undefined
let transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 587,
secure: false,
auth: {
user: "xxxxxx#gmail.com",
pass: "xxxxxxxxxx"
}
});
// send mail with defined transport object
let mailOptions = {
from: '"Fred Foo 👻" <foo#example.com>',
to: `${info.phone}`, // tried phone as well
subject: "Hello ✔",
text: "216+?",
};
transporter.sendMail(mailOptions, error => {
if (error) {
return console.log(error);
} else {
return console.log("message sent");
}
});
}
console.log(Status);
// return
return console.log("im after the if statement. No alarm triggered");
})
.then(message => console.log(message.sid, "success"))
.catch(err => console.log(err))
);
});
So I want to get the phone number and the Status in these 2 images
The error that is returned:
ReferenceError: phone is not defined
There are two things that aren't quite working the way you expect leading to your problem:
The handling of promises isn't really passing data the way you expect -- in particular, the variables phone and email exist only in one promise handler, they aren't global in scope, so phone and email aren't being passed down the promise chain.
You don't actually need to ever read the second document, as the content is passed to you in the function itself. This actually greatly simplifies the overall thing you are doing, and makes dealing with the first point nearly trivial, since you can skip the second database call.
Look at this code where I have omitted the messaging code for clarity and just left in place most of the log messages:
exports.firestoreOnUpdateTest = functions.firestore
.document('/userProfiles/{doc1}/employees/{doc2}')
.onUpdate((change, context) => {
// var info = []; I have removed this list, it is not necessary
const doc1 = context.params.doc1;
// no need to get the doc2 parameter, as we are handed the doc itself by the function call.
const doc2content = change.after.data();
const db = admin.firestore();
return (
db
.collection("userProfiles")
.doc(`${doc1}`)
.get()
.then(doc => {
const doc1content = doc.data();
const email = doc1content.email;
const phone = doc1content.phone;
console.log(email, phone); // sees and gets info
console.log(`No need to fetch doc2, as I already have it: ${JSON.stringify(doc2content)}`);
const Status = doc2content.Status;
console.log(`email for user is still: ${email}`); // email is now defined
console.log(phone, `${Status}`); // phone is now defined
if (`${Status}` === "Alarm") {
console.log(`${phone}`); // phone is now defined
return console.log('message would be sent here - code omitted')
}
console.log(Status);
return console.log("im after the if statement. No alarm triggered");
})
.catch(err => console.error(err))
);
});
In the new version, we just store the content from the document that triggered us, including the Status parameter. We then fetch the document with the content we need -- at the higher level in the tree. Once that document is returned, we just process it and combine with the data from doc2. All the fields are now defined (assuming, of course, the database objects are well-formed).
Your messaging code would be re-inserted right were the obvious log message is.
Finally, the info list I don't think is necessary now, so I've removed it. Instead, I recommend you build what you need as you construct the message itself from the data already on hand. That said, your original code wasn't accessing it correctly (that is, as a list) anyway and may have been confusing you further.
Finally, I haven't addressed the use of the Nodemailer module as the question focused primarily on the undefined fields, but I suspect your original code may not be entirely correct either -- as it doesn't either return a promise back from sendMail() or perform an await on that call (and make the entire function async), so you will need to look at that more closely.

Chaining Firebase Firestore documents/collections

So, I have a Firestore database group like so.
companies > acme-industries > items > []
OR
collection > document > collection > document
Would it be better to just store all items inside a base collection and then add a string value to each item that defines what company it goes too? Then just query the items collection for all items linked to that company?
I am trying to retrieve the items and run them through a forEach in my firebase function. I have tried two different approaches and watched multiple videos and still am not getting results.
First Attempt Code Block
This resulted in a 500 Server Error with no explanation returned.
const itemQuerySnapshot = db.collection('companies').doc(data.userData.company).collection('items').get();
const items: any = [];
itemQuerySnapshot.forEach((doc:any) => {
console.log('doc', doc.data());
items.push({
id: doc.id,
data: doc.data()
});
});
response.json(items);
Second Attempt Code Block
This resulted in the No Such Documents! being returned
const itemRef = db.collection('companies').doc(data.userData.company).collection('items');
itemRef.get().then((doc:any) => {
if(!doc.exists) {
response.send('No such documents!');
} else {
response.send('Document Data: '+ doc.data());
}
}).catch((err:any) => {
response.status(500).send(err);
});
I am expecting something like an array of all the items to be returned from this call. I'm completely new to Firebase Firestore, what am I missing here?
UPDATE
I replaced my code with a third attempt code block and I got success with the console.log(doc.data()). However, the items object still returns empty. Is this because it's returning before the for each is done? If so, how would you prevent that to ensure every item that should be returned is?
const items: any = [];
const userRef = db.collection("companies").doc(data.userData.company);
const itemsRef = userRef.collection("items");
itemsRef
.get()
.then((snapshot: any) => {
snapshot.forEach((doc: any) => {
console.log(doc.data());
items.push({
id: doc.id,
data: doc.data()
});
});
})
.catch((err: any) => {
response.status(500).send(err);
});
response.json(items);
How would you add one more document into the mix? Say you want to get a single item. How would you do that? The following always results in Item does not exist being returned from my function.
const companyRef = db.collection('companies').doc(data.userData.company);
const itemRef = companyRef.collection('items');
const item = itemRef.where('number', '==', itemSku).get();
I must be doing something incredibly wrong here because all the videos are telling me it's incredibly easy to fetch data from Firestore. But I have yet to see that.
get returns a Promise , the callback of then function will be called once the data ready from firestore .
the line response.json(items); will be called before the items array collected correctly.
you need to move this line inside the then callback
checkout this :
.then((snapshot: any) => {
snapshot.forEach((doc: any) => {
console.log(doc.data());
items.push({
id: doc.id,
data: doc.data()
});
});
response.json(items); //items ARRAY IS READY , YOU CAN SEND YOUR RESPONSE HERE
})

Using a callable function to send data back to the client from Firebase

I have created a callable Cloud Function to read data from Firebase and send back the results to the client, however, only "null" is being returned to the client.
exports.user_get = functions.https.onCall((data, context) => {
if (context.auth && data) {
return admin.firestore().doc("users/" + context.auth.uid).get()
.then(function (doc) {
return { doc.data() };
})
.catch(function (error) {
console.log(error);
return error;
})
} return
});
I just reproduced your case connecting from a Cloud Function with a Firestore database and retriving data. As I can see you are trying to access the field in a wrong way when you are using "users/" + context.auth.uid, the method can't find the field so its returning a null value.
I just followed this Quickstart using a server client library documentation to populate a Firestore database and make a Get from it with node.js.
After that i followed this Deploying from GCP Console documentation in order to deploy a HTTP triggered Cloud Function with the following function
exports.helloWorld = (req, res) => {
firestore.collection('users').get()
.then((snapshot) => {
snapshot.forEach((doc) => {
console.log(doc.id, '=>', doc.data().born);
let ans = {
date : doc.data().born
};
res.status(200).send(ans);
});
})
And this is returning the desired field.
You can take a look of my entire example code here
This is because you are making a query from a database firestore, however the cloud support team has made it very cool to protect your applications from data leakages and so in a callable function as the name suggest you can only return data you passed to the same callable function through the data parameter and nothing else. if you try to access a database i suggest you use an onRequest Function and use the endpoint to get you data. that way you not only protect your database but avoid data and memory leakage.
examples of what you can return from a callable function
exports.sayHello = functions.https.onCall((data, context) => {
const name = data.name;
console.log(`hello ${name}`);
return `It was really fun working with you ${name}`;
});
first create a function in your index.js file and accept data through the data parameter but as i said you can only return data you passed through the data parameter.
now call the function
this is in the frontend code (attach an event listener to a button or something and trigger it
/* jsut say hello from firebase */
callButton.addEventListener('click', () => {
const sayHello = firebase.functions().httpsCallable('getAllUsers');
sayHello().then(resutls => {
console.log("users >>> ", resutls);
});
});
you can get your data using an onRequest like so
/* get users */
exports.getAllUsers = functions.https.onRequest((request, response) => {
cors(request, response, () => {
const data = admin.firestore().collection("users");
const users = [];
data.get().then((snapshot) => {
snapshot.docs.forEach((doc) => {
users.push(doc.data());
});
return response.status(200).send(users);
});
});
});
using a fetch() in your frontend code to get the response of the new onRequest function you can get the endpoint to the function in your firebase console dashboard.
but not that to hit the endpoint from your frontend code you need to add cors to your firebase cloud functions to allow access to the endpoint.
you can do that by just adding this line to the top of your index.js file of the firebase functions directory
const cors = require("cors")({origin: true});

Query firestore to get all tokens from userid's

In my cloud function i have an array that contains all userId's that need to get a cloud-message(notification)
const aNotify = [{id: 'id001', text: 'specialTextFor001'}, {id: 'id002', text: 'specialTextFor002'};
This is how the devices collection looks like. the Document ID is the token ID but to find them i need to query on the userId
Is it possible to do it through the DB like with a where clause or do I need to do this by getting all devices and in cloud method do a foreach... ?
In order to find a device document corresponding to a userId, you have to use a simple query like:
const db = admin.firestore();
db.collection('devices').where("userId", "==", element.id).get();
see the corresponding doc here.
Since you need to make a query for each element of the aNotify array, you need to use Promise.all(), since get() returns a Promise.
Something like the following will work. You have to adapt it in order to correctly return the promises in your Cloud Function (since you didn't share your Cloud Function code it is difficult to give more guidance on this point).
const db = admin.firestore();
var aNotify = [{ id: 'id001', text: 'specialTextFor001' }, { id: 'id002', text: 'specialTextFor002' }];
var promises = []
aNotify.forEach(function (element) {
promises.push(db.collection('devices').where("userId", "==", element.id).get());
});
return Promise.all(promises)
.then(results => {
results.forEach(querySnapshot => {
querySnapshot.forEach(function (doc) {
console.log(doc.id, " => ", doc.data());
//here, either send a notification for each user of populate an array, or....
//e.g. return admin.messaging().sendToDevice(doc.data().token, ....);
});
});
});
Note that the results array has exactly the same order than the promises array. So it is not complicated to get the text property of the corresponding object of the aNotify array when you send the notifications.

Resources