Firebase when add/delete data, app do functions more than once - javascript

I have problems with my money app. When I add/delete data from my app (products collection), my app do function "sumPrices()" more than one. For example: When I add one product, make once, add another product, make twice, add another product make three etc. This happen in the same way with delete data.
A do something wrong in my code?
Callback.push push data do array where I unsubscribe events from firebase.
AddStatsUI add UI to my DOM.
index.js:
// delete products
const handleTableClick = e => {
console.log(e); // mouseevent
if (e.target.tagName === 'BUTTON'){
const id = e.target.parentElement.parentElement.getAttribute('data-id');
db.collection('users')
.doc(user.uid)
.collection('products')
.doc(id)
.delete()
.then(() => {
// show message
updateMssg.innerText = `Product was deleted`;
updateMssg.classList.add('act');
setTimeout(() => {
updateMssg.innerText = '';
updateMssg.classList.remove('act');
}, 3000);
productUI.delete(id);
products.sumPrices(user.uid, callbacks).then(value => {
sumStats.addStatsUI('','');
const unsubscribe = db.collection('users').doc(user.uid).get().then(snapshot => {
sumStats.addStatsUI(value[0], snapshot.data().budget);
})
callbacks.push(unsubscribe);
});
})
}
}
table.addEventListener('click', handleTableClick);
callbacks.push(() => table.removeEventListener('click', handleTableClick))
//add new products to firebase
const handleExpenseFormSubmit = e => {
e.preventDefault();
const name = expenseForm.productName.value.trim();
const price = Number(expenseForm.price.value.trim());
console.log(`Product added: ${name}, ${price}`);
const user = firebase.auth().currentUser.uid;
products.addProduct(name, price, user)
.then(() => {
products.sumPrices(user, callbacks).then(value => {
sumStats.addStatsUI('','');
const unsubscribe = db.collection('users').doc(user).onSnapshot(snapshot => {
sumStats.addStatsUI(value, snapshot.data().budget);
})
callbacks.push(unsubscribe);
});
expenseForm.reset()
})
.catch(err => console.log(err));
}
expenseForm.addEventListener('submit', handleExpenseFormSubmit);
callbacks.push(() => expenseForm.removeEventListener('submit', handleExpenseFormSubmit))
product.js:
class Product {
constructor(name, price, budget, user) {
this.products = db.collection('users');
this.budget = budget;
this.name = name;
this.price = price;
this.user = user;
}
async addProduct(name, price, user) { //dodaje produkt do firebase
const now = new Date();
const product = {
name: name,
price: price,
created_at: firebase.firestore.Timestamp.fromDate(now),
};
const response = await this.products.doc(user).collection('products').add(product);
return response;
}
getProducts(callback, user){ //download list from firebase
this.products.doc(user).collection('products')
.orderBy("created_at", "desc")
.onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
if(change.type === 'added'){
//udpate UI
return callback(change.doc.data(), change.doc.id);
}
});
});
}
updateBudget(budget, user){
this.budget = budget;
db.collection('users').doc(user).update({budget: budget});
// callbacks.push(unsubscribe);
}
async sumPrices(user, callbacks){
let finish = [];
const unsubscribe = this.products.doc(user).collection('products').onSnapshot(snapshot => {
let totalCount = 0;
snapshot.forEach(doc => {
totalCount += doc.data().price;
});
const a = totalCount;
console.log(a);
finish.push(a);
return finish;
})
callbacks.push(unsubscribe);
return finish;
};
};
sumStatsUI.js:
class Stats {
constructor(stats, circle, budget){
this.stats = stats;
this.circle = circle;
this.budget = budget;
}
addStatsUI(data, budget){
if(data) {
const outcome = Math.round(data * 100) / 100;
const sumAll = Math.round((budget - outcome) * 100) / 100;
this.stats.innerHTML += `
<div><span class="budget-name">Budget: </span> <span class="stat-value">${budget}$</span></div>
<div><span class="budget-name">Outcome: </span> <span class="stat-value outcome-value">${outcome}$</span></div>
<div><span class="budget-name">All: </span> <span class="stat-value last-value">${sumAll}$</span></div>
`;
const circle = Math.round(((outcome * 100) / budget) * 100) / 100;
this.circle.innerHTML += `${circle}%`;
} else {
this.stats.innerHTML = '';
this.circle.innerHTML = '';
}};
};
export default Stats;
I add console.log to sumPrices
App screenshot, when I add 2 products and try update budget

Okey, a add some improvement to my code, but still have problems with subscriptions. Now everything it's okey, but when I log out and log in functions getProducts() and updateBudget() no unsubscribe.
Code here:
index.js:
//get the products and render
const unsubscribe = products.getProducts((data, id) => {
console.log(data, id);
productUI.render(data, id);
}, user.uid);
callbacks.push(unsubscribe);
//update budget + form
const handleBudgetFormSubmit = e => {
e.preventDefault();
//update budget
const budget = Number(budgetForm.budget_value.value.trim());
sumStats.addStatsUI('', '');
products.updateBudget(budget, user.uid);
//reset form
budgetForm.reset();
const budgetCart = document.querySelector('#budget');
budgetCart.classList.remove('active');
// show message
updateMssg.innerText = `Your budget was updated to ${budget}$`;
updateMssg.classList.add('act');
setTimeout(() => {
updateMssg.innerText = '';
updateMssg.classList.remove('act');
}, 3000);
};
budgetForm.addEventListener('submit', handleBudgetFormSubmit);
callbacks.push(() =>
budgetForm.removeEventListener('submit', handleBudgetFormSubmit)
);
and else to onAuthStateChanged() -> if(user):
} else {
console.log('user logged out');
authUI('');
productUI.render('');
sumStats.addStatsUI('');
console.log('Callbacks array', callbacks);
callbacks.forEach(callback => callback());
callbacks.length = 0;
}
});
getProducts() and updateBudget():
getProducts(callback, user) {
//download list from firebase
this.products
.doc(user)
.collection('products')
.orderBy('created_at', 'desc')
.onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
if (change.type === 'added') {
//udpate UI
return callback(change.doc.data(), change.doc.id);
}
});
});
}
updateBudget(budget, user) {
console.log('budget', budget, user);
const db = firebase.firestore();
// this.budget = budget;
db.collection('users')
.doc(user)
.update({ budget: budget });
}
When I log out and log in:
When I have getProducts and add product to collection, this function render (render()) product twice, but add to collection once. When I update budget this return budget but after that, return 0 (on DOM where show budget a can see "Infinity")
And one thing, when I log out, console return error:
TypeError: callback is not a function
at eval (index.js:182)
at Array.forEach (<anonymous>)
at Object.eval [as next] (index.js:182)
at eval (index.cjs.js:1226)
at eval (index.cjs.js:1336)
I think it's because getProducts and updateBudget don't return unsubscribe, but undefined.
Maybe someone have solution for this?

Related

data position with firebase

I'm trying to keep in position the data that I'm getting from firebase but when i refresh the page it's showed up in diferent positions and not in his created position
I thought use 'position' and 'beforeend' but the result it's the same
for example I post (a,b,c,d,e) and when I refresh the page it's shows (e,d,a,b,c) here some pictures enter image description here
const addForm = document.querySelector(".add");
const list = document.querySelector(".todos");
const search = document.querySelector(".search input");
// generate new toDo's
const generateTemplate = (toDo, id) => {
let html = ` <li data-id=${id} class="list-group-item d-flex justify-content-between align-items-center">
<span>${toDo.title}</span><i class="far fa-trash-alt delete"></i>
</li>`;
const position = "beforeend";
list.insertAdjacentHTML(position, html);
};
const deleteTodo = (id) => {
const todos = document.querySelectorAll("li");
todos.forEach((toDo) => {
if (toDo.getAttribute("data-id") === id) {
toDo.remove();
}
});
};
// get the info in the page
db.collection("Todos").onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
const doc = change.doc;
if (change.type === "added") {
generateTemplate(doc.data(), doc.id);
} else if (change.type === "removed") {
deleteTodo(doc.id);
}
});
});
// submit the todo
addForm.addEventListener("submit", (e) => {
e.preventDefault();
const now = new Date();
const toDo = {
title: addForm.add.value.trim(),
created_at: firebase.firestore.Timestamp.fromDate(now),
};
db.collection("Todos")
.add(toDo)
.then(() => {
console.log("todo added");
})
.catch((err) => {
console.log(err);
});
addForm.reset();
});
// delete todo's
list.addEventListener("click", (e) => {
if (e.target.classList.contains("delete")) {
const id = e.target.parentElement.getAttribute("data-id");
db.collection("Todos")
.doc(id)
.delete()
.then(() => {
console.log("Todo Deleted");
});
}
});
// search todo's
const filterTodos = (term) => {
Array.from(list.children)
.filter((todo) => !todo.textContent.toLowerCase().includes(term))
.forEach((todo) => todo.classList.add("filtered"));
Array.from(list.children)
.filter((todo) => todo.textContent.toLowerCase().includes(term))
.forEach((todo) => todo.classList.remove("filtered"));
};
search.addEventListener("keyup", () => {
const term = search.value.trim().toLowerCase();
filterTodos(term);
});
db.collection("Todos")
.get()
.then((snapshot) => {
snapshot.docs.forEach((doc) => console.log(doc.data()));
});
You need to use orderBy to fetch documents with sorting.
It should look like this.
db.collection("Todos")
.orderBy('created_at')
.get()
.then((snapshot) => {
snapshot.docs.forEach((doc) => console.log(doc.data()));
});

trouble with displaying data from firestore

I'm having two issues this moment
I am having trouble displaying the data from my firestore document after the user signs in and they do have a document with data
I have a button that lets the user upload a document and I will parse it to extract all important data. That data is then put into the document with Ids, which is basically the userId + what number doc it is belonging to the user. When they upload the data it seems that all data extracted uses the same ID and only the last one extracted gets displayed.
this is my code:
const loansRef = firebase.firestore().collection('goals');
const [ courseGoals, setCourseGoals ] = useState([]);
const [goalCounter, setGoalCounter] = useState(0)
//let the user upload doc, what gets called when they press button
const pickDocument = async () => {
try {
let input = await DocumentPicker.getDocumentAsync({
type: "text/plain",
});
setUserOut(await FileSystem.readAsStringAsync(input.uri));
} catch (error) {
console.log(error);
}
createLoans();
};
//extracts important info and returns array with all info for one loan
const fileParser = () => {
const parsedLoans = [];
var newUserOut = userOut;
if (newUserOut.length == 0) {
return;
}
//remove the grants
var grantPos = newUserOut.search("Grant Type:");
var pos = newUserOut.search("Loan Type:");
//hopefully just the loans now
newUserOut = newUserOut.slice(pos, grantPos);
while (newUserOut.length > 0) {
var lastPos = newUserOut.lastIndexOf("Loan Type:");
parsedLoans.push(newUserOut.slice(lastPos, newUserOut.length));
newUserOut = newUserOut.slice(0, lastPos);
}
//console.log('parsed loans: ' + parsedLoans)
return parsedLoans;
};
//where we actually create loans and get the important data for each loan
const createLoans = () => {
const newLoans = fileParser();
const title= 'Loan Amount:$'
const interest = 'Loan Interest Rate:'
for(let i =0; i < newLoans.length; i++){
let loan = newLoans[i]
let goalTitle=loan.substring(loan.indexOf(title)+title.length,loan.indexOf('Loan Disbursed Amount:'))
//console.log("goalTitle: " + goalTitle)
let interestRate = loan.substring(loan.indexOf(interest)+interest.length,loan.indexOf('Loan Repayment Plan Type'))
//console.log("Interest rate: "+ interestRate)
let years = 0
let paidOff = 0
addGoalHandler(goalTitle,interestRate,years,paidOff)
}
return
};
useEffect(() => {
getPW()
let isMounted = true;
if (isMounted) {
loansRef.doc(userId).onSnapshot(
(docSnapshot) => {
if(!docSnapshot.exists){console.log('doc doesnt exist, start from scratch')}
else{
console.log('loaded successfully '+docSnapshot.data())
setGoalCounter(docSnapshot.data().loans.length)
console.log(goalCounter)
}
},
(error) => {
console.log(error);
}
);
}
return () => {
isMounted = false;
};
}, []);
const addGoalHandler = (goalTitle, interestRate, years, paidOff) => {
//setCourseGoals([...courseGoals, enteredGoal])
setGoalCounter(goalCounter+1)
setCourseGoals((prevGoals) => [
...courseGoals,
{
id:userId.toString() + goalCounter.toString(),
value: goalTitle,
interest: interestRate,
years: years,
paidOff: paidOff
}
]);
addToFB(goalTitle, interestRate,years,paidOff)
//var oldIDS = docIDS
//oldIDS.push(userId.toString() + goalCounter.toString())
//setDocIDS(oldIDS)
setIsAddMode(false);
};
const cancelGoalAdditionHandler = () => {
setIsAddMode(false);
};
const addToFB = async (goalTitle, interestRate, years, paidOff) => {
//adding data to firebase, takes into account if doc exists already
const loadDoc = await loansRef.doc(userId).get()
.then((docSnapshot)=> {
if(docSnapshot.exists){
loansRef.doc(userId).onSnapshot((docu)=>{
const updateLoansArr = loansRef.doc(userId).update({
loans: firebase.firestore.FieldValue.arrayUnion({
id: userId+goalCounter.toString(),
value: goalTitle,
interest: interestRate,
years: years,
paidOff: paidOff
})
})
//setGoalCounter(goalCounter+1)
})
}
else{
const addDoc = loansRef.doc(userId).set({
loans: firebase.firestore.FieldValue.arrayUnion({
id: userId+goalCounter.toString(),
value: goalTitle,
interest: interestRate,
years: years,
paidOff: paidOff
})
})
//setGoalCounter(goalCounter+1)
}})
//setGoalCounter(goalCounter+1)
}
const removeGoalHandler = async (goalId) => {
/*
setCourseGoals((currentGoals) => {
loansRef.doc(goalId).delete().then(console.log('removed correctly'))
return currentGoals.filter((goal) => goal.id !== goalId);
});
setGoalCounter(goalCounter-1)
*/
//need to use the value and not the goalId
const removeGoal = await loansRef.doc(goalId).update({
goals: firebase.firestore.FieldValue.arrayRemove(goalId)
})
setCourseGoals((currentGoals)=> {
return currentGoals.filer((goal)=> goal.id !== goalId)
})
};

Firestore cloud function to recursively update subcollection/collectionGroup

I have this cloud function:
import pLimit from "p-limit";
const syncNotificationsAvatar = async (
userId: string,
change: Change<DocumentSnapshot>
) => {
if (!change.before.get("published") || !change.after.exists) {
return;
}
const before: Profile = change.before.data() as any;
const after: Profile = change.after.data() as any;
const keysToCompare: (keyof Profile)[] = ["avatar"];
if (
arraysEqual(
keysToCompare.map((k) => before[k]),
keysToCompare.map((k) => after[k])
)
) {
return;
}
const limit = pLimit(1000);
const input = [
limit(async () => {
const notifications = await admin
.firestore()
.collectionGroup("notifications")
.where("userId", "==", userId)
.limit(1000)
.get()
await Promise.all(
chunk(notifications.docs, 500).map(
async (docs: admin.firestore.QueryDocumentSnapshot[]) => {
const batch = admin.firestore().batch();
for (const doc of docs) {
batch.update(doc.ref, {
avatar: after.avatar
});
}
await batch.commit();
}
)
);
})
];
return await Promise.all(input);
};
How can I recursively update the notifications collection but first limit the query to 1.000 documents (until there are not more documents) and then batch.update them? I'm afraid this query will timeout since collection could grow big over time.
Posting a solution I worked out, not following the context of the question though but it can easily be combined. Hope it helps someone else.
import * as admin from "firebase-admin";
const onResults = async (
query: admin.firestore.Query,
action: (batch: number, docs: admin.firestore.QueryDocumentSnapshot[]) => Promise<void>
) => {
let batch = 0;
const recursion = async (start?: admin.firestore.DocumentSnapshot) => {
const { docs, empty } = await (start == null
? query.get()
: query.startAfter(start).get());
if (empty) {
return;
}
batch++;
await action(
batch,
docs.filter((d) => d.exists)
).catch((e) => console.error(e));
await recursion(docs[docs.length - 1]);
};
await recursion();
};
const getMessages = async () => {
const query = admin
.firestore()
.collection("messages")
.where("createdAt", ">", new Date("2020-05-04T00:00:00Z"))
.limit(200);
const messages: FirebaseFirestore.DocumentData[] = [];
await onResults(query, async (batch, docs) => {
console.log(`Getting Message: ${batch * 200}`);
docs.forEach((doc) => {
messages.push(doc.data());
});
});
return messages;
};

How to test a function with Jest that processes data from an external variable

I want to test a function that returns a user by ID from a list of users!!
There is a file responsible for working with the list of users:
const users = [];
const getUser = (id) => users.find((user) => user.id == id);
module.exports = { users, addUser, removeUser, getUser, getUsers };
Unfortunately, I did not find a solution on how to test this function. Expected result is undefined, because the users array is empty. I do not understand how I can replace an array of users for testing.
const { getUser } = require('../users');
describe('Socket', () => {
let socketId;
beforeEach(() => {
socketId = 'qwertyqwerty';
})
test('getUser', () => {
const user = getUser(socketId);
expect(user).toEqual({id: 'qwertyqwerty',user:{username: 'Max'}});
});
})
Conjured a decision!!! in short. used a babel-plugin-rewire. And here's how to implemen:
users.js
import Helper from './Helper';
const users = [];
const user = {
getUser: (id) => users.find((user) => user.id == id),
}
module.exports = user;
And test file:
const User = require('../users');
User.__Rewire__('users', [{id:'qwertyqwerty',user:{username: 'Max'}},{id:'asdfghasdfgh',user:{username: 'Andy'}}]);
describe('Socket', () => {
let socketId;
beforeEach(() => {
socketId = 'qwertyqwerty';
})
test('getUser is user error', () => {
const user = User.getUser(socketId);
expect(user).toEqual({id: 'qwertyqwerty',user:{username: 'Max'}});
});
})
Thanks to Always Learning, for the quick and correct answer )))

Copy a document into a new collection in firestore using cloud function

Path 1 - Match_Creator/cricket/matchList;
Path 2 - Match_Creator/cricket/completedMatchList;
I have a collection called matchList (Path 1) In which i am having a doc called c434108.
Now I want to move this doc(c434108) to Path 2;
/* eslint-disable promise/catch-or-return */
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const { db } = require("./db/index");
const createCompletedMatchListDoc = (request, response) => {
completedMatchDocsData();
};
function completedMatchDocsData() {
createNewCompletedMatchDocs()
}
function getOldCompletedMatchDocs(){
var completedMatchesRef = db
.collection("Match_Creator")
.doc("cricket")
.collection("matchList");
var completedMatchDocData;
var completedMatchDataArr = [];
return new Promise(resolve => {
let query = completedMatchesRef
.where("status", "==", "live")
.get()
.then(snapshot => {
// eslint-disable-next-line promise/always-return
if (snapshot.empty) {
console.log("No matching documents.");
return;
}
snapshot.forEach(doc => {
completedMatchDocData = doc.data();
completedMatchDataArr.push(completedMatchDocData);
resolve(completedMatchDataArr);
});
console.log("sarang", completedMatchDataArr[2]);
})
.catch(err => {
console.log("Error getting documents", err);
});
});
}
const createNewCompletedMatchDocs = (async(change, context) => {
let completedMatchData = await getOldCompletedMatchDocs();
console.log('aman', completedMatchData[1]);
const newValue = change.after.data();
const previousValue = change.before.data();
const st1 =newValue.status;
const st2 = previousValue.status;
console.log('I am a log entry' + st1 + ' ' + st2);
var data = completedMatchData[0];
return db.collection('Match_Creator').doc('cricket').collection('completedMatchList').add(data)
.catch(error => {
console.log('Error writting document: ' + error);
return false;
});
})
module.exports = createCompletedMatchListDoc;
And After copy this doc(c434108) i want to delete this doc(c434108) from path 1.
And My index.js file is:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const storeMatchData = require("./liveScoring");
const createCompletedMatchListDoc = require("./completedMatchList");
var http = require("https");
module.exports = {
liveScoring: functions.https.onRequest(storeMatchData),
createCompletedMatchListDoc: functions.https.onRequest(
createCompletedMatchListDoc
)
};
I am able to solve my problem.
This is my completeMatchList.js file
/* eslint-disable promise/catch-or-return */
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const { db } = require("./db/index");
const createCompletedMatchListDoc = (request, response) => {
completedMatchDocsData();
};
function completedMatchDocsData() {
setNewCompletedMatchDocs()
}
function getOldCompletedMatchDocs(){
var completedMatchesRef = db
.collection("Match_Creator")
.doc("cricket")
.collection("matchList");
var completedMatchDocData;
var completedMatchDataArr = [];
return new Promise(resolve => {
let query = completedMatchesRef
.where("status", "==", "live")
.get()
.then(snapshot => {
// eslint-disable-next-line promise/always-return
if (snapshot.empty) {
console.log("No matching documents.");
return;
}
snapshot.forEach(doc => {
// completedMatchDocData = doc.data();
completedMatchDocData = {
docId: "",
docData: ""
}
completedMatchDocData.docId = doc.id;
completedMatchDocData.docData = doc.data();
completedMatchDataArr.push(completedMatchDocData);
resolve(completedMatchDataArr); // Here i am getting the data and pushing it in array
});
console.log("sarang", completedMatchDataArr);
})
.catch(err => {
console.log("Error getting documents", err);
});
});
}
const setNewCompletedMatchDocs = (async () => {
let getCompletedMatchData = await getOldCompletedMatchDocs();
// console.log("balram", getCompletedMatchData[0].docId);
let newCompletedMatchDocRef = db.collection("Match_Creator").doc("cricket").collection("completedMatchList").doc(getCompletedMatchData[0].docId);
return newCompletedMatchDocRef.set(getCompletedMatchData[0].docData); //set/copy the data to new path.
})
This is my main index.js file
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const storeMatchData = require("./liveScoring");
const createCompletedMatchListDoc = require("./completedMatchList");
const { db } = require("./db/index");
var http = require("https");
module.exports = {
liveScoring: functions.https.onRequest(storeMatchData),
createCompletedMatchListDoc: functions.https.onRequest(
createCompletedMatchListDoc
)
};
Now after copy document data to a new path i will delete the previous document. For deleting the document i have not written the function.
I'm not seeing anything that would allow you to move a document between collections(someone correct me if I'm wrong). You have to copy from the old collection to the new one and then remove the old one.
This is another post on StackOverflow that is running into this same issue and someone provided Java code on how to implement it.
EDIT: Updated link.
Hope this helps.

Categories