Firebase - Toggling value with transactions - javascript

I'm trying to let users favorite a project. I'm storing these projects at 2 places so I have to update them simultaneously. After looking at the firebase docs, using transactions seemed to be the best option.
.
Function to toggle the favorite status:
function toggleFavorite (projectReference, uid) {
projectReference.transaction(function(project) {
console.log('Before-Favorites :' + project.favoriteCount);
if (project.favorites && project.favorites[uid]) {
project.favoriteCount--;
project.favorites[uid] = null;
} else {
project.favoriteCount++;
if(!project.favorites) {
project.favorites= {};
}
project.favorites[uid] = true;
}
console.log(' After-Favorites :' + project.favoriteCount);
return project;
});
};
Function to add the eventListeners to the projects:
function AddToFavorite (uid, authorId) {
const favoriteList = document.querySelectorAll('.btnFavorite');
for(var i = 0; i<favoriteList.length; i++) {
favoriteList[i].addEventListener('click', function(event) {
const projectId = this.dataset.id;
console.log(projectId);
const globalProjectRef = firebase.database().ref('/projects/' + projectId);
const userProjectRef = firebase.database().ref('/user-projects/' + authorId + '/' + projectId);
toggleFavorite(globalProjectRef,uid);
toggleFavorite(userProjectRef,uid);
});
}
}
I want to store the uid of the current user under a 'favorites' node within the project location.
When i want to store the data I can see it appearing in the database but removing it after instantly. Followed by that i get an error in the console that my project object is null.
What's the best way of solving this issue ?

Related

Wait for all Firebase data query requests before executing code

I am trying to fetch data from different collections in my cloud Firestore database in advance before I process them and apply them to batch, I created two async functions, one to capture the data and another to execute certain code only after all data is collected, I didn't want the code executing and creating errors before the data is fetched when i try to access the matchesObject after the async function to collect data is finished, it keeps saying "it cannot access a property matchStatus of undefined", i thought took care of that with async and await? could anyone shed some light as to why it is undefined one moment
axios.request(options).then(function(response) {
console.log('Total matches count :' + response.data.matches.length);
const data = response.data;
var matchesSnapshot;
var marketsSnapshot;
var tradesSnapshot;
var betsSnapshot;
matchesObject = {};
marketsObject = {};
tradesObject = {};
betsObject = {};
start();
async function checkDatabase() {
matchesSnapshot = await db.collection('matches').get();
matchesSnapshot.forEach(doc => {
matchesObject[doc.id] = doc.data();
console.log('matches object: ' + doc.id.toString())
});
marketsSnapshot = await db.collection('markets').get();
marketsSnapshot.forEach(doc2 => {
marketsObject[doc2.id] = doc2.data();
console.log('markets object: ' + doc2.id.toString())
});
tradesSnapshot = await db.collection('trades').get();
tradesSnapshot.forEach(doc3 => {
tradesObject[doc3.id] = doc3.data();
console.log('trades object: ' + doc3.id.toString())
});
betsSnapshot = await db.collection('bets').get();
betsSnapshot.forEach(doc4 => {
betsObject[doc4.id] = doc4.data();
console.log('bets object: ' + doc4.id.toString())
});
}
async function start() {
await checkDatabase();
// this is the part which is undefined, it keeps saying it cant access property matchStatus of undefined
console.log('here is matches object ' + matchesObject['302283']['matchStatus']);
if (Object.keys(matchesObject).length != 0) {
for (let bets of Object.keys(betsObject)) {
if (matchesObject[betsObject[bets]['tradeMatchId']]['matchStatus'] == 'IN_PLAY' && betsObject[bets]['matched'] == false) {
var sfRef = db.collection('users').doc(betsObject[bets]['user']);
batch11.set(sfRef, {
accountBalance: admin.firestore.FieldValue + parseFloat(betsObject[bets]['stake']),
}, {
merge: true
});
var sfRef = db.collection('bets').doc(bets);
batch12.set(sfRef, {
tradeCancelled: true,
}, {
merge: true
});
}
}
}
});
There are too many smaller issues in the current code to try to debug them one-by-one, so this refactor introduces various tests against your data. It currently won't make any changes to your database and is meant to be a replacement for your start() function.
One of the main differences against your current code is that it doesn't unnecessarily download 4 collections worth of documents (two of them aren't even used in the code you've included).
Steps
First, it will get all the bet documents that have matched == false. From these documents, it will check if they have any syntax errors and report them to the console. For each valid bet document, the ID of it's linked match document will be grabbed so we can then fetch all the match documents we actually need. Then we queue up the changes to the user's balance and the bet's document. Finally we report about any changes to be done and commit them (once you uncomment the line).
Code
Note: fetchDocumentById() is defined in this gist. Its a helper function to allow someCollectionRef.where(FieldPath.documentId(), 'in', arrayOfIds) to take more than 10 IDs at once.
async function applyBalanceChanges() {
const betsCollectionRef = db.collection('bets');
const matchesCollectionRef = db.collection('matches');
const usersCollectionRef = db.collection('users');
const betDataMap = {}; // Record<string, BetData>
await betsCollectionRef
.where('matched', '==', false)
.get()
.then((betsSnapshot) => {
betsSnapshot.forEach(betDoc => {
betDataMap[betDoc.id] = betDoc.data();
});
});
const matchDataMap = {}; // Record<string, MatchData | undefined>
// betIdList contains all IDs that will be processed
const betIdList = Object.keys(betDataMap).filter(betId => {
const betData = betDataMap[betId];
if (!betData) {
console.log(`WARN: Skipped Bet #${betId} because it was falsy (actual value: ${betData})`);
return false;
}
const matchId = betData.tradeMatchId;
if (!matchId) {
console.log(`WARN: Skipped Bet #${betId} because it had a falsy match ID (actual value: ${matchId})`);
return false;
}
if (!betData.user) {
console.log(`WARN: Skipped Bet #${betId} because it had a falsy user ID (actual value: ${userId})`);
return false;
}
const stakeAsNumber = Number(betData.stake); // not using parseFloat as it's too lax
if (isNaN(stakeAsNumber)) {
console.log(`WARN: Skipped Bet #${betId} because it had an invalid stake value (original NaN value: ${betData.stake})`);
return false;
}
matchDataMap[matchId] = undefined; // using undefined because its the result of `doc.data()` when the document doesn't exist
return true;
});
await fetchDocumentsById(
matchesCollectionRef,
Object.keys(matchIdMap),
(matchDoc) => matchDataMap[matchDoc.id] = matchDoc.data()
);
const batch = db.batch();
const queuedUpdates = 0;
betIdList.forEach(betId => {
const betData = betDataMap[betId];
const matchData = matchDataMap[betData.tradeMatchId];
if (matchData === undefined) {
console.log(`WARN: Skipped /bets/${betId}, because it's linked match doesn't exist!`);
continue;
}
if (matchData.matchStatus !== 'IN_PLAY') {
console.log(`INFO: Skipped /bets/${betId}, because it's linked match status is not "IN_PLAY" (actual value: ${matchData.matchStatus})`);
continue;
}
const betRef = betsCollectionRef.doc(betId);
const betUserRef = usersCollectionRef.doc(betData.user);
batch.update(betUserRef, { accountBalance: admin.firestore.FieldValue.increment(Number(betData.stake)) });
batch.update(betRef, { tradeCancelled: true });
queuedUpdates += 2; // for logging
});
console.log(`INFO: Batch currently has ${queuedUpdates} queued`);
// only uncomment when you are ready to make changes
// batch.commit();
}
Usage:
axios.request(options)
.then(function(response) {
const data = response.data;
console.log('INFO: Total matches count from API:' + data.matches.length);
return applyBalanceChanges();
}

Stuck with JSON API Call in Javascript

I am trying to build a Calculator where you can Pick different Cryptocurrencies and build a Portfolio for Testing and Tracking. I choose Coingecko API v3 for this and use Fetch to do the API Calls. https://www.coingecko.com/en/api
The API Calls are working and I get JSON Data from the Server. I built a Function that is delivering the Top 100 Coins, putting them into a Datalist-Tag for Choosing from an Input:
function fetch_coinlist(url) {
fetch(url)
.then(function(response) {
if (response.status !== 200) {
alert("Can Not get Price Api! Status: " + response.status);
return;
}
response.json().then(function(data) {
document.getElementById("coinlist").innerHTML = "";
for (i = 0; i <= 99; i++) {
const toplist = document.createElement("option");
toplist.id = data[i].symbol;
toplist.innerHTML =
data[i].symbol.toUpperCase() + " - " + data[i].name;
document.getElementById("coinlisting").appendChild(toplist);
//console.log(data[i].id);
//console.log(data[i].name);
//console.log(data[i].symbol);
//console.log(data[i].image);
//console.log(data[i].current_price);
}
});
})
.catch(function(err) {
alert("Can Not get Price Api! Status: " + err);
});
}
<input list="coinlisting" id="coinlist" name="coinlist" class="curser-choose" />
<datalist id="coinlisting" placeholder="Choose Coin"></datalist>
This is working, I am using a For loop to go through the response. I try to Display the Price for a Coin if the User picked one Option from this Datalist and for further functionality I think it would be best that I could call an object like data.coinname.current_price so I can build a Function where i could pass a string to it and get the price for that coin.
However, right now I would have to use data[0].current_price for the current Price of the first item in the list. The items on the called List can Change over time, so I can not make a static assignment, I think I could do this each time I call the API but it would not benefit my target to have a function that I can feed with a name as a string to do the Price Call.
It is possible to get the Price in the same call as the list but I can not figure out how I would be able to do what I have in mind with that. On the site for the API are different calls listed, one of the first is the /coins/list call and it says "Use this to obtain all the coins’ id in order to make API calls" and gives a JSON object for each available coin like:
{
"id": "1inch",
"symbol": "1inch",
"name": "1inch"
}
Do I need to do this call first in order to achieve what I have in mind? But I'm unsure how that would help me get the solution I am looking for... I am struggling to find a solution for this and feel stuck, I feel like I don't understand it properly and it should be not as hard as I think it is right now :D If you have an idea of how to accomplish this please let me know!
You could do something like this. I kept the html simple.
fetch_coinlist: fetches a coin list. After fetching the coins list it converts the result to an object instead of a list. Each coin can be accessed using coins["<coin id>"].
show_coinlist does the visualisation
addEventListener catches when you select an item.
const gecko = "https://api.coingecko.com/api/v3/";
var coins = {};
// selector actions
const selector = document.querySelector('#coinlisting');
const info = document.querySelector('#info');
selector.addEventListener('change', event => {
info.innerHTML = "Selected item: " + selector.value + " : " + coins[selector.value].name;
});
function fetch_coinlist() {
fetch(gecko + "coins/list")
.then(function(response) {
if (response.status !== 200) {
alert("Can Not get List Api! Status: " + response.status);
return;
}
response.json().then(function(data) {
coins = {};
for (let i = 0, l = data.length; i < l; i++) {
let {id, ...info} = data[i];
coins[id] = info;
}
show_coinlist();
});
})
.catch(function(err) {
alert("Can Not get Price Api! Status: " + err);
});
}
function show_coinlist(){
for (let id in coins) {
let item = coins[id];
const toplist = document.createElement("option");
toplist.value = id;
toplist.innerHTML = item.symbol.toUpperCase() + " - " + item.name;
document.getElementById("coinlisting").appendChild(toplist);
}
}
fetch_coinlist();
<select id="coinlisting" placeholder="Choose Coin"></select>
<div id="info"></div>

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").

Save multiple objects in a sessionStorage

I am trying to save objects in a sessionStorage anytime I click on a bookmark icon. But instead of saving several objects. Anytime I click on my icon bookmark to save a book; it is being overridden by the new value.
How could I add several objects (books) in my sessionStorage?
Thanks
here is the code snippet
try {
const responseData = await sendHttpRequest(
'GET',
"https://www.googleapis.com/books/v1/volumes?q=" + search );
const listOfBooks = responseData;
console.log(listOfBooks);
for (i=0; i < listOfBooks.items.length; i++) {
console.log(listOfBooks.items[i]);
const postEl = document.importNode(template.content, true);
console.log("postEl", postEl);
//BookMark Icon event
const target = postEl.querySelector('.icon-bookmark');
target.addEventListener('click', () => {
sessionStorage.setItem('bookStorage', bookStorage);
sessionStorage.setItem('bookStorage' , JSON.stringify(bookStorage));
const extractedBook = sessionStorage.getItem('bookStorage');
const extractedBookInfo = JSON.parse(sessionStorage.getItem('bookStorage'));
console.log(extractedBookInfo);
});
postEl.querySelector('.id').textContent = 'id: '+ listOfBooks.items[i].id;
postEl.querySelector('.titre').textContent = 'Titre: '+ listOfBooks.items[i].volumeInfo.title;
postEl.querySelector('.author').textContent = 'Auteur: '+ listOfBooks.items[i].volumeInfo.authors;
postEl.querySelector('.desc').textContent ='Description: '+ listOfBooks.items[i].volumeInfo.description;
postEl.querySelector('img').src = listOfBooks.items[i].volumeInfo.imageLinks.thumbnail;
if (postEl.querySelector('.desc').textContent.length > 200) {
postEl.querySelector('.desc').textContent = postEl.querySelector('.desc').textContent.substring(0,200);
}
if (!postEl.querySelector('img').src) {
postEl.querySelector('img').src ="resources/css/img/unavailable.png";
}
listElement.append(postEl);
const bookStorage = {
id: listOfBooks.items[i].id,
title: listOfBooks.items[i].volumeInfo.title,
author: listOfBooks.items[i].volumeInfo.authors,
desc: listOfBooks.items[i].volumeInfo.description,
img: listOfBooks.items[i].volumeInfo.imageLinks.thumbnail
};
}
} catch (error) {
alert(error.message);
}
}
btnRechercher.addEventListener('click', fetchBooks, false);
You can only have unique keys inside session storage, that’s why everytime you insert a new entity with the same key the storage overwrites the old one. You can do a for and save every book, for example ‘book_1’, ‘book_2’. Or you can store an array, but you have to use json.stringify(name_of_your_array) and then when you want your array back you can use json.parse(storage.getItem(key))

Write to two database refs simultaneously

I have a Firebase web app serving as a registration system. When a user registers for a course, the course data is added to their list of all registrations as part of the callback function. When the user registers, the newest class is duplicated in the list. On a page load, each course is only listed once.
Realtime Database Structure
{
courses: {
courseIdA: {
// course data
},
couseIdB ... {}
},
users: {
uid: {
regs: {
courseIdA: true
}
}
}
}
When a user registers, they are added to both the course ID as a member object and to their users object under their uid. The callback fires twice because I'm writing to the courses ref and the users ref. Is there a way to write to both simultaneously? Or do I need to come up with a better structure for the database?
Get classes, listen for changes
PDReg.prototype.getAllClasses = function() {
this.database = firebase.database()
var uid = firebase.auth().currentUser.uid;
var courses = [];
var today = new Date().toISOString();
this.classesRef = this.database.ref('courses');
this.userRef = this.database.ref('users/' + uid + "/regs");
var setClass = function(snapshot) {
var course = snapshot.val();
course.key = snapshot.key;
// check for the current user in a course
if(course.members) {
if(course.members.hasOwnProperty(uid)) {
// This callback fires twice when someone registers
this.buildUserClasses(course);
} else {
this.buildAllClasses(course)
}
} else {
this.buildAllClasses(course)
}
}.bind(this);
// listen for changes to the classes database, rebuild the UI
this.classesRef.orderByChild('start').startAt(today).on('child_added', setClass);
this.classesRef.on('child_changed', setClass);
};
Register
PDReg.prototype.register = function(e) {
// It's a form submit, don't reload the page
e.preventDefault();
var form = this.courseForm;
var classes = [];
var uid = firebase.auth().currentUser.uid;
for (var i=0; i<form.elements.length; i++) {
// build the object to submit and add to an array
}
for (var i=0; i<classes.length; i++) {
this.coursesRef = this.database.ref('courses/' + classes[i].id + '/members/' + uid);
// Write the member object to `courses` under the correct key
this.coursesRef.set({'code': classes[i]['code']}).then(function(classes) {
// write to the user ref
firebase.database().ref('users/' + uid + '/regs/' + id).set(true);
onSuccess(title);
document.getElementById('allCourses').removeChild(document.getElementById(id))
}, function(e) {
onFailure(e, title);
});
}
};

Categories

Resources