Firebase multi-path updates just overwrites the supposed node - javascript

In the code below I am trying to use a cloud function to do a multiple update of setting some of my fields to a new value, but it just results to overwriting each of the nodes. I don't really understand this behavior, cos I just needed a simple update.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.onJobBid_Status = functions.database
.ref("/JobBids/{jobId}/{bidId}/status")
.onWrite((event) => {
let newStatus = event.data.val();
let updates = {};
updates["/Jobs/" + event.params.jobId] = { status: newStatus, };
updates["/Users/" + event.params.bidId + "/JobBids/" + event.params.jobId] = { status: newStatus, level:"4", color:"green" };
return admin.database().ref().update(updates);
});

When you call update, the Firebase server:
Loops through the properties/paths of the updates.
For each property/path, performs a set() operation.
So while you can update specific paths, at each path the operation is a regular set(). This means it replaces the data under each path.
The solution is to have the entire path to the lowest-level property in your key. So in your case:
let updates = {};
updates["/Jobs/" + event.params.jobId+"/status"] = newStatus;
let jobPath = "/Users/" + event.params.bidId + "/JobBids/" + event.params.jobId;
updates[jobPath+/status"] = newStatus;
updates[jobPath+/level"] = "4"; // consider storing this as a number
updates[jobPath+/color"] = "green" ;
With these updates, you will only replace the values of the status, level and color properties.

Related

Firebase cloud function onUpdate is triggered but doesn't execute as wanted

I'm currently making a web application with React front-end and Firebase back-end. It is an application for a local gym and consists of two parts:
A client application for people who train at the local gym
A trainer application for trainers of the local gym
The local gym offers programs for companies. So a company takes out a subscription, and employees from the company can train at the local gym and use the client application. It is important that the individual progress of the company employees is being tracked as well as the entire progress (total number of kilograms lost by all the employees of company x together).
In the Firestore collection 'users' every user document has the field bodyweight. Whenever a trainer fills in a progress form after a physical assessment for a specific client, the bodyweight field in the user document of the client gets updated to the new bodyweight.
In Firestore there is another collection 'companies' where every company has a document. My goal is to put the total amount of kilograms lost by the employees of the company in that specific document. So every time a trainer updates the weight of an employee, the company document needs to be updated. I've made a cloud function that listens to updates of a user's document. The function is listed down below:
exports.updateCompanyProgress = functions.firestore
.document("users/{userID}")
.onUpdate((change, context) => {
const previousData = change.before.data();
const data = change.after.data();
if (previousData === data) {
return null;
}
const companyRef = admin.firestore.doc(`/companies/${data.company}`);
const newWeight = data.bodyweight;
const oldWeight = previousData.bodyweight;
const lostWeight = oldWeight > newWeight;
const difference = diff(newWeight, oldWeight);
const currentWeightLost = companyRef.data().weightLostByAllEmployees;
if (!newWeight || difference === 0 || !oldWeight) {
return null;
} else {
const newCompanyWeightLoss = calcNewCWL(
currentWeightLost,
difference,
lostWeight
);
companyRef.update({ weightLostByAllEmployees: newCompanyWeightLoss });
}
});
There are two simple functions in the cloud function above:
const diff = (a, b) => (a > b ? a - b : b - a);
const calcNewCWL = (currentWeightLost, difference, lostWeight) => {
if (!lostWeight) {
return currentWeightLost - difference;
}
return currentWeightLost + difference;
};
I've deployed the cloud function to Firebase to test it, but I can't get it to work. The function triggers whenever the user document is updated, but it doesn't update the company document with the new weightLostByAllEmployees value. It is the first time for me using Firebase cloud functions, so big change it is some sort of rookie mistake.
Your current solution has some bugs in it that we can squash.
Always false equality check
You use the following equality check to determine if the data has not changed:
if (previousData === data) {
return null;
}
This will always be false as the objects returned by change.before.data() and change.after.data() will always be different instances, even if they contain the same data.
Company changes are never handled
While this could be a rare, maybe impossible event, if a user's company was changed, you should remove their weight from the total of the original company and add it to the new company.
In a similar vein, when a employee leaves a company or deletes their account, you should remove their weight from the total in a onDelete handler.
Handling floating-point sums
In case you didn't know, floating point arithmetic has some minor quirks. Take for example the sum, 0.1 + 0.2, to a human, the answer is 0.3, but to JavaScript and many languages, the answer is 0.30000000000000004. See this question & thread for more information.
Rather than store your weight in the database as a floating point number, consider storing it as an integer. As weight is often not a whole number (e.g. 9.81kg), you should store this value multiplied by 100 (for 2 significant figures) and then round it to the nearest integer. Then when you display it, you either divide it by 100 or splice in the appropriate decimal symbol.
const v = 1201;
console.log(v/100); // -> 12.01
const vString = String(v);
console.log(vString.slice(0,-2) + "." + vString.slice(-2) + "kg"); // -> "12.01kg"
So for the sum, 0.1 + 0.2, you would scale it up to 10 + 20, with a result of 30.
console.log(0.1 + 0.2); // -> 0.30000000000000004
console.log((0.1*100 + 0.2*100)/100); // -> 0.3
But this strategy on its own isn't bullet proof because some multiplications still end up with these errors, like 0.14*100 = 14.000000000000002 and 0.29*100 = 28.999999999999996. To weed these out, we round the multiplied value.
console.log(0.01 + 0.14); // -> 0.15000000000000002
console.log((0.01*100 + 0.14*100)/100); // -> 0.15000000000000002
console.log((Math.round(0.01*100) + Math.round(0.14*100))/100) // -> 0.15
You can compare these using:
const arr = Array.from({length: 100}).map((_,i)=>i/100);
console.table(arr.map((a) => arr.map((b) => a + b)));
console.table(arr.map((a) => arr.map((b) => (a*100 + b*100)/100)));
console.table(arr.map((a) => arr.map((b) => (Math.round(a*100) + Math.round(b*100))/100)));
Therefore we can end up with these helper functions:
function sumFloats(a,b) {
return (Math.round(a * 100) + Math.round(b * 100)) / 100;
}
function sumFloatsForStorage(a,b) {
return (Math.round(a * 100) + Math.round(b * 100));
}
The main benefit of handling the weights this way is that you can now use FieldValue#increment() instead of a full blown transaction to shortcut updating the value. In the rare case that two users from the same company have an update collision, you can either retry the increment or fall back to the full transaction.
Inefficient data parsing
In your current code, you make use of .data() on the before and after states to get the data you need for your function. However, because you are pulling the user's entire document, you end up parsing all the fields in the document instead of just what you need - the bodyweight and company fields. You can do this using DocumentSnapshot#get(fieldName).
const afterData = change.after.data(); // parses everything - username, email, etc.
const { bodyweight, company } = afterData;
in comparison to:
const bodyweight = change.after.get("bodyweight"); // parses only "bodyweight"
const company = change.after.get("company"); // parses only "company"
Redundant math
For some reason you are calculating an absolute value of the difference between the weights, storing the sign of difference as a boolean and then using them together to apply the change back to the total weight lost.
The following lines:
const previousData = change.before.data();
const data = change.after.data();
const newWeight = data.bodyweight;
const oldWeight = previousData.bodyweight;
const lostWeight = oldWeight > newWeight;
const difference = diff(newWeight, oldWeight);
const currentWeightLost = companyRef.data().weightLostByAllEmployees;
const calcNewCWL = (currentWeightLost, difference, lostWeight) => {
if (!lostWeight) {
return currentWeightLost - difference;
}
return currentWeightLost + difference;
};
const newWeightLost = calcNewCWL(currentWeightLost, difference, lostWeight);
could be replaced with just:
const newWeight = change.after.get("bodyweight");
const oldWeight = change.before.get("bodyweight");
const deltaWeight = newWeight - oldWeight;
const currentWeightLost = companyRef.get("weightLostByAllEmployees") || 0;
const newWeightLost = currentWeightLost + deltaWeight;
Rolling it all together
exports.updateCompanyProgress = functions.firestore
.document("users/{userID}")
.onUpdate(async (change, context) => {
// "bodyweight" is the weight scaled up by 100
// i.e. "9.81kg" is stored as 981
const oldHundWeight = change.before.get("bodyweight") || 0;
const newHundWeight = change.after.get("bodyweight") || 0;
const oldCompany = change.before.get("company");
const newCompany = change.after.get("company");
const db = admin.firestore();
if (oldCompany === newCompany) {
// company unchanged
const deltaHundWeight = newHundWeight - oldHundWeight;
if (deltaHundWeight === 0) {
return null; // no action needed
}
const companyRef = db.doc(`/companies/${newCompany}`);
await companyRef.update({
weightLostByAllEmployees: admin.firestore.FieldValue.increment(deltaHundWeight)
});
} else {
// company was changed
const batch = db.batch();
const oldCompanyRef = db.doc(`/companies/${oldCompany}`);
const newCompanyRef = db.doc(`/companies/${newCompany}`);
// remove weight from old company
batch.update(oldCompanyRef, {
weightLostByAllEmployees: admin.firestore.FieldValue.increment(-oldHundWeight)
});
// add weight to new company
batch.update(newCompanyRef, {
weightLostByAllEmployees: admin.firestore.FieldValue.increment(newHundWeight)
});
// apply changes
await db.batch();
}
});
With transaction fallbacks
In the rare case where you get a write collision, this variant falls back to a traditional transaction to reattempt the change.
/**
* Increments weightLostByAllEmployees in all documents atomically
* using a transaction.
*
* `arrayOfCompanyRefToDeltaWeightPairs` is an array of company-increment pairs.
*/
function transactionIncrementWeightLostByAllEmployees(db, arrayOfCompanyRefToDeltaWeightPairs) {
return db.runTransaction((transaction) => {
// get all needed documents, then add the update for each to the transaction
return Promise
.all(
arrayOfCompanyRefToDeltaWeightPairs
.map(([companyRef, deltaWeight]) => {
return transaction.get(companyRef)
.then((companyDocSnapshot) => [companyRef, deltaWeight, companyDocSnapshot])
})
)
.then((arrayOfRefWeightSnapshotGroups) => {
arrayOfRefWeightSnapshotGroups.forEach(([companyRef, deltaWeight, companyDocSnapshot]) => {
const currentValue = companyDocSnapshot.get("weightLostByAllEmployees") || 0;
transaction.update(companyRef, {
weightLostByAllEmployees: currentValue + deltaWeight
})
});
});
});
}
exports.updateCompanyProgress = functions.firestore
.document("users/{userID}")
.onUpdate(async (change, context) => {
// "bodyweight" is the weight scaled up by 100
// i.e. "9.81kg" is stored as 981
const oldHundWeight = change.before.get("bodyweight") || 0;
const newHundWeight = change.after.get("bodyweight") || 0;
const oldCompany = change.before.get("company");
const newCompany = change.after.get("company");
const db = admin.firestore();
if (oldCompany === newCompany) {
// company unchanged
const deltaHundWeight = newHundWeight - oldHundWeight;
if (deltaHundWeight === 0) {
return null; // no action needed
}
const companyRef = db.doc(`/companies/${newCompany}`);
await companyRef
.update({
weightLostByAllEmployees: admin.firestore.FieldValue.increment(deltaHundWeight)
})
.catch((error) => {
// if an unexpected error, just rethrow it
if (error.code !== "resource-exhausted")
throw error;
// encountered write conflict, fall back to transaction
return transactionIncrementWeightLostByAllEmployees(db, [
[companyRef, deltaHundWeight]
]);
});
} else {
// company was changed
const batch = db.batch();
const oldCompanyRef = db.doc(`/companies/${oldCompany}`);
const newCompanyRef = db.doc(`/companies/${newCompany}`);
// remove weight from old company
batch.update(oldCompanyRef, {
weightLostByAllEmployees: admin.firestore.FieldValue.increment(-oldHundWeight)
});
// add weight to new company
batch.update(newCompanyRef, {
weightLostByAllEmployees: admin.firestore.FieldValue.increment(newHundWeight)
});
// apply changes
await db.batch()
.catch((error) => {
// if an unexpected error, just rethrow it
if (error.code !== "resource-exhausted")
throw error;
// encountered write conflict, fall back to transaction
return transactionIncrementWeightLostByAllEmployees(db, [
[oldCompanyRef, -oldHundWeight],
[newCompanyRef, newHundWeight]
]);
});
}
});
There are several points to adapt in your Cloud Function:
Do admin.firestore() instead of admin.firestore
You cannot get the data of the Company document by doing companyRef.data(). You must call the asynchronous get() method.
Use a Transaction when updating the Company document and return the promise returned by this transaction (see here for more details on this key aspect).
So the following code should do the trick.
Note that since we use a Transaction, we actually don't implement the recommendation of the second bullet point above. We use transaction.get(companyRef) instead.
exports.updateCompanyProgress = functions.firestore
.document("users/{userID}")
.onUpdate((change, context) => {
const previousData = change.before.data();
const data = change.after.data();
if (previousData === data) {
return null;
}
// You should do admin.firestore() instead of admin.firestore
const companyRef = admin.firestore().doc(`/companies/${data.company}`);
const newWeight = data.bodyweight;
const oldWeight = previousData.bodyweight;
const lostWeight = oldWeight > newWeight;
const difference = diff(newWeight, oldWeight);
if (!newWeight || difference === 0 || !oldWeight) {
return null;
} else {
return admin.firestore().runTransaction((transaction) => {
return transaction.get(companyRef).then((compDoc) => {
if (!compDoc.exists) {
throw "Document does not exist!";
}
const currentWeightLost = compDoc.data().weightLostByAllEmployees;
const newCompanyWeightLoss = calcNewCWL(
currentWeightLost,
difference,
lostWeight
);
transaction.update(companyRef, { weightLostByAllEmployees: newCompanyWeightLoss });
});
})
}
});

Firebase Cloud Function updating ref with incorrect values

I want to add a new node to the database if the node doesn't exist. I don't want to return anything to the client, I just want to update the database with the new values. On the client I have a listener that observes the credit_counts property, once the update happens it receives it there and notifies all users that this particular user has a new credit.
In the code below I check to see if (!snapshot.exists() and if it's not there I add the node to the database using admin.database().ref('/user_credits/{creditId}/{userId}').set({ dict });. After pasting the url I check the db and the layout is:
I'm a Swift developer. In Swift I can just do:
Database.database().reference().child("/user_credits/\(creditId)/\(userId)").setValue(dict) and the tree will be correct.
user_credits > {creditId} > {userId} > dict are incorrect. It should be user_credits > sample_123 > user_xyz > dict values. Where am I going wrong at?
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.updateViewsCtAtPostsRef = functions.https.onRequest((request, response) => {
const currentTimeStamp = Date.now();
const receivedTimeStamp = admin.database.ServerValue.TIMESTAMP;
const creditId = "sample_123";
const userId = "userId_xyz";
admin.database().ref('user_credits').child(creditId).child(userId).once('value', snapshot => {
if (!snapshot.exists()) {
var dict = {
"joined_date": receivedTimeStamp,
"timeStamp": receivedTimeStamp,
"credits_count": 1
};
return admin.database().ref('/user_credits/{creditId}/{userId}').set({ dict });
} else {
const previousTimeStamp = snapshot.child("timeStamp").val();
const creditsCount = snapshot.child("credits_count").val();
if (previousTimeStamp + whatever) < currentTimeStamp {
let updatedCount = creditsCount + 1
return admin.database().ref('/user_credits/{creditId}/{userId}').update({ "timeStamp": receivedTimeStamp, "credits_count": updatedCount });
} else {
return true
}
}
});
});
I had to change the ref to:
return admin.database().ref('/user_credits/' + creditId + '/' + userId).set({ "joined_date": receivedTimeStamp, "timeStamp": receivedTimeStamp, "credits_count": 1 });
I also had to update the ref inside the else statement to follow the same format.
The syntax is fine, but the reference does not match the structure; that should rather be:
admin.database().ref('user_credits').child(creditId).child(userId).child('dict')
... else there won't be any snapshot.child("timeStamp") or snapshot.child("credits_count").

localStorage .parse .stringify

I need help with pushing 2 data values into localStorage. I know a little about the stringify and parse methods but cant grasp how to implement them.The 2 data values are from "Scores" and "saveName"(a username that is put into an input box).
var Score = (answeredCorrect * 20) + (timeleft);
var saveName = document.querySelector("#saveName");
function Storage() {
localStorage.setItem("User", JSON.stringify(saveName.value));
localStorage.setItem("Scores", JSON.stringify(Score));
var GetStorage = localStorage.getItem("User");
var GetStorage2 = localStorage.getItem("Scores");
return {
first:console.log("GetStorage: "+ GetStorage + GetStorage2),
second:GetStorage,
third:GetStorage2,
};
};
var values = Storage();
var first = values.first;
var second = values.second;
var third = values.third;
As mentioned in the comments you need to parse it once retrieved from storage with JSON.parse, also naming Storage should be avoided.
Since your making a wrapper for localstorage, it could be done like this:
const Store = {
set: (key, value) => localStorage[key] = JSON.stringify(value),
get: key => JSON.parse(localStorage[key])
}
Then you can simply call it like the following, with a set and get methods:
//
Store.set('Score', Score)
Score = Store.get('Score')
//
Store.set('User', saveName.value)
saveName = Store.get('User')
Though you only need to get() on page load as you already have the value in Score/saveName etc.

Why does it not detect DB changes(functions)?

So I'm learning firebase functions and I'm trying to have a function detect a change in the DB but it doesn't. Its suppose to be detect when the gamemode is changed though it doesn't do shit. if it does detect the change it changes it to gamemode 3 though as stated it does not do anything. This is done via firestore
my test DB: https://gyazo.com/91afd83cd27a0e7c55bd79b2b86529bf
Here is what i do to trigger it:
https://gyazo.com/8c7206d80a343b0e7ee9432cf3fae47c
and my node.js script is as follows:
exports.tellGameModeofUser = functions.firestore
.document('users/{userId}')
.onUpdate(event => {
// Retrieve the current and previous value
const data = event.data.data();
const previousData = event.data.previous.data();
// We'll only update if the name has changed.
// This is crucial to prevent infinite loops.
console.log("the new game mode: " + data );
console.log("old gmae mode: " + previousData)
if (data.gamemode === previousData.gamemode){
return;
}else if (data.gamemode === "1"){
console.log("value changed game mode on");
}
});
When I check the log is see nothing posted, there is no trigger.
The cloud functions have been updated, so you need to change to the following:
exports.tellGameModeofUser = functions.firestore
.document('users/{userId}')
.onUpdate(event => {
const data = event.data.data();
const previousData = event.data.previous.data();
to this:
exports.tellGameModeofUser = functions.firestore.document('users/{userId}').onUpdate((change,context) => {
const data = change.after.data();
const previousData = change.before.data();
});
more info here:
https://firebase.google.com/docs/functions/beta-v1-diff#cloud-firestore
So 1 I had to update the firebase to 1.0.0 and that had allowed me to use the new function syntax. Then I was able to use the new syntax for onUpdate functions and have the function run as needed.
exports.tellGameModeofUser = functions.firestore.document('Users/{userId}')
.onUpdate((change,context) => {
console.log("Hey");
console.log("change: " +change);
const beforeData = change.before.data() // data before the write
const afterData = change.after.data(); // data after the write
// We'll only update if the name has changed.
// This is crucial to prevent infinite loops.
console.log("the new game mode: " + afterData );
console.log("old gmae mode: " + beforeData)
if (afterData.gamemode === beforeData.gamemode){
console.log("game mode is the same");
return;
}else if (afterData.gamemode === "1"){
console.log("value changed game mode on");
}
});

Array Reduce Error in Cloud Function

Below is the index.JS of my firebase database cloud function.
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.update = functions.database.ref('/Player')
.onWrite(event=>{
ref = admin.database().ref(`/users/UMFabxncKoZ6XcHpPQYZHizJ7Yr1/week1`);
pref1 = admin.database().ref("Player").child("playerweek8");
ref2 = admin.database().ref(`/users/UMFabxncKoZ6XcHpPQYZHizJ7Yr1`);
if(n === 2){
ref2.once('value', function(usersSnapshot){
var users = usersSnapshot.val();
var selection = users.selection;
const loadedPlayers = admin.database().ref("Player").child("playerweek8").orderByChild("id");
var normalizedPlayers = loadedPlayers.reduce(function(acc, next) { acc[next.id] = next; return acc; }, {});
var selectedPlayers = selection.map(function(num){
return normalizedPlayers[num];
});
var players = selectedPlayers;
var sum = function(items, prop){
return items.reduce( function(a, b){
return a + b[prop];
}, 0);
};
var points = sum(players, 'goals');
return ref.set(points);
});
}
else return ref.set(0);
});
The function returns the error message: TypeError: loadedPlayers.reduce is not a function at /user_code/index
Is there a way that the reduce and mapping function can work to address this error? Sorry if this question is silly but I am new to Firebase.
The way loadedPlayers stands right now, it's a Query object, because that's what orderByChild() returns. You haven't actually performed the query to get the data at Player/playerweek8.
You'll need to use the once() method on loadedPlayers to actually make the query and get the data.
If you're new to Cloud Functions, I suggest doing a codelab and looking at the sample code to find out how it works.

Categories

Resources