I'm creating an expense tracker app where:
(Task completed) The user can set a monthly budget (ie. $3,000) that is stored in a '--currentBudget--' document in a year-month collection.
(Task completed) The user can add an expense through a form (title, amount, category, date) which is stored in a unique document in the year-month collection, the data is then aggregated in the '--totalSpent--' by total spending and categorized total spending.
(Not completed) The user can delete an expense that would update the values in the '--totalSpent--'. How would I do this using Cloud Firestore? I'm thinking I could reference the document I'm deleting and decrement by the amount value to its respected year-month(date) record based on total spending and categorized total spending.
Firestore Console
https://i.stack.imgur.com/eKTUg.jpg
/* ADD EXPENSE */
function addExpense(user) {
// DOM Form Selectors
const expenseForm = document.getElementById('expense-form');
const expenseTitle = document.getElementById('expense-title');
const expenseAmount = document.getElementById('expense-amount');
const expenseCategory = document.getElementById('expense-category');
const expenseDate = document.getElementById('expense-date');
// Submit Event
expenseForm.addEventListener('submit', e => {
e.preventDefault();
// Database Reference
const increment = firebase.firestore.FieldValue.increment(expenseAmount.value);
const expenseRef = db.collection('users').doc(user.uid).collection(expenseDate.value).doc();
const totalSpent = db.collection('users').doc(user.uid).collection(expenseDate.value).doc('--totalSpent--');
// Database Batch Write
const batch = db.batch();
// Creates Unqiue Expense Document
batch.set(expenseRef, { title: expenseTitle.value, amount: expenseAmount.value, category: expenseCategory.value, date: expenseDate.value });
// Updates totalSpending
batch.set(totalSpent, { totalSpending: increment }, { merge: true });
//Updates totalSpending by category
batch.set(totalSpent, { [expenseCategory.value]: increment }, { merge: true });
batch.commit();
// Reset Form
expenseForm.reset();
modal[1].classList.add('hidden');
overlay.classList.add('hidden');
});
}
That is a perfect fit for a Firebase Cloud Functions solution.
You can use the onCreate and onDelete event triggers to change the total value. With the usage of transactions you ensure a safe execution.
If the users can even change the values in existing expenses you could also use the onUpdate trigger to handle even those use cases.
Here is a basic example for your database and for the onCreate and onDelete triggers:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
exports = module.exports = functions.firestore
.document("/users/{userUid}/{year_month}/{expenseUid}")
.onCreate(async (eventSnapshot, context) => {
const { userUid, expenseUid } = context.params;
const expenseData = eventSnapshot.data();
const { spending = 0 } = expenseData;
try {
await admin.firestore().runTransaction(async (t) => {
const totalRef = admin
.firestore()
.doc("/users/{userUid}/{year_month}/--totalSpend--");
const doc = await t.get(totalRef);
const newTotalSpending = doc.data().totalSpending || 0 + spending;
t.update(totalRef, { totalSpending: newTotalSpending });
});
console.log("Transaction success!");
} catch (e) {
console.log("Transaction failure:", e);
}
});
exports = module.exports = functions.firestore
.document("/users/{userUid}/{year_month}/{expenseUid}")
.onDelete(async (eventSnapshot, context) => {
const { userUid, expenseUid } = context.params;
const expenseData = eventSnapshot.data();
const { spending = 0 } = expenseData;
try {
await admin.firestore().runTransaction(async (t) => {
const totalRef = admin
.firestore()
.doc("/users/{userUid}/{year_month}/--totalSpend--");
const doc = await t.get(totalRef);
const newTotalSpending = doc.data().totalSpending || 0 - spending;
t.update(totalRef, { totalSpending: newTotalSpending });
});
console.log("Transaction success!");
} catch (e) {
console.log("Transaction failure:", e);
}
});
The main benefit of using the cloud functions is that you don't need to give your users the right to edit the total value and all changes are done automaticaly in teh backend.
Related
I have action in vuex. This action do request to firebase.
I would like to receive record from firebase by 'id'.
But I don't know where I must input this 'id' for searching in database.
I did it here:
const snapshot = await get(child(dbRef, `/users/${uid}/records`), id);
but I receive all records, without filtration by 'id'.
async fetchRecordById({ dispatch, commit }, id) {
try {
let record
const uid = await dispatch('getUid')
const dbRef = ref(getDatabase());
const snapshot = await get(child(dbRef, `/users/${uid}/records`), id);
record = snapshot.val()
if (record === null) {
record = {}
}
return {...record, id}
} catch (e) {
commit('setError', e)
throw e
}
},
For version 8 firebase it looks like:
const record = (await firebase.database().ref(`/users/${uid}/records`).child(id).once('value')).val() || {}
How to integrate in version 9?
The get() function only takes one DatabaseReference as a parameter but you are also passing the id.
Instead, you can specify the path directly in ref() function as shown below:
console.log(uid, id) // <-- check if values are correct
const dbRef = ref(getDatabase(), `/users/${uid}/records/${id}`)
const snapshot = await get(dbRef);
console.log(snapshot.val())
I'm trying to add new fields in a document that related to the logged in user (same UID)
to create a new document with the same UID as the user's UID I use the following:
const collectionRef = collection(db, 'data');
// submit function to add data
const handleSubmit = async (e) => {
e.preventDefault();
onAuthStateChanged(auth, async (user) => {
const uid = user.uid;
try {
// overrides the data
await setDoc(doc(collectionRef, uid), {
food: food,
quantity: quantity,
})
alert("Data added successfully")
} catch (error) {
alert(`Data was not added, ${error.message}`)
}
}, [])
}
For add more fields to the array, I use the following:
const collectionRef = collection(db, 'data');
// submit function to add data
const handleSubmit = async (e) => {
e.preventDefault();
onAuthStateChanged(auth, async (user) => {
const uid = user.uid;
try {
await updateDoc(doc(collectionRef, uid), {
food: arrayUnion(food),
quantity: arrayUnion(quantity),
})
alert("Data added successfully")
} catch (error) {
alert(`Data was now added, ${error.message}`)
}
}, [])
}
the complete code:
const collectionRef = collection(db, 'data');
const handleSubmit = async (e) => {
e.preventDefault();
onAuthStateChanged(auth, async (user) => {
const uid = user.uid;
try {
// overrides the data - ???
await setDoc(doc(collectionRef, uid), {
food: food,
quantity: quantity,
})
// add more fields to the array
await updateDoc(doc(collectionRef, uid), {
food: arrayUnion(food),
quantity: arrayUnion(quantity),
})
alert("Data added successfully")
} catch (error) {
alert(`Data was now added, ${error.message}`)
}
}, [])
}
I can add more fields with the updateDoc() function, but if I will not comment or delete the setDoc() function after creating the new document, it will override the fields inside the document.
screenshot of the DB (Firestore):
I can add more fields only if I comment the setDoc() function after the doc first created
Firestore
If you want to create-or-update the document, you can use { merge: true }:
await setDoc(doc(collectionRef, uid), {
food: food,
quantity: quantity,
}, { merge: true }) // 👈
Also see the second code snippet in the documentation on setting a documents.
The onAuthStateChanged() observer runs every time the user's auth state changes and then the set() will overwrite the existing document removing any existing data. You can try using merge option that won't delete rest of the fields as shown below:
const docRef = doc(db, 'users', user.uid)
await setDoc(docRef, { food, quantity }, { merge: true })
This will still however overwrite the 'food' and 'quantity' arrays with the new value that you provide. So it might be best to use setDoc() only once after users registers in your application and use updateDoc thereafter.
Alternatively, you can also unsubscribe from the listener as shown below:
const handleSubmit = async (e) => {
e.preventDefault();
const unsub = onAuthStateChanged(auth, async (user) => {
// process data
unsub();
})
}
The handler might still trigger again if auth state changes before your processing is complete. You should use onAuthStateChanged() when the web app loads for the first time and then redirect user to different pages based on their auth state. Once you confirm that user is logged in then you can simply use the following to get current user:
const user = auth.currentUser;
if (user) {
// update document.
}
I am getting the following error:
TypeError: Cannot read properties of undefined (reading 'set').
I am trying to update the endTime in the script and i want to do it for each user then calculate the total amount of hours worked and update it in a field called 'totalTime'.
const {
initializeApp,
applicationDefault,
cert,
} = require("firebase-admin/app");
const {
getFirestore,
Timestamp,
FieldValue,
} = require("firebase-admin/firestore");
const firebaseConfig = {
xxxxxx
};
const serviceAccount = require("./fitness-69af3-firebase-adminsdk-c3hmg-c7fda98049.json");
initializeApp({
credential: cert(serviceAccount),
});
const db = getFirestore();
const age = "30";
async function main() {
const fitnessRef = db.collection("/food");
const snapshot = await fitnessRef
.where("role", "==", "junior")
.where("age", "==", age)
.get();
if (snapshot.empty) {
console.log("No matching documents.");
}
snapshot.forEach((doc) => {
let doc1 = doc.data();
let schedules = doc1.userInfo.schedule;
console.log(schedules);
const res = schedules.ref.set(
{
endTime: "17:30",
startTime: "09:00",
// totalTime: "{endTime - startTime}"
}, { merge: true }
);
});
}
main();
The ref property is a DocumentReference, and thus only exists on the full document, not on individual fields in there. So you will have to call doc.ref.update(...), with the field(s) you want to update.
Moreover, you can't update an individual item in an array in Firestore. You will have to read the entire array from the document, update the item in that array, and then write the entire array back to the database.
I have these collection of items from firestore:
availability : true
stocks: 100
item: item1
I kind of wanted to decrement the stocks after submitting the form: I have these where() to compare if what the user chose is the same item from the one saved in the firestore.
function incrementCounter(collref) {
collref = firestore
.collection("items")
.doc()
.where(selectedItem, "==", selectedItem);
collref.update({
stocks: firestore.FieldValue.increment(-1),
});
}
This is how I'll submit my form and I've set the incrementCounter() after saving it:
const handleSubmit = (e) => {
e.preventDefault();
try {
const userRef = firestore.collection("users").doc(id);
const ref = userRef.set(
{
....
},
},
{ merge: true }
);
console.log(" saved");
incrementCounter();
} catch (err) {
console.log(err);
}
};
There's no error in submitting the form. However, the incrementCounter() is not working and displays this error:
TypeError: _Firebase_utils__WEBPACK_IMPORTED_MODULE_5__.firestore.collection(...).doc(...).where is not a function
There are few problems here
There should be async-await for both functions
Your fieldValue should start from firebase.firestore.FieldValue not firestoreFieldValue
Also where clause is used for collection, not doc() so remove that as well. Also I don't think this will update the full collection but do check it and see. (The error you are getting is because of this)
I don't know how you are importing firebase in this application and I don't know how you have declared firestore but mostly firestore variable is declared like this
const firestore = firebase.firestore();
In here firestore is a function, not a property
But when you are using it in the FieldValue then it should be like
firebase.firestore.FieldValue.increment(-1),
Notice that firestore here is a property not a function
Your full code should be like this
async function incrementCounter(collref) {
collref = firestore
.collection("items")
.where(selectedItem, "==", selectedItem);
const newRef = await collref.get();
for(let i in newRef.docs){
const doc = newRef.docs[i];
await doc.update({
stocks: firebase.firestore.FieldValue.increment(-1),
});
// You can also batch this
}
}
const handleSubmit = async (e) => {
e.preventDefault();
try {
const userRef = firestore.collection("users").doc(id);
const ref = await userRef.set(
{
....
},
},
{ merge: true }
);
console.log(" saved");
await incrementCounter();
} catch (err) {
console.log(err);
}
};
The where() method exists on a CollectionReference and not a DocumentReference. You also need to get references to those documents first so first get all the matching documents and then update all of them using Promise.all() or Batch Writes:
function incrementCounter() {
// not param required ^^
const collref = firestore
.collection("items")
// .doc() <-- remove this
.where(selectedItem, "==", selectedItem);
// ^^^ ^^^
// doc field field value
// "item" {selectedItemName}
collRef.get().then(async (qSnap) => {
const updates = []
qSnap.docs.forEach((doc) => {
updates.push(doc.ref.update({ stocks: firebase.firestore.FieldValue.increment(-1) }))
})
await Promise.all(updates)
})
}
If you are updating less than 500 documents, consider using batch writes to make sure all updates either fail or pass:
collRef.get().then(async (qSnap) => {
const batch = firestore.batch()
qSnap.docs.forEach((doc) => {
batch.update(doc.ref, { stocks: firebase.firestore.FieldValue.increment(-1) })
})
await batch.commit()
})
You can read more about batch writes in the documentation
am working on a pagination using Firebase and so far i have a button to go forward and other one to get back and they are working fine ,but i have problem detecting either if am in the first page or the last page so i can disable the pagination buttons,so am wondering how that work and should i change the way i paginate data?
export const getNextItems = (last_Visible) => {
return async (dispatch, getState, { getFirebase }) => {
const firestore = getFirebase().firestore();
// const items = [];
const dbRef = firestore
.collection('items')
.orderBy('createdAt', 'desc')
.startAfter(last_Visible)
.limit(2);
const usersRef = firestore.collection('users');
let temps = [];
const { data: items, firstVisible, lastVisible } = await dbRef.get().then(getAllDocs);
for (const item of items) {
const { data: user } = await usersRef.doc(item.owner).get().then(getDoc);
temps.push({ ...item, owner: user });
}
return { docs: temps, lastVisible, firstVisible };
};
};
export const getPrevItems = (first_Visible) => {
return async (dispatch, getState, { getFirebase }) => {
const firestore = getFirebase().firestore();
// const items = [];
const dbRef = firestore
.collection('items')
.orderBy('createdAt', 'desc')
.endBefore(first_Visible)
.limitToLast(2);
const usersRef = firestore.collection('users');
let temps = [];
const { data: items, lastVisible, firstVisible } = await dbRef.get().then(getAllDocs);
for (const item of items) {
const { data: user } = await usersRef.doc(item.owner).get().then(getDoc);
temps.push({ ...item, owner: user });
}
return { docs: temps, lastVisible, firstVisible };
};
};
To detect whether there are more pages to load, you'll need to request an additional item. So since you seem to show 2 items per page, you should request 3 items - and display only two of them. If you get a third item, you know there's an additional page.