Can I loop through documents within a Firestore Transaction? [closed] - firebase

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 3 years ago.
Improve this question
I am trying to run multiple document updates within one firestore transaction and I am wondering whether or not this is an anti pattern.
I have a document named "Group" containing an array named "members" which contains different IDs from a users collection. Now I want to loop through all members and update the according user documents within one transaction. Is this possible?
I have tried to loop through all members with .forEach() but the problem is that .forEach() does not support async/await or the use of promises as far as I know.

The following should do the trick:
var firestore = firebase.firestore();
//.....
var groupDocRef = firestore.doc('collectionRef/docRef');
return firestore
.runTransaction(function(transaction) {
var arrayOfMemberIds;
return transaction
.get(groupDocRef)
.then(function(groupDoc) {
if (!groupDoc.exists) {
throw 'Group document does not exist!';
}
arrayOfMemberIds = groupDoc.data().members;
return transaction.update(groupDocRef, {
lastUpdate: firebase.firestore.FieldValue.serverTimestamp()
});
})
.then(function() {
arrayOfMemberIds.forEach(function(memberId) {
transaction = transaction.update(
firestore.collection('users').doc(memberId),
{ foo: 'bar' }
);
});
return transaction;
});
});
This will work because the update() method returns the Transaction instance which can be used for chaining method calls.
Note also that we must update the initial groupDoc. If not, the following error will be thrown: FirebaseError: "Every document read in a transaction must also be written.". In the example above we just update a lastUpdate field with a Timestamp. It's up to you to choose the update you want to do!
You can easily test the transactional aspect of this code by setting some security rules as follows:
service cloud.firestore {
match /databases/{database}/documents {
match /collectionRef/{doc} {
allow read: if true;
}
match /users/{user} {
allow read: if false;
}
}
}
Since it is not possible to write to the users collection, the Transaction will fail and the collectionRef/docRef document will NOT be updated.
Another (even better) way to test the transactional aspect is to delete one of the users document. Since the update() method fails if applied to a document that does not exist, the entire Transaction will fail.

Related

How to only allow a unique document ID to be upload to Firebase Firestore using Rules [duplicate]

I want to create Firestore documents if they don't exist - if they do exist, skip them (don't update).
Here's the flow
var arrayOfRandomIds = [array of 500 random numbers];
for (var id of arrayOfRandomIds)
{
var ref = db.collection("tickets").doc(id);
batch.set(ref, {name: "My name", location: "Somewhere"}, { merge: true });
}
batch.commit();
I just want to know, would this overwrite any existing documents if they exist? I don't want anything overwritten, just skipped.
Thanks.
I think you can use security rules to accomplish that. That way you won't be charged for an additional document read to see if it already exists.
service cloud.firestore {
match /databases/{database}/documents {
match /tickets/{id} {
allow create;
}
}
}
Meanwhile there is a "create but don't overwrite" function.
Assuming you are using JavaScript here is the reference: https://googleapis.dev/nodejs/firestore/latest/DocumentReference.html#create
Here is the corresponding example code from the docs:
let documentRef = firestore.collection('col').doc();
documentRef.create({foo: 'bar'}).then((res) => {
console.log(`Document created at ${res.updateTime}`);
}).catch((err) => {
console.log(`Failed to create document: ${err}`);
});
Using .create() instead of .set() should do the trick for you without relying on security rules for application logic.
Firestore doesn't have a native "create but don't overwrite" operation. Here are the only available operations:
update: only change the contents of an existing document
set without merge: create or overwrite
set with merge: create or update if exists
Instead of a batch, what you can do instead is perform a transaction that checks to see if the document exists, then creates it conditionally if it does not already exist. You will have to write that logic inside your transaction handler.
I want to create Firestore documents if they don't exist - if they do exist, skip them (don't update).
In that case, you should check if a particular document actually exists in a collection, right before the write operation takes place. If it does not exist, create it, otherwise take no action.
So you should simply use set() function, without passing merge: true.

Firestore create documents if they don't exist, skip if they do

I want to create Firestore documents if they don't exist - if they do exist, skip them (don't update).
Here's the flow
var arrayOfRandomIds = [array of 500 random numbers];
for (var id of arrayOfRandomIds)
{
var ref = db.collection("tickets").doc(id);
batch.set(ref, {name: "My name", location: "Somewhere"}, { merge: true });
}
batch.commit();
I just want to know, would this overwrite any existing documents if they exist? I don't want anything overwritten, just skipped.
Thanks.
I think you can use security rules to accomplish that. That way you won't be charged for an additional document read to see if it already exists.
service cloud.firestore {
match /databases/{database}/documents {
match /tickets/{id} {
allow create;
}
}
}
Meanwhile there is a "create but don't overwrite" function.
Assuming you are using JavaScript here is the reference: https://googleapis.dev/nodejs/firestore/latest/DocumentReference.html#create
Here is the corresponding example code from the docs:
let documentRef = firestore.collection('col').doc();
documentRef.create({foo: 'bar'}).then((res) => {
console.log(`Document created at ${res.updateTime}`);
}).catch((err) => {
console.log(`Failed to create document: ${err}`);
});
Using .create() instead of .set() should do the trick for you without relying on security rules for application logic.
Firestore doesn't have a native "create but don't overwrite" operation. Here are the only available operations:
update: only change the contents of an existing document
set without merge: create or overwrite
set with merge: create or update if exists
Instead of a batch, what you can do instead is perform a transaction that checks to see if the document exists, then creates it conditionally if it does not already exist. You will have to write that logic inside your transaction handler.
I want to create Firestore documents if they don't exist - if they do exist, skip them (don't update).
In that case, you should check if a particular document actually exists in a collection, right before the write operation takes place. If it does not exist, create it, otherwise take no action.
So you should simply use set() function, without passing merge: true.

What happens if i delete a document and dont delete its subcollections in firestore? [closed]

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 3 years ago.
Improve this question
I have a flutter app that basically acts as a filter app for another app, meaning i can scroll through certain posts and decide whether to delete them from this app so they dont show up on the other main app.
My question is, since Firestore does not support deleting subcollections, what happens if i just delete the document of the post and ignore the remaining subcollections of things like comments? Is it possible that firestore will later assign a random postId with the same as the one previously deleted and end up showing a deleted post's comments and subcollection info? Because it says on firestore that the ancestor document that doesnt exist for a subcollection that does will not show up in queries does that mean that no other post will be created with the same postId?
Basically Is there any harm for not deleting the subcollections, if there is what do you recommend i do about it, manually delete it?
You can use firebase functions to delete collections when a document is deleted. In other words, you would code a function that executes every time a document (in your case post) is deleted. Then you would go through your subcollections and delete them in the function.
To delete a collection I use this code (I didn't code this):
/**
* Delete a collection, in batches of batchSize. Note that this does
* not recursively delete subcollections of documents in the collection
*/
function deleteCollection (db, collectionRef, batchSize) {
var query = collectionRef.orderBy('__name__').limit(batchSize)
return new Promise(function (resolve, reject) {
deleteQueryBatch(db, query, batchSize, resolve, reject)
})
}
function deleteQueryBatch (db, query, batchSize, resolve, reject) {
query.get()
.then((snapshot) => {
// When there are no documents left, we are done
if (snapshot.size === 0) {
return 0
}
// Delete documents in a batch
var batch = db.batch()
snapshot.docs.forEach(function (doc) {
batch.delete(doc.ref)
})
return batch.commit().then(function () {
return snapshot.size
})
}).then(function (numDeleted) {
if (numDeleted <= batchSize) {
resolve()
return
}
else {
// Recurse on the next process tick, to avoid
// exploding the stack.
return process.nextTick(function () {
deleteQueryBatch(db, query, batchSize, resolve, reject)
})
}
})
.catch(reject)
}
Is it possible that firestore will later assign a random postId with the same as the one previously deleted
The collisions of ids in this case is incredibly unlikely and you can/should assume they'll be completely unique. So you don't have to be concerned about it because that's why those ids were designed for, to be unique.
This built-in generator for unique ids that is used in Firestore when you call CollectionReference's add() methods or CollectionReference's document() method without passing any parameters, generates random and highly unpredictable ids, which prevents hitting certain hotspots in the backend infrastructure.
does that mean that no other post will be created with the same postId?
Yes, not other document will be created with the same id.
Basically Is there any harm for not deleting the subcollections
There is not. You can do it in two ways, client side my getting all documents within that subcollection and deleting them in smaller chunks or using a function as #jonasxd360 mentioned in his answer.

Firestore: remove sensitive fields on documents

I'm trying to figure it out how to remove a sensitive field on a firestore document. For example, my collection is a group information. The group is protected with a pin code field. Any one wants to join the group has to know the pin code.
In the meantime, I want to let users query what group is available to join. For query part, I don't want return group information with pin code information. Do we have anyway to remove sensitive fields from a document for Firestore for reading event?
Cloud function only supports write event. 1 possible solution is use cloud function on write event, and put pin code in a separate document. Is there a better solution? THanks.
My group schema is:
group: {
name: string,
pinCode: string
}
A user can either access a document, or they can't. There is no property-level access control in Firestore.
So to accomplish what you want, you will need to store the public and private information in separate documents.
You could either create a second document with the private information in the same collection and then secure them using:
match /databases/{database}/documents {
match /groups/{group} {
allow read: if resource.data.visibility != "private"
}
}
Alternatively (and simpler to secure) you could create a separate collection for the private documents.
You can create a Firebase Function that returns only the fields that you need (non sensitive), here an example:
exports.getTopUsers = functions.https.onCall(async (data) => {
const users = [];
return db.collection('users').orderBy('bids').limit(data.limit).get()
.then((querySnapshot) => {
querySnapshot.forEach((user) => {
users.push({
diplayName: user.get('displayName'),
});
});
return {
topUsers: users,
};
})
.catch((err) => {
console.error(err);
});
});
So, you need to create a separate array (that will be returned) and filling it with only the field that you want while iterating your Firestore collection.

How to store users and groups for a chat using Firebase

I would like to make a chat using Firebase. I need to display for each users the list of group they belong to and also for each group all the members.
Because of how Firebase is designed, and in order to have acceptable performance, I am think making a list of all the groups containing the list of members, and for each users, an entry with all the group they belong too.
My first question is, is it the right approach?
If so, my second question is how can I atomically add (or removed) a user, i.e. making sure both the user is added in the group and the group added into the user or not added at all (i.e. never stored at 1 location only and make the DB inconsistent) ?
The data model you have proposed is consistent with recommendations for firebase. A more detailed explanation is outlined in best way to structure data in firebase
users/userid/groups
groups/groupid/users
Firebase provides .transaction for atomic operations. You may chain multiple transactions to ensure data consistency. You may also use onComplete callback function. For detailed explanation refer to firebase transaction. I am using this same approach to keep multiple message counts up to date for a dashboard display.
Sample code for nested transaction:
ref.child('users').child(userid).child(groupid).transaction(function (currentData) {
if (currentData === null) {
return groupid;
} else {
console.log('groupid already exists.');
return; // Abort the transaction.
}
}, function (error, committed, snapshot) {
if (committed) {
console.log('start a new transaction');
ref.child('groups').child(groupid).child(userid).tranaction(function (currentData) {
if (currentData === null) {
return userid;
} else {
console.log('Userid already exists.');
//add code here to roll back the groupid added to users
return; // Abort the transaction.
}
}, function (error, committed, snapshot) {
if (error) {
//rollback the groupid that was added to users in previous transaction
}
})
}
});

Resources