How to get inner collection in firebase firestore - node.js

I'm trying to get the device token of a particular user in firestore which is stored in tokens collection inside either "clients" or "lawyers" collection.
When i remove the second .collection("tokens") from the chain i get the user object back but with the token collection in the chain i just can't seem to get any user (client or lawyer) back, even though the user and it's token exist. what am i doing wrong
exports.onReceiveChatMessage = functions.database
.ref("/messages/{uid}")
.onCreate(async (snapshot, context) => {
const newMessage = snapshot.val();
console.log("NEW_MESSAGE", newMessage);
const senderName = newMessage.sender_name;
const messageContent = newMessage.content;
console.log("SENDER'S_NAME", senderName);
console.log("MESSAGE_BODY", messageContent);
const uid = context.params.uid;
console.log("RECEIVERS_ID", uid);
if (newMessage.sender_id == uid) {
//if sender is receiver, don't send notification
console.log("sender is receiver, dont send notification...");
return;
} else if (newMessage.type === "text") {
console.log(
"LETS LOOK FOR THIS USER, STARTING WITH CLIENTS COLLECTION..."
);
let userDeviceToken;
await firestore
.collection("clients")
.doc(uid)
.collection("tokens")
.get()
.then(async (snapshot) => {
if (!snapshot.exists) {
console.log(
"USER NOT FOUND IN CLIENTS COLLECTION, LETS CHECK LAWYERS..."
);
await firestore
.collection("lawyers")
.doc(uid)
.collection("tokens")
.get()
.then((snapshot) => {
if (!snapshot.exists) {
console.log(
"SORRY!!!, USER NOT FOUND IN LAWYERS COLLECTION EITHER"
);
return;
} else {
snapshot.forEach((doc) => {
console.log("LAWYER_USER_TOKEN=>", doc.data());
userDeviceToken = doc.data().token;
});
}
});
} else {
snapshot.forEach((doc) => {
console.log("CLIENT_USER_TOKEN=>", doc.data());
userDeviceToken = doc.data().token;
});
}
});
// console.log("CLIENT_DEVICE_TOKEN", userDeviceToken);
} else if (newMessage.type === "video_session") {
}
})

This line
if (!snapshot.exists) {
should be:
if (snapshot.empty) {
because you're calling get() on a CollectionReference (which returns a QuerySnapshot), not on a DocumentReference (which returns a DocumentSnapshot).
If you remove the .collection('tokens') from the chain in your example, it does work because a DocumentSnapshot does have the member exists, but a CollectionReference doesn't.
Take a look at their members here:
https://googleapis.dev/nodejs/firestore/latest/CollectionReference.html#get
Then:
https://googleapis.dev/nodejs/firestore/latest/QuerySnapshot.html
As a suggestion, I used to confuse snapshots and got that problem because of working with Javascript instead of Typescript. So I got used to calling the result snap when called on a document, and snaps when called on collections. That reminds me of what kind of response I'm working on. Like this:
// single document, returns a DocumentSnapshot
const snap = await db.collection('xyz').doc('123').get();
if (snap.exists) {
snap.data()...
}
// multiple documents, returns a QuerySnapshot
const snaps = await db.collection('xyz').get();
if (!snaps.empty) { // 'if' actually not needed if iterating over docs
snaps.forEach(...);
// or, if you need to await, you can't use the .forEach loop, use a plain for:
for (const snap of snaps.docs) {
await whatever(snap);
}
}

Related

retrieve Firestore document from onCreate trigger with Cloud Functions

I need to retrieve information from a Firestore Document when another document is created. When I try to do this I get hit with an error about the function not being async. It has been so long since I used javascript I am basically a novice again and have no idea how to fix this.
ok, so I am using Firebase Cloud Functions and the function in question is a Firestore .onCreate() trigger.
When the function is triggered I set a sender variable (which is the document ID from a different collection that I need to retrieve)
then I try to get the document as per the documentation.
The function ends up like this:
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}')
.onCreate((snap, context) => {
// when friend request is created
data = doc.data()//get request data
sender = data["sender"]//get request sender from data
const requestRef = db.collection('User').doc(sender);
const doc = await requestRef.get();//get user data of sender
if (!doc.exists) {
console.log('No such document!');
} else {
console.log('Document data:', doc.data());
}
});
when I run this in the emulator I get this error:
const doc = await requestRef.get();//get user data of sender
^^^^^
SyntaxError: await is only valid in async functions and the top level bodies of modules
I have absolutely no idea where to go from here.
Can anyone help me with this?
Thanks
The await keyword is valid only in an async function.
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}')
.onCreate(async (snap, context) => {
// ^^^^^
})
If you are (or need to) use synchronous function then you would have to use promise chaining.
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}')
.onCreate((snap, context) => {
return requestRef.get().then((snapshot) => {
if (snapshot.exists) { ... }
})
})
Apart from that, the order of variables/statements looks incorrect. With the current code (as in original question), you may end up getting an error: "Cannot access 'doc' before initialization" Try refactoring it like this:
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}')
.onCreate(async (snap, context) => {
// accessing data from newly created doc
const newDocData = snap.data()
// const sender = "" // ??
const requestRef = db.collection('User').doc(sender);
const doc = await requestRef.get();//get user data of sender
if (!doc.exists) {
console.log('No such document!');
} else {
console.log('Document data:', doc.data());
}
})
Where is the sender coming from? I've just commented it above but if the sender is present in new document then you can access it by: const sender = newDocData.sender
If your using await you have to specify that function is asynchronous. Otherwise it will throw error.
exports.pushFriendRequestNotification = functions.firestore.document('friends/{friendID}').onCreate(async (snap, context) => {
// when friend request is created
data = doc.data()//get request data
sender = data["sender"]//get request sender from data
const requestRef = db.collection('User').doc(sender);
const doc = await requestRef.get();//get user data of sender
if (!doc.exists) {
console.log('No such document!');
} else {
console.log('Document data:', doc.data());
}
});
Yet some of your references is unknown to us. Maybe this code is not completed.
The main point is you need to understand when you can access async/await or Promise
All await methods must be inside an async block or be handled in an async manor using .then() promises
in this case, the parent function is on this line .onCreate((snap, context) => {
simply inserting an async at the start of the variables will upgrade the arrow function to an async arrow function
.onCreate(async (snap, context) => {

Get the first document from Firestore subcollection with primary document?

I am using Node.js (which I am very new at) and Google Cloud Firestore database to save documents according to:
Users
Tweets
One Users document, i.e. a User, has many Tweets in a subcollection 'Tweets'. I am interested in retrieving a User together with the last Tweet in the subcollection so I get a JSON-file like this. In other words, this is what I want to get:
users: {
{
name:'john',
twitter_username:'johnny',
tweets: {
text: "I am called johnny, this is my tweet",
created_at: "2021-06-29 12:00:00"
},
},
{
name:'anne',
twitter_username:'anne',
tweets: {
text: "I am called anne, this is another tweet",
created_at: "2019-06-28 12:00:00"
},
}
}
I have this function:
function getUserData() {
return db.collection('users').get()
.then((querySnapshot) => {
var docs = querySnapshot.docs.map(doc => [doc.data(), doc.id]);
//console.log(docs);
return docs
which, if I could fetch and replace doc.id (i.e. the User document ID) with the last tweet, would solve it I guess. But how can I do that?
Any other solution, possibly with for loops, would be fine as well. I have spent hours on this seemingly easy problem but can't get it to return both the User-data and the tweet-data.
Edit 2:
I realized I could do this:
function getUserData() {
return db.collection('users').get()
.then((querySnapshot) => {
var docs = querySnapshot.docs.map(doc => [doc.data(), doc.id, getStuff(doc.id)])
console.log(docs)
return docs
});
}
function getStuff(doc_id) {
return db.collection('users').doc(doc_id).collection('tweets').limit(1).get()
.then((querySnapshot) => {
var docs = querySnapshot.docs.map(doc => doc.data());
console.log("TWEETS", doc_id, docs[0]['text']);
return docs[0]['text']
});
}
which produces a log result as:
TWEETS DAU86mxIhmD6qQQpH4F God’s country!
TWEETS JQHTO0jUjAodMQMR6wI I’m almost there Danny!
from the getStuff-function.
The only issue now is that I can't get the map function to wait for getStuff so the return docs return a Promise { <pending> } for getStuff(doc.id).
I am not to familiar with Promises and await/async and I can't get that to work. Solving this Promise pending -> twitter text would then solve my problem. How do I do that?
If you want to get the data of a single user you could write the code like this:
const getUserData = async (userUid) => {
const userSnap = await db.collection("users").doc(userUid).get();
const tweetSnaps = await db
.collection("tweets")
.orderBy("created_at", "desc")
.limit(1)
.get();
let tweet = {};
tweetSnaps.forEach((doc) => {
tweet = doc.data();
});
return {
...userSnap.data(),
...tweet,
};
};
We first get the user and then query for the last tweet and get that. We sort the tweets collection by created_at and limit it for a single doc.
If you want to get the same data for all users at once we would need to change the code a little bit but the logic would be the same.
If the data is saved in separate collections you can't get them in a single database request.
UPDATE for Edit 2
Here your code how it should look like with correct async/await:
const getUserData = async () => {
const querySnapshot = await db.collection("users").get();
const docs = querySnapshot.docs;
for (let i = 0; i < docs.length; i++) {
const element = docs[i];
doc.data(),
doc.id,
await getStuff(doc.id),
}
console.log(docs);
return docs;
};
const getStuff = async (doc_id) => {
const querySnapshot = await db
.collection("users")
.doc(doc_id)
.collection("tweets")
.limit(1)
.get();
var docs = querySnapshot.docs.map((doc) => doc.data());
console.log("TWEETS", doc_id, docs[0]["text"]);
return docs[0]["text"];
};

Waiting for async call to finish so I can use the result, its not working?

I tried querying Firestore using .get():
//Cloud function to perform leaderboard calculation
exports.scheduledLeaderboardFunction = functions.pubsub.schedule('00 21 * * *')
.timeZone('America/Los_Angeles')
.onRun(async (context) => {
var globalPostsArray = [];
try {
await admin.firestore()
.collection('globalPosts')
.get()
.then((querySnapshot) => {
if(querySnapshot.exists) {
querySnapshot.forEach((res) => {
const {
//Fields
//Removed
} = res.data();
globalPostsArray.push({
//Fields
//Removed
});
});
}
else {
throw new Error("Data doesn't exist") <-------- This error is thrown
}
return null
})
.then(() => {
if (globalPostsArray.length > 0) {
console.log(globalPostsArray)
}
else {
throw new Error("length not greater than 0")
}
return null;
})
}
catch(error) {
console.log(error);
}
return null;
});
but in the firebase cloud log, I get the following error printed:
Error: Data doesn't exist
Which means querySnapshot doesn't exist when I use .get() (the error is thrown).
globalPosts, the collection I am querying, is NOT empty
if I can make .get() to work, that would work for my issue, since I am not waiting for updates which is what .onSnapshot() is good for.
summary: onSnapshot() worked for fetching the data from Firestore, but I can't use .then() to wait for the data so I can finish up the work. get() isn't working, but I can use .then() to wait for the collection if it does work.
How can I fix my issue?
EDIT: changed the function to this, but its still not working
//Cloud function to perform leaderboard calculation
exports.scheduledLeaderboardFunction = functions.pubsub.schedule('00 21 * * *')
.timeZone('America/Los_Angeles')
.onRun(async (context) => {
try {
await admin.firestore().collection('globalPosts').orderBy("date_created", "desc")
.get()
.then(function(querySnapshot) {
if(querySnapshot) {
querySnapshot.forEach(function(doc) {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});
return null
}
else {
throw new Error("Data doesn't exist")
}
})
}
catch(error) {
console.log(error);
}
return null;
});
Proof globalPosts isn't empty:
Same error:
Error: Data doesn't exist
I think you're on the right track with .get(), but don't mix async syntax.
const shortSnapshot = await firebase.firestore()
.collection("stuff")
.where("stuff", "==", "new")
.limit(1)
.get();
if (shortSnapshot.empty) {
console.error("No stuff");
return response.sendStatus(404);
}
console.log(shortSnapshot.docs[0].data());
There is no .then for .onSnapShot - it attaches a Listener Function, and returns synchronously, with no data - it does NOT return a promise.
the anonymous function you created and passed - it begins (querySnapshot) => { is called with a querySnapshot as it's argument when the Listener is triggered. Judging by the other parts of the code, you're attempting a cloud function? As a general rule, listeners are NOT the correct approach for a Cloud Function, as these functions are intended to be short-lived.
The message you are shown is precisely what would be expected if your collection 'globalPosts' were empty - and you show nothing here to indicate that this is an error.
Removing the try, catch fixed my issue!
await admin.firestore().collection('globalPosts').orderBy("date_created", "desc")
.get()
.then(function(querySnapshot) {
if(querySnapshot) {
querySnapshot.forEach(function(doc) {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});
return null
}
else {
throw new Error("Data doesn't exist")
}
})
The error of "data doesn't exist" is no longer an issue

Cloud function: No document found

I'm using cloud function to check if specific document exists but it did not work. No document found even there was. The code is below:
exports.onUserAppCreated = functions.firestore.document('users/{userId}/first_col/{colId}')
.onCreate((snap, context) => {
const data = snap.data();
const colId = data.colId;
console.log(colId);
var docRef = db.collection('users/{userId}/somecollection');
let query = docRef.where('colId', '==', colId).get().then(doc => {
if (doc.exists) {
console.log("Document data:", doc.data());
let tracksRef = db.collection('users/{userId}/othercolllection');
tracksRef.where('otherId', '==', colId).get()
.then(transSnapshot => {
if (!transSnapshot.exists) {
transSnapshot.ref.set({
otherId: colId,
time:admin.firestore.FieldValue.serverTimestamp()
});
}
return transSnapshot;
}).catch(error => {
console.log(error);
//response.status(500).send(error);
})
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
return;
}
return doc;
}).catch(function(error) {
console.log("Error getting document:", error);
});
Has I done something wrong here?
I understand that you want to get the value of colId from the {colId} wildcard which is in the path 'users/{userId}/first_col/{colId}'. You should use the context object as follows:
exports.onUserAppCreated = functions.firestore.document('users/{userId}/first_col/{colId}')
.onCreate((snap, context) => {
const data = snap.data();
const colId = context.params.colId;
//....
});
Note that snap is the DocumentSnapshot corresponding to the document that triggered the Cloud Function. So snap.data() gives you an object containing the fields of this document, and therefore data.colId is undefined (unless you have saved the document id in a colId field in your document).
Note also that you could get the value of colId through the snap object by doing snap.id, but for the other wildcard, i.e. userId, you will need to use context.params.
In addition, note that you don't take into account the promises returned by the asynchronous methods of the Admin SDK (get(), set()). It is very important that you correctly return those promises, see the corresponding doc.

Synchronously iterate through firestore collection

I have a firebase callable function that does some batch processing on documents in a collection.
The steps are
Copy document to a separate collection, archive it
Run http request to third party service based on data in document
If 2 was successful, delete document
I'm having trouble with forcing the code to run synchronously. I can't figure out the correct await syntax.
async function archiveOrders (myCollection: string) {
//get documents in array for iterating
const currentOrders = [];
console.log('getting current orders');
await db.collection(myCollection).get().then(querySnapshot => {
querySnapshot.forEach(doc => {
currentOrders.push(doc.data());
});
});
console.log(currentOrders);
//copy Orders
currentOrders.forEach (async (doc) => {
if (something about doc data is true ) {
let id = "";
id = doc.id.toString();
await db.collection(myCollection).doc(id).set(doc);
console.log('this was copied: ' + id, doc);
}
});
}
To solve the problem I made a separate function call which returns a promise that I can await for.
I also leveraged the QuerySnapshot which returns an array of all the documents in this QuerySnapshot. See here for usage.
// from inside cloud function
// using firebase node.js admin sdk
const current_orders = await db.collection("currentOrders").get();
for (let index = 0; index < current_orders.docs.length; index++) {
const order = current_orders.docs[index];
await archive(order);
}
async function archive(doc) {
let docData = await doc.data();
if (conditional logic....) {
try {
// await make third party api request
await db.collection("currentOrders").doc(id).delete();
}
catch (err) {
console.log(err)
}
} //end if
} //end archive
Now i'm not familiar with firebase so you will have to tell me if there is something wrong with how i access the data.
You can use await Promise.all() to wait for all promises to resolve before you continue the execution of the function, Promise.all() will fire all requests simultaneously and will not wait for one to finish before firing the next one.
Also although the syntax of async/await looks synchronous, things still happen asynchronously
async function archiveOrders(myCollection: string) {
console.log('getting current orders')
const querySnapshot = await db.collection(myCollection).get()
const currentOrders = querySnapshot.docs.map(doc => doc.data())
console.log(currentOrders)
await Promise.all(currentOrders.map((doc) => {
if (something something) {
return db.collection(myCollection).doc(doc.id.toString()).set(doc)
}
}))
console.log('copied orders')
}

Resources