How to create a firebase cloud function that writes data to every 'user' node in firebase realtime database - firebase

I am attempting to write a firebase function that creates new data nodes for every user in the database, but i just can't seem to get it to work.
Database structure:
I wish to add additional days to the TimeSlots node, through this function
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
var database = admin.database();
exports.addTimeNode = functions.database.ref('/message/{id}').onCreate((snapshot, context) =>
{
var root = firebase.database().ref();
var db = root.child('Users').child('Barbers');
db.once('value', function(snapshot)
{
snapshot.forEach(function(data)
{
//var reqStatus = data.val().request_status;
var uid = data.key;
root.child('User').child(uid).child(TimeSlots).once('value', function(userSnapshot)
{
var now = new Date().toDateString();
database.ref('/Users/Barbers/' + uid + '/TimeSlots/'+ now +'/9am').set({
availability: 'Available',
id: '01',
time: '09:00 AM'
});
database.ref('/Users/Barbers/' + uid + '/TimeSlots/'+ now +'/9:30am').set({
availability: 'Available',
id: '02',
time: '09:30 AM'
});
...

Maybe you can try this way:
By the way I am not a big fan of using snapshot.key instead I add the value of UID in the node itself with a child node named as id and it would stay below firstName node in your case. Won't take long for you too adjust your code to add ID there as well along with adding the first name.
const allBarbersSnapshot = await admin.database().ref(`/users/barbers/`).once('value').then(function (snapBarbers: any) {
snapBarbers.forEach((eachBarber: any) => {
const currentBarberId = eachBarber.val().id
admin.database().ref(`/users/barbers//${currentBarberId}/TimeSlots/${now}/${yourTimeVariableHere}`).update({ "availability": 'Available', "id": 1, "time": yourTimeVariableHere })
});
//Anything after above stuff is done goes here !!!
})
If you have any questions, drop a comment below ;)
PS: All the above code goes inside your Cloud Function

Related

Firestore + Vue: When refreshing the page, how to avoid delay in data display?

Every time I refresh the page, a new query is made in the Firestore database to show (sometimes) the same data to the user.
It is a bookcase. The code below only reads data that has already been added:
export default {
name: "Shelf",
date: () => ({ shelfName: "", books: [] }),
async mounted() {
const auth = getAuth();
const db = getFirestore();
const userID = auth.currentUser.uid;
const userRef = doc(db, "users", userID);
const userSnap = await getDoc(userRef);
userSnap.exists()
? (this.shelfName = "Shelf: " + userSnap.data().name)
: (this.shelfName = "Your Shelf");
const userBooksRef = query(collection(db, "users", userID, "addedBooks"));
const querySnapshot = await getDocs(userBooksRef);
querySnapshot.forEach((doc) => {
this.books.push({
id: doc.id,
addedIn: doc.data().addedIn,
readIn: doc.data().readIn,
});
});
this.books.map(async (book) => {
const booksRef = doc(db, "books", book.id);
const bookSnap = await getDoc(booksRef);
book.authors = bookSnap.data().authors;
book.title = bookSnap.data().title;
book.thumbnail = bookSnap.data().thumbnail;
});
},
};
I'm not sure if I should look for a caching solution or if there is some other type of solution.
Below I show the books collection. It's important to say that within the users collection there is a collection called addedBooks that reference the id of each book below:
When refreshing the page, you're destroying the previous instance of your application and loading a new one. As such, your app will re-execute all of its queries just like it did the first time opening the app.
As Estus mentioned in the comments, this problem is often solved by storing your data in state, and then putting that state in localstorage. When your page is refreshed, you first load the state from localstorage (no delay), and then update the state with the current data from your query when it becomes available. You can implement this process manually as well without using a state management library.
When storing data in localstorage, make sure you update it every time the data changes, and clear it when you no longer need it.

Firestore how to get last document of collection and add new with incremented id?

I have probably made a mistake with autogenerated id's for documents inside my events collection. I have added eventId for generated events and assigned eventId manually to each event.
Can I somehow get last document with its eventId and add new document with eventId of last document incremented by one.
Or should I delete autogenerated Id - based events and create new ones with non-autogenrated ids?
GitHub: https://github.com/Verthon/event-app
I have posted working React.js app on netlify: https://eventooo.netlify.com/
How it works:
I added unique eventId to each dummy events inside of events collection,
Based on that unique eventId I create link to specific single Event.js,
User can create event providing information in /create-event,
Once someone is creating an event I would like to add event to events collection with increment id, I have added 7 events created inside of console, so next should have id=7, something maybe like event1, event2 ... with autoincrement
Inside of users collection I store currentUser.uid from auth and host name provided by user
Event Creator
submitEvent = (e) => {
e.preventDefault();
const eventRef = this.props.firebase.db.collection("events").doc();
eventRef.set({
title: this.state.title,
host: this.state.host,
localization: this.state.localization,
description: this.state.description,
category: this.state.category,
day: this.state.day,
hour: this.state.hour,
featuredImage: this.state.imageUrl,
uid: this.props.firebase.auth.currentUser.uid
});
const citiesRef = this.props.firebase.db.collection("cities").where("city", "==", this.state.localization);
const cityRef = this.props.firebase.db.collection("cities").doc();
citiesRef.get()
.then(querySnapshot => {
if(querySnapshot.exists){
console.log("exist");
return
}else{
cityRef.set({
city: this.state.localization,
})
}
});
const userRef = this.props.firebase.db.collection("users").doc();
userRef.set({
user: this.state.host,
uid: this.props.firebase.auth.currentUser.uid
});
Thank you
I understand that you want your eventId value to come from a number sequence. The best approach for such need is to use a distributed counter, as explained in the doc: https://firebase.google.com/docs/firestore/solutions/counters
I don't know which language you are using, but I paste below the JavaScript code of the three functions from this documentation and I write the code that will generate the sequential number that you can use to create the documents.
var db = firebase.firestore();
//From the Firebase documentation
function createCounter(ref, num_shards) {
var batch = db.batch();
// Initialize the counter document
batch.set(ref, { num_shards: num_shards });
// Initialize each shard with count=0
for (let i = 0; i < num_shards; i++) {
let shardRef = ref.collection('shards').doc(i.toString());
batch.set(shardRef, { count: 0 });
}
// Commit the write batch
return batch.commit();
}
function incrementCounter(db, ref, num_shards) {
// Select a shard of the counter at random
const shard_id = Math.floor(Math.random() * num_shards).toString();
const shard_ref = ref.collection('shards').doc(shard_id);
// Update count
return shard_ref.update(
'count',
firebase.firestore.FieldValue.increment(1)
);
}
function getCount(ref) {
// Sum the count of each shard in the subcollection
return ref
.collection('shards')
.get()
.then(snapshot => {
let total_count = 0;
snapshot.forEach(doc => {
total_count += doc.data().count;
});
return total_count;
});
}
//Your code
var ref = firebase
.firestore()
.collection('counters')
.doc('1');
var num_shards = 2 //Adapt as required, read the doc
//Initialize the counter bay calling ONCE the createCounter() method
createCounter(ref, num_shards);
//Then, when you want to create a new number and a new doc you do
incrementCounter(db, ref, num_shards)
.then(() => {
return getCount(ref);
})
.then(count => {
console.log(count);
//Here you get the new number form the sequence
//And you use it to create a doc
db.collection("events").doc(count.toString()).set({
category: "education",
//....
})
});
Without much details on the Functional Requirements it is difficult to say if there is a difference between using the number form the sequence as the uid of the doc or as a field value in the document. It depends on the queries you may do on this collection.

Cloud Function Update Problem in Firestore Database

I'm trying to build an Android application. In my Firestore database I have Users collection and Counters collection. In Counters collection I have userCounter. What I want to do is whenerever a new user logs in and I push it to firestore, userCounter to increase.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.addNewUser =
functions.firestore.document('Users/{userID}').onCreate((event) => {
var db = admin.firestore();
var counterRef = db.collection("Counters");
var temp = counterRef.doc("0").data().userCounter++;
counterRef.doc("0").update(
{
userCounter: temp
});
});
In this state, this function doesn't work, and I'm a newbie so I'd appreciate any help.
Thx beforehand
EDIT
After implementing Firebaser and Pablo Almécija Rodríguez's answers, my code looks like this.
const Firestore = require('#google-cloud/firestore');
const firestore = new Firestore({
projectId: process.env.GCP_PROJECT,
});
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
exports.addNewUser =
functions.firestore.document('Users/{userId}').onCreate((snapShot) => {
const userCounterRef = db.collection('Counters').doc('Users');
return db.runTransaction(transaction => {
const doc = transaction.get(userCounterRef);
console.log("1");
const count = doc.data();
console.log(`5`);
const updatedCount = count + 1;
console.log(`6`);
return transaction.update(userCounterRef, {counter: updatedCount })
})
});
And this is the firebase console log. The problem is
const count = doc.data();
TypeError: doc.data is not a function
Firebase Console Log
I suggest you to create a collection named counters and inside it a document named users to handle counter for users. Here is the structure:
- counters (collection)
- users (document)
count: 0 (field)
Then, you should use a transaction to perform an update on this counter document to make sure you are working with up-to-date data to deal with concurrency (multiple accounts created at the same time)
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
exports.addNewUser =
functions.firestore.document('users/{userId}').onCreate((snapShot) => {
const userCounterRef = db.doc('counters/users');
return db.runTransaction(async transaction => {
const doc = await transaction.get(userCounterRef)
const { count } = doc.data()
const updatedCount = count + 1
return transaction.update(userCounterRef, {count: updatedCount })
})
});
https://firebase.google.com/docs/firestore/manage-data/transactions
EDIT: If you don't want to deal with async/await syntax, update your transaction like that :
return db.runTransaction(transaction => {
return transaction.get(userCounterRef)
.then(doc => {
const count = doc.data().count
const updatedCount = count + 1
transaction.update(userCounterRef, {count: updatedCount })
})
})
I have reproduced it in Cloud Functions, and this simple solution worked. Edited answer to fit Firebase, and it also uses the Firestore dependency for nodejs.
const Firestore = require('#google-cloud/firestore');
const firestore = new Firestore({
projectId: process.env.GCP_PROJECT,
});
const functions = require('firebase-functions');
//I am not using next two lines right now though
const admin = require('firebase-admin');
admin.initializeApp();
exports.helloFirestore =
functions.firestore.document('users/{userID}').onCreate((event) => {
const doc = firestore.doc('Counters/UserCounter/');
doc.get().then(docSnap => {
//Get the specific field you want to modify
return docSnap.get('userCount');
}).then(field => {
field++;
//Log entry to see the change happened
console.log(`Retrieved field value after +1: ${field}`);
//Update field of doc with the new value
doc.update({
userCount: field,
});
});
});
The wildcard you used should be fine, just be careful with the full path for the collection, mind the upper/lower case. For this case, this is how my package.json looks like:
{
"name": "sample-firestore",
"version": "0.0.1",
"dependencies": {
"#google-cloud/firestore": "^1.1.0",
"firebase-functions": "2.2.0",
"firebase-admin": "7.0.0"
}
}

How to add a field to the document which is created before?

I need to store some details entered in Dialogflow console into Firestore database,
I have this following code:
index.js
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp(functions.config().firebase);
exports.dialogflowFirebaseFulfillment = functions.https.onRequest(
(request, response) => {
let action = request.body.result.action;
var Name = request.body.result.parameters.Name;
let params = request.body.result.parameters;
var Answer1 = request.body.result.parameters.Answer1;
let query = request.body.result.resolvedQuery;
const parameters = request.body.result.parameters;
const inputContexts = request.body.result.contexts;
console.log("Parameters is" + params);
console.log("Answer1 is " + Answer1);
console.log("Helllo");
console.log("action is " + action);
if (action == "save.name") {
admin
.firestore()
.collection("users")
.doc(Name)
.set({
name: Name,
answer: ""
})
.then(ref => {
var doc = admin
.firestore()
.collection("users")
.doc(Name)
.get();
console.log("Added new user");
console.log("Name is " + Name);
console.log("ref id is:" + ref.id);
});
}
//const ref = this.Name;
if (action == "save.answer1") {
// var Name = this.request.body.result.parameters.Name;
console.log("name in second if", Name);
admin
.firestore()
.collection("users")
.doc(doc)
.update({
answer: Answer1
})
.then(ref => {
console.log("Added new user");
console.log("Name is " + Name);
console.log("ref id is:" + ref.id);
});
}
}
);
This code successfully adds the name into the database, then quits out of code and again re-enters to save Answer1, So I don't know how to refer the document where the name is added, If I hard code the 'Name' previously entered by the user, then it adds the answer1 under the same document
example:
if (action == "save.answer1") {
admin
.firestore()
.collection("users")
.doc("ABC")
.update({
answer: Answer1
})
I have here hard-coded the name of the document as ABC so it adds answer1 under the document, How to avoid hard-coding the name?
I need to get the DocumentId of the document where Name is stored, So that I can add Answer1,
expected Output is:
First - don't try to find a query that finds the "last document added". I don't know if it exists, but it will do the wrong thing if you have two different users in different sessions add documents and then try to update them. This is a race condition, and it is very bad.
Instead, what you can do is save the document identifier (the name in your case) in a parameter in a Dialogflow Context. Then, the next time your webhook is called, you can retrieve the parameter from that context and use that in the .doc() reference.
You don't show how you're sending messages back to Dialogflow, but it looks like you might be using the Dialogflow V1 JSON. If so, you'd set the contexts back in the response under the contextOut attribute. This will be an array of contexts you want to set. So this portion of the JSON fragment might look something like:
"contextOut": [
{
"name": "doc_name",
"lifespan": 5,
"parameters": {
"name": Name
}
}
]
When handling the "save.answer1" action, you would go through the incoming contexts to find the correct context and use this name in your update. So that segment might look something like this:
if (action == "save.answer1") {
var Name;
for( var co=0; inputContexts && co<inputContexts.length && !Name; co++ ){
var context = inputContexts[co];
if( context.name == 'doc_name' ){
Name = context.parameters.name;
}
}
console.log("name in second if", Name);
admin
.firestore()
.collection("users")
.doc(Name)
.update({
answer: Answer1
})
.then(ref => {
console.log("Updated user");
console.log("Name is " + Name);
console.log("ref id is:" + ref.id);
});
}

Cloud Functions for Firebase Time Out w/ wrong response

Newbie question: Cloud Function times out every single time I run it.
In addition, it only returns ONE value, which is the first userId, in the Functions Log and none of its children. Im assuming this is because it's calling the .once however, it's in a forEach loop, so I'm not sure what it wants.
Firebase database
-items
---- userId0123456789
---- randomKey987654321
-- itemName
-- itemDate
-- itemType
---- userId987654321
---- randomKey012345678
-- itemName
-- itemDate
-- itemType
And here is the function code...
const key = req.query.key;
**let userID = 'xxxxx';
let ikey = 'xxx';**
var dbRef = admin.database().ref('/items/{userID}/{ikey}');
dbRef.once("value", function(snapshot) {
snapshot.forEach(function(child) {
console.log(child.key+": "+child.val());
});
});
UPDATE: here is the entire function and now it's just timing out with no response.
'use strict';
// Firebase Functions
const functions = require('firebase-functions');
// Firebase Admin
const admin = require('firebase-admin');
// Default admin firebase configuration
admin.initializeApp(functions.config().firebase);
const rp = require('request-promise');
const promisePool = require('es6-promise-pool');
const PromisePool = promisePool.PromisePool;
const secureCompare = require('secure-compare');
const MAX_CONCURRENT = 3;
//Initial function call:
exports.CheckItemTypeinFB = functions.https.onRequest((req, res) => {
const key = req.query.key;
// Exit if the keys don't match
if (!secureCompare(key, functions.config().cron.key)) {
console.log('The key provided in the request does not match the key set in the environment. Check that', key,
'matches the cron.key attribute in `firebase env:get`');
res.status(403).send('Security key does not match. Make sure your "key" URL query parameter matches the ' +
'cron.key environment variable.');
return;
}
// Try the database here...
let userID = 'xxx';
let ikey = 'xxxxx
//create database ref
let ref = admin.database().ref(`/items/${userID}/${ikey}`);
//do a bunch of stuff
ref.once("value", function(snapshot) {
snapshot.forEach(function(child) {
console.log(`${child.key}: ${child.val()}`);
});
res.send(200, {/* response data */});
});
//send back response
// res.redirect(200);
}) // END THE MAJJOR CONTAINER THINGS
// Returns an access token using the Google Cloud metadata server. */
function getAccessToken(accessToken) {
// If we have an accessToken in cache to re-use we pass it directly.
if (accessToken) {
return Promise.resolve(accessToken);
}
const options = {
uri: 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token',
headers: {'Metadata-Flavor': 'Google'},
json: true
};
return rp(options).then(resp => resp.access_token);
}
Help is much appreciated.
Update:. Timeout is fixed and it returns the userId's that are in the database under "/items". HOWEVER, if I use ${userId}/${key} I get nothing. I'm still not able to tell how to get the children under random userId's in the database and none of the other posts I read explain it. Firebase's docs state to use {userId} to get all under that wildcard but its not working. What am I missing?
You're not returning the result of the once function or returning at all, so the function doesn't know when to finish hence the timeout.
let userID = 'xxxxxxxxx';
let key = 'xxxxxxxx';
let ref = admin.database().ref(`/items/${userID}/${key}`);
return ref.once("value", function(snapshot) {
snapshot.forEach(function(child) {
console.log(`${child.key}: ${child.val()}`);
});
});
Also please be aware that the reference you are observing will give you the children of a particular user item (itemName, itemDate, itemType). If you want the items belonging to a particular user, adjust your reference path to be /items/${userID}.
When inside a HTTP trigger, you can send a response after observing the value.
exports.CheckItemTypeinFB = functions.https.onRequest((req, res) => {
...
ref.once("value", function(snapshot) {
snapshot.forEach(function(child) {
console.log(`${child.key}: ${child.val()}`);
});
res.send(200, {/* response data */});
});
});

Resources