Firestore JS - incrementing within transactions - javascript

I have some code that looks like the following:
export const createTable = async (data) => {
const doc = db.collection("tables").doc();
const ref = db
.collection("tables")
.where("userId", "==", data.userId)
.orderBy("number", "desc").limit(1);
db.runTransaction(async transaction => {
const query = await transaction.get(ref);
let number = 1;
if (!query.empty) {
const snapshot = query.docs[0];
const data = snapshot.data();
const id = snapshot.id;
number = data.number + 1;
}
data = {number, ...data};
transaction.set(doc, data);
});
Basically I have a tables collection and each table has an auto generated number like #1, #2, #3
When creating new tables, I want to fetch the latest table number and create the new table with that number incremented by 1.
I wanted to wrap it in a transaction so that if a table created while running the transaction, it will restart so that I don't end up with duplicate numbers.
However, this errors out on the .get(), and from googling I've read that Firestore can't monitor a whole collection within transactions, but instead it requires a specific doc passed to it. Which I obviously can't do because I need to monitor for new docs created in that collection, not changes in a particular doc.
If so, what's the correct way to implement this?

The Firestore transaction API for client apps requires that you get() each individual document that you want to participate in the transaction. So, if you have a query whose results you want to transact with, you will need to:
Perform the query (outside of the transaction)
Collect document references for each document in the result set
In the transaction, get() them all individually.
You will be limited to 500 documents per transaction.
If you want to dynamically look for new documents to modify, you will probably much better off implementing that on the backend using a Firestore trigger in Cloud Functions to automatically handle each new document as they are created, without requiring any code on the client.

Because you're updating just one document, you probably don't need to use transactions for incrementing values.
You can use Firestore Increment to achieve this.
Here is an example taken from here:
const db = firebase.firestore();
const increment = firebase.firestore.FieldValue.increment(1);
// Document reference
const storyRef = db.collection('stories').doc('hello-world');
// Update read count
storyRef.update({ reads: increment });
This is the easiest way to increment values in Firestore.

Related

Firestore transaction sequential creation of collections and updating of documents

Enviroment: nodejs, firebase-admin, firestore.
Database scructure (space):
Database scructure (user):
Creating new space (example):
// init data
const userId = "someUserId";
// Create new space
const spaceRef = await db.collection("spaces").add({ name: "SomeName" });
// Get spaceId
spaceId = spaceRef.id;
// Get user Doc for upate their spaces
const userRef = await db.collection("users").doc(userId);
// Add "spaceId" to user spaces list
userRef.collection("spaces").doc(spaceId).set({ some: "data" });
// Create collection "members" in new space with "userId"
spaceRef.collection("members").doc(userId).set({role: "OWNER"})
Question: I want to execute this code inside single runTransaction, but as I see transactions support only one-time read and multiple update, this does not suit me, since I get the spaceId I need during the execution of the code.
Why I want to use transaction: In my data structure, the relationship between the created space and the presence of the ID of this space on the user is required. If we assume that an error occurred during the execution of this code, for example, the space was created, but this space was not added inside the user profile, then this will be a fatal problem in my database structure.
Similar to other databases, transactions solve this problem, but I can't figure out how to do it with firestore.
Maybe you know a better way to protect yourself from consistent data in this case?
Actually, you don't need a Transaction for that, since you are not reading documents.
With db.collection("users").doc(userId); you are actually not reading a document, just calling "locally" the doc() method to create a DocumentReference. This method is not asynchronous, therefore you don't need to use await. To read the doc, you would use the asynchronous get() method.
So, using a batched write, which atomically commits all pending write operations to the database, will do the trick:
const userId = 'someUserId';
const userRef = db.collection('users').doc(userId);
const spaceRef = firestore.collection('spaces').doc();
const spaceId = spaceRef.id;
const writeBatch = firestore.batch();
writeBatch.set(spaceRef, { name: "SomeName" });
writeBatch.set(userRef.collection("spaces").doc(spaceId), { some: "data" });
writeBatch.set(spaceRef.collection("members").doc(userId), {role: "OWNER"});
await writeBatch.commit();
You should include this code in a try/catch block and if the batch commit fails you will be able to handle this situation in the catch block, knowing that none of the write were committed.

How to go through dynamic Firebase Realtime Database?

I am making a web app that accesses a Firebase Real-Time Database. The database will be of a dynamic size as new data is entered.
example:
Each element holds an irrelevant object.
I've hard-coded the size and am for-looping through it (js):
for (let i = 0; i < DB_SIZE; i++) {
let ref = firebase.database().ref(i.toString());
let snapshot = await ref.once('value');
let data = snapshot.val();
}
The data achieved is the object associated with the element.
This works when I know the exact, unchanging size of the database, but when a new object is added as a new element in the database, my loop wouldn't access it, since the database size is now larger.
How can I access the data in this dynamic database using Firebase's syntax? It's possible that try...catch will be involved, but I'm not so sure.
The Firebase DataSnapshot has a built-in forEach method precisely for this purpose. So you can load the entire parent node, and then loop over it client-side with:
const snapshot = await firebase.database().ref().once('value');
snapshot.forEach((child) => {
let data = child.val();
});
By the way, Firebase recommends against using sequential, numeric indexes as the keys. For an explanation why, see Best Practices: Arrays in Firebase.

Currently refactoring my Firestore post data function into an array storage

Currently I have a dashboard application and when a user adds to their dashboard it does a post request to Firestore to set the data in a certain document. Here is what that code looks like:
export function createEntry(data) { // Data has a authorId key with the value of the UID
const userEntry = firestore.collection('userEntries').doc();
userEntry.set(data);
return userEntry;
}
So above, a new entry gets created with a random ID that I then use to grab the data to display later. To get this information later, I loop through all of the entries to see where the authorId matches the user that is log in to return that.
So what I am trying to do is refactor the code so that each document in 'userEntries' is the ID of the user, authorID: UID, and have it be an array where I can keep pushing items into. In return, I can just grab the user array and map over those items to display instead of looping through all my entries and checking.
What I've tried:
export function createEntry(data) {
const userEntry = firestore.collection('userEntries').doc(data.authorId);
userEntry.update({
userData: firebase.firestore.FieldValue.arrayUnion(data)
})
}
So to sum it up, I would like a way to set the document to users ID and push any future entries into their own array, in which I could easily access from the frontend to display.
Hope you can point me in the right direction, thank you!
You don't actually have to revise the original method. If you have the authorId as a field value, you can just query the data using where clause like this:
const uid = auth.currentUser.uid;
firestore.collection().where('authorId', '==', uid).get();
With this method, you are not getting the entire collection and looping through to see which one is written by the user.

Is there a way to update a collectionGroup in cloud function

I'm building a chat app. When a user makes an update on their local profile I'd like to use cloud functions to make that update across a collectionGroup.
I'm successfully listening to the update in cloud functions and retrieving a list of collectionGroups with the following:
const collectionGroupNameref = await db.collectionGroup('collectionGroupName').where('userId', '==', data.uid).get();
collectionGroupNameref.forEach(async (val: any) => {
const connectionsRef = await db.collection('collectionGroupName').doc(val.id).get();
});
But now I need to update a field within that collectionGroup and that's where I'm running into issues.
The collectionGroup is stored in 2 locations:
users{id}collectionGroupName{id}
groups{id}collectionGroupName{id}
Is it possible to update all of the documents in that collectionGroup
Firestore doesn't provide any methods to update an entire collection or collection group like "UPDATE WHERE" in SQL. What you are required to do instead is write each document individually. So, if you've already executed a query for documents in the collection group, can you simply iterate the documents in the result set and update each document as needed. You can use the ref property of DocumentSnapshot to easily update each document, no matter what collection contains it.
const querySnapshot = await db
.collectionGroup('collectionGroupName')
.where('userId', '==', 'data.uid')
.get();
querySnapshot.docs.forEach(snapshot => {
snapshot.ref.update(...)
})

How do a query a number from documents and sum it all up into a parent document in firestore using firebase functions

I have a collection of documents and i have a function that counts each document and saves the count into the parent document and is updated everytime a new document is created. I wanted to query that field and add it to its parent document to sum it all up. I need help coming up with a code that queries the all the fields from different documents and sum it all in the parent document
Here is my code that counts each document and saves it as a document field
const yearId = context.params.yearId;
const daysId = context.params.daysId;
const monthId = context.params.monthId;
const ticketsId = context.params.ticketsId;
// ref to the parent document
//const docRef = admin.firestore().collection('TrafficViolations').doc(TrafficViolationsId)
const docRef = admin.firestore().collection('stats').doc(yearId).collection('months').doc(monthId).collection('days').doc(daysId)
// get all comments and aggregate
return docRef.collection('tickets')
.get()
.then(querySnapshot => {
// get the total comment count
const tickets = querySnapshot.size
// data to update on the document
const data = { tickets }
// run update
return docRef.update(data)
})
.catch(err => console.log(err) )
Structure of my firestore
As far as accessing the docs on the query snapshot goes, this answer shows a good way to do that.
In terms of efficiency, instead of looping through and summing each value, you can have a function watch the tickets documents for change.
OnCreate -> might do nothing
OnEdit -> diff the old and new counts get the parent day document, and update the total with the diff in a transaction.
OnDelete -> Similar to OnEdit, just remove the value from the parent day document's total.
You probably are aware but you can use path IDs in the functions to get the required IDs for the parent day doc update.

Categories