Making a follow system with firestore and cloud functions - javascript

Hi I am attempting to make a social media app on Firestore.
Now to model a follow system here is my plan.
users (Collection)
{uid} document which contains Followers and Following as a number.
following (Collection)
{uid}
myFollowing (subCollection)
{uid of other user}
followers (Collection)
{uid}
myFollowers (subCollection)
{uid of other user}
So here is my plan, and please feel free to critique it and help me make it better, because I dont know if this is the best way to do it.
When user A follows user B, I will write a document in:
following
A uid
myFollowing
B uid
This write will happen straight from the app.
After which I plan to trigger a cloud function that does two things, 1. It will increment a counter in the users collection, that holds total following. 2. It will write another document which would be
Followers
B uid
myFollowers
A uid
And after this I can have another cloud function that triggers whenever a document is made in the Followers/uid/myFollowers collection which increments followers count in the users collection.
So here are the questions
Is this the best way to go about this?
How do i write the cloud functions?
Thanks for any help you can give me!

I solved this by doing everything I did above, and using the following code for cloud functions
const functions = require('firebase-functions');
const admin = require("firebase-admin");
admin.initializeApp(functions.config().firebase);
exports.onFollowCreate = functions.firestore
.document("following/{userID}/myFollowing/{id}")
.onCreate((snap, context) => {
const newValue = snap.data()
const db = admin.firestore();
db.collection("users").doc(context.params.userID).update({following: admin.firestore.FieldValue.increment(1)}).catch((er)=>{console.log(er)})
db.collection('followers').doc(newValue.uid).collection("myFollowers").doc(context.params.userID).set({uid: context.params.userID, timeStamp: new Date()}).catch(er=>console.log(er))
});
exports.onFollowDelete = functions.firestore
.document("following/{userID}/myFollowing/{id}")
.onDelete((snap, context)=>{
const deletedValue = snap.data()
const db = admin.firestore();
db.collection("users").doc(context.params.userID).update({following: admin.firestore.FieldValue.increment(-1)}).catch(er=>console.log(er))
db.collection('followers').doc(deletedValue.uid).collection("myFollowers").doc(context.params.userID).delete().catch(er=>console.log(er))
})
exports.onFollowersCreate = functions.firestore
.document("followers/{userID}/myFollowers/{id}")
.onCreate((snap, context)=>{
const db = admin.firestore();
db.collection("users").doc(context.params.userID).update({followers: admin.firestore.FieldValue.increment(1)}).catch(er=>console.log(er))
})
exports.onFollowersDelete = functions.firestore
.document("followers/{userID}/myFollowers/{id}")
.onDelete((snap, context)=>{
const db = admin.firestore();
db.collection("users").doc(context.params.userID).update({followers: admin.firestore.FieldValue.increment(-1)}).catch(er=>console.log(er))
})

I've thought of this before and it was very similar. I think this might be the best way to go about structuring your database. Here's an article on Medium about some database designs.
Now for the functions, you want one which will trigger once you write that document about A following B. See the docs for a onCreate function. Your cloud functions will live in a node.js 10 serverless environment and will have no connection to your front-end application. Here's a real world example of some of my functions on a deployed site. I would recommend not adding data to firestore on your front-end. Instead make a onCall HTTP function, see more about those here.
Sorry for not giving you actual code to go off of, but I find doing it yourself will help you learn. Good luck :)

Related

Have an empty docs in querySnaphsot when trying to get data with firebase

I recently developped a firebase schedule function to retrieve data on every monday.
I tested it during the previous days and it was working correctly. However, this morning, I discovered that my query wasn't able anymore to retrieve data as it used to do. I now have an empty array at QuerySnapshot.docs. You can find my code below:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const db = admin.firestore();
exports.scheduledFunction = functions.pubsub.schedule("0 0 * * 1").onRun(async () => {
console.log("start");
const querySnapshotnext = await db.collection("Next_challenges").orderBy("createdAt").get();
console.log("Let see querySnapshot :", querySnapshotnext); //it works, I can see QuerySnapshot object
console.log("Let see docs :", querySnapshotnext.docs); //have an empty array [] even if it should not, it wasn't empty few days ago
console.log("let see the data of the first doc : ", querySnapshotnext.docs[0].data()); //is of course undefined
return null;
});
You can find my database below with the doc that is normally selected:
The rules of my databases are the following :
I don't really understand why my code isn't working anymore, I think it's certainly related to some parameters stuffs and don't really know how I could debug this by myself so don't hesitate to give me some tips so I can be more autonomous with firebase. Thank you :)
Firestore's documentation has a note that says,
An orderBy() clause also filters for existence of the given field. The result set will not include documents that do not contain the given field.
You have .orderBy("createdAt") in your query but there isn't any createdAt field in the documents (at least the one in screenshot). Did you change your document structure recently that is causing this issue?
Also the Admin SDK bypasses any security rules so I'd recommend setting them to false if you don't need to access data directly from client.

Firebase v9 ,Cannot get document from a nested collection

I feel like it was easier to get subcollection in v8 ,It's been like 2 days trying to do it the new way but I gave up.
Im building a simple react social media app for learning purposes. where each user logs in and be able to post some text (and images but not atm),
I have a main collection for Users and it has the users ID .each of these users have a collection called Posts and it contains all the user posts.
I can do so by entering the UID of each user like so
so what can i do to access the Users collection then get ALL the users and be able to access the Posts subcollection?
ps : sorry if any part of this question is unclear ,english isn't my first language and it's my first time posting here. appreciate any help!.
If you want to fetch posts from all the users, you are looking for collectionGroup queries using which you can fetch documents in all the sub-collections named as 'posts'. You can run a collectionGroup query using Modular SDK (V9) as shown below:
import { getFirestore, getDocs, collectionGroup } from "firebase/firestore"
const db = getFirestore()
const allPosts = await getDocs(collectionGroup(db, "posts"))
const docRef = doc(db, "Users", currentUser.uid);
const docSnap = await getDocs(collection(docRef, "Posts");)

How do I share information between functions on Firebase realtime database

I am developing a project now in react-native and I integrated firebase to handle my data.
I am now working on the index.js file from the function.
So basically what I am trying to do is through the use of .onCreate functions retrieve the information of the user once it is created and use the same information in .onCreate function for orders.
I tried to store my information on global variables but it doesn't work it appears as undefined.
Probably in the image it would be explained better.
This is where i am having problems
var username;
// ------------ USER DATA FUNCTION ------
exports.requestUserData = functions.database.ref('/users/{userId}/{autoGen}')
.onCreate((snap, context) => {
username = snap._data.name;
console.log(` Username: ${username}`);
});
// ---- ORDERS FUNCTION -----
exports.sendEmail = functions.database.ref('/orders/{userId}/{orderId}/')
.onCreate((snap, context) => {
const userId = context.params.userId;
// USERNAME IS WHAT I NEED TO GET TO USE IN THIS FUNCTION
console.log(`THIS IS WHAT I AM EXPECTING TO GET! --> ${username}`);
EDIT: If it is not possible then could i make a request to a database within another function?
Thanks a lot for any help!!
Cloud Functions cannot share information through shared global variables. Each function runs completely independently from each other and have no shared state. You're going to have to find a way to share data by some other means, such as another location in the database.
I suggest watching this for more information: https://www.youtube.com/watch?v=rCpKxpIMg6o
As Doug said, you can't share information between different Cloud Functions through global variables. The two functions run in different containers. But even if they ran on the same container, there's no guarantee that requestUserData would be invoked before sendEmail.
Instead, you'll need to read the username inside the sendEmail function with something like this:
var username;
exports.sendEmail = functions.database.ref('/orders/{userId}/{orderId}/')
.onCreate((snap, context) => {
const userId = context.params.userId;
return snapshot.ref.root.child(`/users/${userId}/name`).once("value).then((nameSnapshot) => {
const username = nameSnapshot.val();
console.log(username);
... any code that needs the username
});
});

How to write data to cloud Firestore using cloud Functions

For a scheduling app I'm building in Flutter I'm trying to write data to my cloud Firestore database with cloud functions and cron jobs to make my app more self-sustaining. I'm stuck at the first step, which is trying to get my cloud function to write data to my cloud Firestore database.
Below I've included links to pictures on github of how my current data is structured in Firestore. What I want is to add named documents to the collection 'days' with a subcollection of 'hours', in which there are more named documents with the fields 'hour' and 'reserved'.
Picture 1: https://github.com/winckles/rooster/blob/master/Schermafbeelding%202019-11-07%20om%2014.27.55.png?raw=true
Picture 2: https://github.com/winckles/rooster/blob/master/Schermafbeelding%202019-11-07%20om%2014.28.26.png?raw=true
Below I have also included my try on getting data in Firestore.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
// admin.firestore().collection('days').add({original: original}). then(writeResult => {
//
// });
exports.updateData = functions.https.onCall((data, context) => {
const days = admin.firestore().collection('days');
return days.doc(data['2019-11-08/hours/001']).set({
hour: '08:00-09:00',
reserved: '',
});
});
Ideally I would want the cloud function to add 14 documents (eg. 2019-11-06 to 2019-11-19) to the collection 'days' at the same time. Those documents would then each have a subcollection 'hours' with the data from the second screenshot (so the documents 001, 002, 003 etc. with the fields hour and reserved).
I read the documentation on cloud functions and mostly found triggers when data is written to Firestore, but this is not what I want. I also tried the quickstart function samples but with no success. It shouldn't be too hard but I can't seem to figure it out. I'm using Javascript to write the cloud function in. Any help/tips would be greatly appreciated!
You didn't specify exactly what wasn't working for you. However, the first thing I notice is that you're not calling initilizeApp properly from the cloud functions context. For cloud functions, you don't need any parameters, as the default credentials should work for you (unless you have done something very unusual already).
Here is a cloud function that will model the behavior you want. It does not do the full behavior, as I found that writing the date handling code would likely distract from the main part of the problem you are asking about, which is the firestore and functions code itself.
Likewise, this uses an https function (as it is a bit easier for me to test :), to use a callable function (or any other function type, e.g. a scheduled function if you're using a cron job) you would need to adjust it slightly (e.g., for a callable function you would need to change the declaration back to onCall and changing the final .then() call to return the value you want to return).
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
exports.doIt = functions.https.onRequest((request, response) => {
const days = db.collection('days');
const writePromises = [];
['2019-11-08', '2019-11-09'].forEach((day) => {
const documentReference = days.doc(day);
writePromises.push(documentReference.set({reserved: ''}));
const hoursReference = documentReference.collection('hours');
const dataMap = { '001': '08:00-09:00',
'002': '09:00-10:00',
'003': '10:00-11:00',
'004': '11:00-12:00' };
Object.keys(dataMap).forEach((hour) => {
writePromises.push(hoursReference.doc(hour).set({
hour: dataMap[hour],
reserved: ''
}))
});
});
return Promise.all(writePromises)
.then(() => { response.send('ok'); })
.catch((err) => { console.log(err); });
});
Note that when you write this entire structure, you will be billed for a write to each document. There isn't really a way to avoid that though, as writes are billed per document, not per request.
Likewise, you may want to consider doing this as a batched write -- the above example is just showing a basic approach to writing. A batched write would put all of the documents into the database atomically. However, a batched write is limited to 500 updates, which you would hit at around 21 days of data (21 days * 24 hours). This would look very similar to the above (below is just the content of the function itself:
const days = db.collection('days');
const batch = db.batch();
['2019-11-08', '2019-11-09'].forEach((day) => {
const documentReference = days.doc(day);
batch.set(documentReference, {reserved: ''});
const hoursReference = documentReference.collection('hours');
const dataMap = { '001': '08:00-09:00',
'002': '09:00-10:00',
'003': '10:00-11:00',
'004': '11:00-12:00' };
Object.keys(dataMap).forEach((hour) => {
batch.set(hoursReference.doc(hour), {
hour: dataMap[hour],
reserved: ''
});
});
});
return batch.commit()
.then(() => { response.send('ok'); })
.catch((err) => { console.log(err); });
I do wonder a small bit about why you need to do this in a cloud function, rather than via a Firestore call directly from your app. Regardless, the above should allow you to get started.
if you wanna do a cron in firebase maybe its better if you use Google Cloud Scheduler, but be careful this approach have a different kind of facturation
exports.scheduledFunction = functions.pubsub.schedule('every 5 minutes').onRun((context) => {
console.log('This will be run every 5 minutes!');
return null;
});
You can learn more about this in:
https://firebase.google.com/docs/functions/schedule-functions

Javascript: failed in creating a node on Firebase

I am a beginner in Javascript, started learning it 2 days ago because I wanted to set up some cloud functions on Firebase. The database structure of my app (already published) is shown in the figure, and what I want is:
When users upload their "achievements" to Firebase, the cloud function calculates "the total times" that a specific achievement has been achieved by all users. For example, achievements[0] is achieved 80000 times in total, and achievements[28] is achieved 54321 times in total. After calculating them, I store the array under the node "achievementsCount_total", which would be at the same level as the node "users".
To do this, I followed the documents on Firebase and tried to accomplish the cloud function. Here are the codes:
const functions = require('firebase-functions');
// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.updateScores = functions.database.ref('/users/{userUID}/achievements')
.onWrite(event => {
var database=firebase.database();
var achievementsCount_total=new Array();
for(var i=0;i<60;i++){achievementsCount_total[i]=0;}
var allUsersSnapshot = event.data.parent.parent.child('/{userUID}');//a list containing all users
allUsersSnapshot.forEach(function(element) {
var achievements_this = element.child('/achievements').val();
for(var i=0;i<60;i++)
{
achievementsCount_total[i]+=achievements_this[i];
}
}, this);
database.ref('achievementsCount_total').set(achievementsCount_total);
return null;
});
However, the new node("achievementsCount_total") was not created at the same level as "users". I checked it on Firebase and found that the function was successfully executed, while the new node did not show up. Please help me with what I did wrong. Thank u~

Categories

Resources