I have a problem that's bugging me for days. I am trying to create a Firebase Cloud function that reads from the Firestore database.
My Firestore DB looks like this:
Problem is that I cannot list users like this:
db.collection('users').get().then((snapshot) => snapshot.forEach(...));
If I try to do this I get empty response, like there are no users in my users collection.
But I try to access user directly it works:
await db.collection('users/5CZxgu8nmNXu2TgplwOUdOIt8e33/receipts').get()
My complete code:
import * as functions from 'firebase-functions';
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.cat = functions.https.onRequest(async (req, res) => {
const receiptList: any = [];
const db: Firestore = admin.firestore();
const usersRef = await db.collection('users').get();
console.log(usersRef.empty); // Returns true
const receiptsRef = await db
.collection('users/5CZxgu8nmNXu2TgplwOUdOIt8e33/receipts')
.get();
receiptsRef.forEach((receipt: any) => {
console.log(receipt);
receiptList.push(receipt);
// Here I can access data
});
res.send(receiptList);
return '';
});
Does anyone have any idea what I'm doing wrong? Thank you!
Your users collection is actually empty. See how the document IDs are shown in italics? That means there is not actually a document in its place, however, there are subcollections with documents organized underneath them.
When you query a collection, you only get the documents that are immediately within that collection. A query will not pick up documents organized in subcollections. In this respect, queries are said to be "shallow". As you've seen, you need to reach deeper into the subcollection to get its documents.
Bottom line is that the queries you're showing are doing exactly what they're supposed to do.
Thanks again Doug for your help.
I manage to solve my problem. Here is my complete solution.
import * as functions from 'firebase-functions';
import {
Firestore
} from '#google-cloud/firestore';
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.cat = functions.https.onRequest(async (req, res) => {
const receiptList: any = [];
const db: Firestore = admin.firestore();
const receipts = await db.collectionGroup('receipts').get();
receipts.forEach((doc: any) => {
console.log(doc.id, ' => ', doc.data());
receiptList.push(doc.data());
});
res.send(receiptList);
return '';
});
.get() gets all documents. In your case those documents are empty therefore .get() doesn't consider them.
The simplest solution that I found for this is to replace .get() with .listDocuments(). Now you could read each doc entry like you would a doc.
Related
I am trying to update a user's firestore doc from a Firebase Function using a query, and having issues getting it to work. My Function code is the following:
const functions = require('firebase-functions');
// The Firebase Admin SDK to access Firestore.
const admin = require('firebase-admin');
admin.initializeApp();
/**
* A webhook handler function for the relevant Stripe events.
*/
// Here would be the function that calls updatePlan and passes it the customer email,
// I've omitted it to simplify the snippet
const updatePlan = async (customerEmail) => {
await admin.firestore()
.collection('users').where('email', '==', customerEmail).get()
.then((doc) => {
const ref = doc.ref;
ref.update({ 'purchasedTemplateOne': true });
});
};
I'm getting the following error in the firebase logs when the query is run:
Exception from a finished function: TypeError: Cannot read properties of undefined (reading 'update')
Any help regarding what I may be doing wrong or suggestions on how I could achieve this would be greatly appreciated, Thank you in advance!
Update:
I was able to solve my problem with more understanding of Firestore Queries:
const updatePlan = (customerEmail) => {
const customerQuery = admin.firestore().collection("users").where("email", "==", customerEmail)
customerQuery.get().then(querySnapshot => {
if (!querySnapshot.empty) {
// Get just the one customer/user document
const snapshot = querySnapshot.docs[0]
// Reference of customer/user doc
const documentRef = snapshot.ref
documentRef.update({ 'purchasedTemplateOne': true })
functions.logger.log("User Document Updated:", documentRef);
}
else {
functions.logger.log("User Document Does Not Exist");
}
})
};
The error message is telling you that doc.ref is undefined. There is no property ref on the object doc.
This is probably because you misunderstand the object that results from a Firestore query. Even if you are expecting a single document, a filtered query can return zero or more documents. Those documents are always represented in an object of type QuerySnapshot. That's what doc actually is - a QuerySnapshot - so you need to treat it as such.
Perhaps you should check the size of the result set before you access the docs array to see what's returned by the query. This is covered in the documentation.
here's the basic premise of what im trying to accomplish here. if a user ask a question about a product i want to send a notification to other users who currently own that product. basically saying "hey, so and so has a question about this product. maybe you can help since you own it already"
each userProfile collection has a subcollection called 'notify' where notifications are stored for various things. what i need to do is sort through the userProducts and find every user who owns the product and then create a notification post in only the notify sub-collections for those specific users who own that product.
here is the basic code. the first bit works in that it does return an array of userIDs who own that product. where im struggling now is getting it to create a new doc in the Notify sub-collection for just those specific users. is this possible to do?
exports.Questions = functions.firestore
.document("/userPost/{id}")
.onCreate(async (snap, context) => {
const data = snap.data();
if (data.question == true) {
const userProducts = await db
.collection("userProducts")
.where("product", "==", data.tag)
.get();
const userData = userProducts.docs.map((doc) => doc.data().userId);
await db
.collection("userProfile")
.where("userId", "in", userData)
.get()
.then((querySnapshot) => {
return querySnapshot.docs.ref.collection("notify").add({
message: "a user has asked about a product you own",
});
});
});
Your current solution is on the right track, but there are improvements that can be made.
Use a guard pattern for the data.question == true check.
You don't need to get userProfile/<uid> as you aren't using its contents.
When changing multiple documents at once, you should consider batching them together for simpler error handling.
ref.add(data) is shorthand for ref.doc().set(data) which you can use in the batched write to create new documents.
exports.Questions = functions.firestore
.document("/userPost/{id}")
.onCreate(async (snap, context) => {
const data = snap.data();
if (!data.question) {
console.log("New post not a question. Ignored.")
return;
}
const userProducts = await db
.collection("userProducts")
.where("product", "==", data.tag)
.get();
const userIds = userProducts.docs.map(doc => doc.get("userId")); // more efficient than doc.data().userId
// WARNING: Limited to 500 writes at once.
// If handling more than 500 entries, split into groups.
const batch = db.batch();
const notificationContent = {
message: "a user has asked about a product you own",
};
userIds.forEach(uid => {
// creates a ref to a new document under "userProfile/<uid>/notify"
const notifyDocRef = db.collection(`userProfile/${uid}/notify`).doc();
batch.set(notifyDocRef, notificationContent);
});
await batch.commit(); // write changes to Firestore
});
Note: There is no special handling here for when no one has bought a product before. Consider pinging the product's owner too.
Hi,
I have a problem with downloading all collections from the document. I would like after finding the id (userUid) document to be able to download all its collections, I need the id of each of these collection
export const getAllMessagesByUserId = async (userUid) => {
const result = await firebase
.firestore()
.collection('messages')
.doc(userUid)
.onSnapshot((snapshot) => {
console.log(snapshot);
});
};
I wrote an article which proposes solutions to this problem: How to list all subcollections of a Cloud Firestore document? As a matter of fact, "retrieving a list of collections is not possible with the mobile/web client libraries" as explained in the Firestore documentation.
I would suggest you use the second method proposed in the article, using a Cloud Function.
Here is the code copied from the article.
Cloud Function:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.getSubCollections = functions.https.onCall(async (data, context) => {
const docPath = data.docPath;
const collections = await admin.firestore().doc(docPath).listCollections();
const collectionIds = collections.map(col => col.id);
return { collections: collectionIds };
});
Example of calling the Cloud Function from a web app:
const getSubCollections = firebase
.functions()
.httpsCallable('getSubCollections');
getSubCollections({ docPath: 'collectionId/documentId' })
.then(function(result) {
var collections = result.data.collections;
console.log(collections);
})
.catch(function(error) {
// Getting the Error details.
var code = error.code;
var message = error.message;
var details = error.details;
// ...
});
I was using the chaining mode of the Firestore Web 8, but I'm in the way of updated it to Module 9 and have been a hard time trying to figure out how to get all the content of my subcollection (collection inside my collection).
My older function is like this and works fine:
function getInfo(doc_name) {
let infoDB = db
.collection("collection_name")
.doc(doc_name)
.collection("subcollection_name")
.get();
return alunoHistorico;
}
so with the module way I tried this code
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const docRef = doc(db, "collection_name", "doc_name");
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
console.log("Document data:", docSnap.data());
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
but the function doc() expects a even arguments (not counting the db argument) so if I try to use with 3 arguments like this, I get a error:
const docRef = doc(db, "collection_name", "doc_name", "subcollection_name");
to it work I have to pass the exactly document that is inside the subcollection
const docRef = doc(db, "collection_name", "doc_name", "subcollection_name", "sub_doc");
but it doesn't work for me because I have a list os docs inside the subcollection, that I want o retrieve.
So how can I get all my docs inside my subcollection?
Thanks to anyone who take the time.
You need to use collection() to get a CollectionReference instead of doc() which returns a DocumentReference:
const subColRef = collection(db, "collection_name", "doc_name", "subcollection_name");
// odd number of path segments to get a CollectionReference
// equivalent to:
// .collection("collection_name/doc_name/subcollection_name") in v8
// use getDocs() instead of getDoc() to fetch the collection
const qSnap = getDocs(subColRef)
console.log(qSnap.docs.map(d => ({id: d.id, ...d.data()})))
I wrote a detailed answer on difference between doc() and collection() (in V8 and V9) here:
Firestore: What's the pattern for adding new data in Web v9?
If someone want to get realtime updates of docs inside sub collection using onSnapshot in Modular Firebase V9, you can achieve this like:
import { db } from "./firebase";
import { onSnapshot, collection } from "#firebase/firestore";
let collectionRef = collection(db, "main_collection_id", "doc_id", "sub_collection_id");
onSnapshot(collectionRef, (querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log("Id: ", doc.id, "Data: ", doc.data());
});
});
I want to trigger a new collection (timeline collection) from the existing collection of followers collection and videos collection whenever I clicked the following button in my app.
Now the problem is that, the Cloud Function is created from the view log but the new collection (timeline collection) won't be created.
Below is the code for the Cloud Function where I target the followers collection and the videos collection to create a new timeline collection. I anticipate for your help.
Videos collection
Followers collection
Cloud function view logs
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
// // Create and Deploy Your First Cloud Functions
// // https://firebase.google.com/docs/functions/write-firebase-functions
//
// exports.helloWorld = functions.https.onRequest((request, response) => {
// response.send("Hello from Firebase!");
// });
exports.onCreateFollower = functions.firestore
.document("/followers/{userId}/userFollowers/{userfollowerId}")
.onCreate(async (snapshot, context) => {
console.log("The Event has Created The Follower", snapshot.id);
const userId = context.params.userId;
const userfollowerId = context.params.userfollowerId;
// 1) Create followed users posts ref
const followedUserVideosCollection = admin
.firestore()
.collection("videos")
.doc(userId)
.collection("userVideos");
// 2) Create following user's timeline ref
const timelineVideosCollection = admin
.firestore()
.collection("timeline")
.doc(userfollowerId)
.collection("timelinePosts");
// 3) Get followed users posts
const querySnapshot = await followedUserVideosCollection.get();
// 4) Add each user post to following user's timeline
querySnapshot.forEach(doc => {
if (doc.exists) {
const videoId = doc.id;
const videoData = doc.data();
timelineVideosCollection.doc(videoId).set(videoData);
}
});
});
I figured out what causes the error "querySnapshot.forEach isn't a function". According to this answer, you need to query the collection first because get() returns a document instead of a snapshot. Here's a sample code (see step 3):
// 1) Create followed users posts ref
const followedUserVideosCollection = admin
.firestore()
.collection("videos")
.doc("Videos 1") // I changed the value with your sample for test purposes and also because I'm not sure how you fill up this doc.
.collection("userVideos");
// 2) Create following user's timeline ref
const timelineVideosCollection = admin
.firestore()
.collection("timeline")
.doc(userfollowerId)
.collection("timelinePosts");
// 3) Get followed users posts & Add each user post to following user's timeline
await followedUserVideosCollection.where('id', '==', 0).get().then((querySnapshot) => {
if (querySnapshot) {
querySnapshot.forEach(doc => {
if (doc) {
const videoId = doc.id;
const videoData = doc.data();
timelineVideosCollection.doc(videoId).set(videoData);
}
});
}else {
console.log("Document not found");
}
}).catch((error) => {
console.log(error);
});
A solution is to create a filter, and make sure that the document you're looking for matches the filter. For example, a document inside the subcollection userVideos should have a field called id with value of 0.
You may have to remodel your DB to fix the line where I put a comment but this code should write the timeline collection.