I am building a social media app (very simple) an I want to store user's activity in firestore database. I have a collection of "users" and I keep user's id, user's username, user's profile pic there. But I dont think that user's activity should be stored there as well (correct me if I am wrong?)
So I created a new collection called UserActivity where I store user's activity. I wanted to store if a user has been searching on a profile so I do the following:
const logUserSearch = async (term) => {
await firebase
.firestore()
.collection("userActivity")
.doc(firebase.auth().currentUser.uid)
.collection("userSearch")
.add({
term: term,
date: firebase.firestore.FieldValue.serverTimestamp(),
})
};
I think the above query solves the problem with user's search term's. However I want to store if a user has visited a profile. So here is my question: what is the correct way to store if a user visited a profile? Should I add new subcollection "profileVisit", something like that:
const logProfileVisit = async (searchTerm, profileId) => {
await firebase
.firestore()
.collection("userActivity")
.doc(firebase.auth().currentUser.uid)
.collection("profileVisit")
.doc(profileId)
.add({
source: searchTerm,
date: firebase.firestore.FieldValue.serverTimestamp(),
})
};
But then how do I calculate which are the most "popular" profiles? Should I create my database in another way, like this:
const logProfileVisit = async (searchTerm, profileId) => {
await firebase
.firestore()
.collection("userActivity")
.doc(profileId)
.collection("profileVisit")
.add({
user: firebase.auth().currentUser.uid
source: searchTerm,
date: firebase.firestore.FieldValue.serverTimestamp(),
})
};
So that I can easily calculate which are the most "popular" profiles? What about the user case where I need to calculate "top 10 fan profiles" or something similar? I.e. How do I calculate who visited your profile most often?
A root level collection "userActivity" (or a sub-collection) should be enough. You can store the activity type as a field instead of sub-collections as shown below:
users -> {userId} -> userActivity -> {logId}
(col) (doc) (col) (doc)
But then how do I calculate which are the most "popular" profiles?
You can store a number field in that profile's document and whenever the logProfileVisit is called, increment that:
const usersCol = firebase.firestore().collection("users")
const logProfileVisit = async (searchTerm, profileId) => {
await Promise.all([
usersCol
.doc(currentUserId)
.collection("userActivity")
.add({
source: searchTerm,
date: firebase.firestore.FieldValue.serverTimestamp(),
type: "profileVisit"
}),
usersCol
.doc(profileUserId)
.update({
profileViews: firebase.firestore.FieldValue.increment(1),
})
])
};
You can also use batch writes while updating these fields so either both the operations fail or pass.
You can also use firestore -> audit logs -> pub/sub sink -> cloud function -> firestore.
I explain it a little more at https://justin.poehnelt.com/posts/tracking-firestore-user-activity/. Below is the function that listens to the Pub/Sub sink and writes back to Firestore.
import * as firebaseAdmin from "firebase-admin";
import * as functions from "firebase-functions";
export default functions.pubsub
.topic("firestore-activity")
.onPublish(async (message) => {
const { data } = message;
const { timestamp, protoPayload } = JSON.parse(
Buffer.from(data, "base64").toString()
);
const uid =
protoPayload.authenticationInfo.thirdPartyPrincipal.payload.user_id;
const writes = protoPayload.request.writes;
const activityRef = firebaseAdmin
.firestore()
.collection("users")
.doc(uid)
.collection("activity");
await Promise.all(
// eslint-disable-next-line #typescript-eslint/no-explicit-any
writes.map((write: any) => {
activityRef.add({ write, timestamp });
})
);
});
I then have a collection that looks like the following.
Related
I got this function to add data to a Firestore db and was wondering how to do it in the newer version.
db.doc(`User/${fields.user}/Address/${fields.address}`)
.set({
User: fields.user,
Address: fields.address,
})
.then(
db.doc(`User/${fields.user}/Address/${fields.address}/Orders/${fields.ID}`)
.set({
ID: fields.ID,
});
This function is to add a document with data in a collection then create a subcollection with a diferent document with its own data. The document id are form inputs.
You first need to use doc() function to create a DocumentReference for the documents and then use setDoc() function to add document in Firestore as mentioned in the documentation.
import { doc, setDoc } from "firebase/firestore"
// here db is getFirestore()
const docRef = doc(db, `User/${fields.user}/Address/${fields.address}`)
await setDoc(docRef, { test: "test" })
Alternatively you can use a batched write to add both the documents at once. Try refactoring the code as shown below:
import {
writeBatch,
doc
} from "firebase/firestore";
// Get a new write batch
const batch = writeBatch(db);
const docRef = doc(db, `User/${fields.user}/Address/${fields.address}`);
batch.set(docRef, {
User: fields.user,
Address: fields.address
});
const subDocRef = doc(db, `User/${fields.user}/Address/${fields.address}/Orders/${fields.ID}`);
batch.update(subDocRef, {
ID: fields.ID
});
// Commit the batch
batch.commit().then(() => {
console.log("Documents added")
}).catch(e => console.log(e));
Also checkout: Firestore: What's the pattern for adding new data in Web v9?
I'm currently ripping my hair out trying to query a nested collection in my Firestore db.
my db is set up as follows:
users (collection)
- username
- displayName
- ratedItems (collection)
- itemId
- timestamp
- likedItem
I want to be able to add/update the rated item in the ratedItems collection for a specific user but I seem to be hitting 2 problems:
I assume the query is incorrect as if(querySnapshot.empty) always returns false even if there are no matching items (matching the itemId) in the ratedItems collection, so it always attempts to add a new item which results in duplicate entries in the collection.
if I force the code to bypass the conditional it throws an error when it attempts to update the existing item:
Expected type 'mc', but it was: a custom yc object
My thoughts are I am using the collectionGroup query incorrectly but I haven't found a solution for this yet. Should I even be using collectionGroup at all?? from what I've read, if I understand correctly this will query every ratedItem collection regardless of the user, which isn't what I want
const rateItem = async (itemId, liked) => {
try {
const itemRef = collection(db, 'users', currentUser.uid, 'ratedItems');
const q = query(
collectionGroup(db, 'users', currentUser.uid),
where('itemId', '==', itemId)
);
const querySnapshot = await getDocs(q);
if (querySnapshot.empty) {
await addDoc(itemRef, {
itemId: itemId,
timestamp: serverTimestamp(),
likedItem: liked,
});
} else {
await updateDoc(itemRef, {
timestamp: serverTimestamp(),
likedItem: liked,
});
}
} catch (err) {
console.log(err.message);
}
};
I assume the query is incorrect as if(querySnapshot.empty) always returns false even if there are no matching items (matching the itemId) in the ratedItems collection, so it always attempts to add a new item which results in duplicate entries in the collection.
You used the itemRef for both of addDoc and updateDoc which is not the proper way to do it. You need a correct query syntax in order to update your Firestore document.
if I force the code to bypass the conditional it throws an error when it attempts to update the existing item
You can try the code below to update your nested document:
const updateitemRef = query(
collection(db, 'users', 'user_item', 'ratedItems'),
where('itemId', '==', itemId)
);
const itemSnapshot = await getDocs(updateitemRef);
itemSnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
updateDoc(doc.ref, {
likedItem: liked,
timestamp: serverTimestamp()
});
});
For the complete code, you can try the code below:
const rateItem = async (itemId, liked) => {
try {
const q = query(
collectionGroup(db, 'users', currentUser.uid),
where('itemId', '==', itemId)
);
const querySnapshot = await getDocs(q);
const additemRef = collection(db, 'users', currentUser.uid, 'ratedItems');
const updateitemRef = query(
collection(db, 'users', currentUser.uid, 'ratedItems'),
where('itemId', '==', itemId)
);
const itemSnapshot = await getDocs(updateitemRef);
if (querySnapshot.empty) {
await addDoc(additemRef, {
itemId: itemId,
likedItem: liked,
timestamp: serverTimestamp()
});
} else {
itemSnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
updateDoc(doc.ref, {
likedItem: liked,
timestamp: serverTimestamp()
});
});
}
} catch (err) {
console.log(err.message);
}
};
For more references in creating a query, you can refer to the guides below:
Add a document
Perform simple and compound queries in Cloud Firestore
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.
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.
I am trying to save a user to a collection in my Firestore. It seems that the users are being created, I can see them in the Authentication tab, however they are not saving to my collection inside my firestore. There doesn't seem to be any errors inside my console either. I've been struggling with this for a couple hours now and i'm not even sure how to debug this.
export const authMethods = {
signup: (email, password, setErrors, setToken) => {
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
// make res asynchronous so that we can make grab the token before saving it.
.then(async res => {
const token = await Object.entries(res.user)[5][1].b
// set token to localStorage
await localStorage.setItem('token', token)
// grab token from local storage and set to state.
setToken(window.localStorage.token)
const userUid = firebase.auth().currentUser.uid
const db = firebase.firestore()
db.collection('/users')
.doc(userUid)
.set({
email,
password,
})
console.log(res)
})
.catch(err => {
setErrors(prev => [...prev, err.message])
})
},
....
}
Any ideas?
Remove await from localStorage.setItem it isn't an asynchronous function.
You'll also need to add await to db.collection("/users").doc(userUid)
This is another approach that you could do. Let a cloud function handle that for you.
Whenever a user is created the following function is triggered.
export const onUserCreate = functions.auth
.user()
.onCreate(async (user, context) => {
await admin.firestore().collection("users").doc(user.uid).set({
id: user.uid,
emailAddress: user.email,
verified: user.emailVerified,
});
});
If you need more information about cloud functions read the following.
https://firebase.google.com/docs/functions/get-started