exists() function not finding any documents in Firestore Security Rules - javascript

So I have a followers collection and a users collection. Creating a doc in the followers collection with a certain ID requires a doc to exist with the same ID in the users collection.
So, in the security rules, I check for the existence of that document.
match /followers/{followers} {
function loggedInUserMatching() {
return (request.auth != null) && (request.auth.uid == followers);
}
function userExistsAndLoggedIn() {
return loggedInUserMatching() && exists(/databases/$(database)/documents/users/$(request.auth.uid));
}
allow create, delete: if userExistsAndLoggedIn();
}
In my tests, I try to create a doc in followers without the corresponding doc in the users collection. This should fail.
const database = testEnv.authenticatedContext('user1').firestore();
let testFollowersDoc = database.collection("followers").doc("user1");
expect(assertFails(testFollowersDoc.set({followers: []}))).resolves.toBeDefined();
Then, I create the doc in the users collection and then try to create the doc in followers again. This should succeed, but it always fails.
const testUserDoc = database.collection("users").doc("user1");
testUserDoc.set({about: "Wow", following: []});
expect(assertSucceeds(testFollowersDoc.set({followers: []}))).resolves.toBeUndefined();
The create rule for the users collection is correct and the document is actually created. I can access its data and verify it exists. But in my security rules, the exists() function always returns false, so the permission is denied and the document is not created.
What could be the cause of this? Am I using the exists() function incorrectly? Or am I not creating the document properly in my test?
I've been trying to solve this for a long time so any help would be appreciated!

These lines are asynchronous and should have await.
expect(assertFails(await testFollowersDoc.set({followers: []}))).resolves.toBeDefined();
expect(assertSucceeds(await testFollowersDoc.set({followers: []}))).resolves.toBeUndefined();

Related

Best way to batch create if not exists in firestore

I am working with a project where we create a bunch of entries in firestore based on results from an API endpoint we do not control, using a firestore cloud function. The API endpoint returns ids which we use for the document ids, but it does not include any timestamp information. Since we want to include a createdDate in our documents, we are using admin.firestore.Timestamp.now() to set the timestamp of the document.
On subsequent runs of the function, some of the documents will already exist so if we use batch.commit with create, it will fail since some of the documents exist. However, if we use batch.commit with update, we will either not be able to include a timestamp, or the current timestamp will be overwritten. As a final requirement, we do update these documents from a web application and set some properties like a state, so we can't limit the permissions on the documents to disallow update completely.
What would be the best way to achieve this?
I am currently using .create and have removed the batch, but I feel like this is less performant, and I occasionally do get the error Error: 4 DEADLINE_EXCEEDED on the firestore function.
First prize would be a batch that can create or update the documents, but does not edit the createdDate field. I'm also hoping to avoid reading the documents first to save a read, but I'd be happy to add it in if it's the best solution.
Thanks!
Current code is something like this:
const createDocPromise = docRef
.create(newDoc)
.then(() => {
// success, do nothing
})
.catch(err => {
if (
err.details &&
err.details.includes('Document already exists')
) {
// doc already exists, ignore error
} else {
console.error(`Error creating doc`, err);
}
});
This might not be possible with batched writes as set() will overwrite the existing document, update() will update the timestamp and create() will throw an error as you've mentioned. One workaround would be to use create() for each document with Promise.allSettled() that won't run catch() if any of the promise fails.
const results = [] // results from the API
const promises = results.map((r) => db.doc(`col/${r.id}`).create(r));
const newDocs = await Promise.allSettled(promises)
// either "fulfilled" or "rejected"
newDocs.forEach((result) => console.log(result.status))
If any documents exists already, create() will throw an error and status for that should be rejected. This way you won't have to read the document at first place.
Alternatively, you could store all the IDs in a single document or RTDB and filter out duplicates (this should only cost 1 read per invocation) and then add the data.
Since you prefer to keep the batch and you want to avoid reading the documents, a possible solution would be to store the timestamps in a field of type Array. So, you don't overwrite the createdDate field but save all the values corresponding to the different writes.
This way, when you read one of the documents you sort this array and take the oldest value: it is the very first timestamp that was saved and corresponds to the document creation.
This way you don't need any extra writes or extra reads.

How do I check if collection exist in firestore (not document) in JS

Hoi, I would like to check, using React javascript, if a collection in the Firestore already exists, no matter if it's empty or not. I tried:
if (collection(db, ref)) // is always true somehow
Any ideas? Thanks!
You would need to try to fetch from the collection and see if anything is returned:
const snap = await query(collection(db, ref), limit(1));
if (snap.empty) {
// no docs in collection
}
There is no function available in the SDK that can help you can check if a particular collection exists. A collection will start to exist only if it contains at least one document. If a collection doesn't contain any documents, then that collection doesn't exist at all. So that being said, it makes sense to check whether a collection contains or not documents. In code, it should look as simple as:
const snapshot = await query(collection(db, yourRef), limit(1));
if (snapshot.empty) {
//The collection doesn't exist.
}
One thing to mention is that I have used a call to limit(1) because if the collection contains documents, then we limit the results so we can pay only one document read. However, if the collection doesn't exist, there is still one document read that has to be paid. So if the above query yields no resul## Heading ##t, according to the official documentation regarding Firestore pricing, it said that:
Minimum charge for queries
There is a minimum charge of one document read for each query that you perform, even if the query returns no results.
You have to fetch the collection out of the database and check if it has more than 0 documents. Even, if the collection doesn't exist, it will return 0.
const db = firebase.firestore();
db.collection("YOUR COLLECTION NAME").get().then((res) =>{
if(res.size==0){
//Collection does not exist
}else{
//Collection does exist
}

deleting a collection that includes a nested sub collection in firebase?

I have the following batch, where I delete three documents in a collection,
the data-set-c collection however has one nested collection for massages where each message is a doc each.
the problem is that the (data-set-c) collection never gets deleted, I don't know if it is due to the nesting taking place? will deleting this way affect also sub-collection? or is it the rules that are blocking it since my rules are specific to the deepest level endpoint, or should use the cloud function instead since this doc will be massive later on, and each day is massages collection but the main issue here is how to delete this one level nested structure.
could you please take a look and see what I am doing wrong.
// Batch
const batch = writeBatch(db);
// data-set-a/{Id}/sub-set-a/{subSetId} ---> the direct document
const colA = collection(db, data-set-a,_authContext.currentUser.uid, sub-set-a);
// data-set-b/{Id}/sub-set-b/{subSetId} ---> the direct document
const colB = collection(db, data-set-b, _authContext.currentUser.uid, sub-set-b);
// data-set-c/{Id}/sub-set-c/{subSetId}/masseges/{mgsId} /*nested collection*/
const colC = collection(db, 'data-set-c', _authContext.currentUser.uid, sub-set-c);
const docA = doc(colA, subSetId);
const docB = doc(colB, subSetId);
const docC = doc(colC, subSetId);
const docsArr = [docA, docB, docC];
docsArr.forEach((col: any) => {
batch.delete(col)
});
await batch.commit();
// sub-set-a + sub-set-b SECURITY RULES
match /data-set-a/Id}sub-set-a/{subId} {
allow delete: if request.auth != null
&& request.auth.token.email_verified
&& request.auth.uid == Id
}
// sub-set-c SECURITY RULES
match /data-set-c/{Id}/sub-set-c/{subSetId}/masseges/{mgsId} {
allow delete: if request.auth != null
&& request.auth.token.email_verified
&& request.auth.uid == Id
}
the problem is that the (data-set-c) collection never gets deleted.
That's the expected behavior. If you delete a document, doesn't mean that you delete all sub-collections that exist within that document. Besides that, that document ID will continue to exist and the Firebase Console will display it in italic.
will deleting this way affect also sub-collection?
No, it won't.
or is it the rules that are blocking it since my rules are specific to the deepest level endpoint.
If it was about the security rules, then you would have received an error indicating that.
or should use cloud function instead since this doc will be massive later on
Yes, you can indeed use a Cloud Function to delete all documents that exist in all sub-collection of the document that was deleted.
How to delete this one-level nested structure.
You can achieve this by iterating each sub-collection, and by deleting each document. But don't do it on the client!

Firestore - Skip document update if it doesn't exists, without need of failures

I have a collection
/userFeed
Where I create/delete docs (representing users) when the current user starts following/unfollowing them.
...
/userFeed (C)
/some-followed-user (D)
-date <timestamp>
-interactions <number>
When the user likes a post, the interactions field will be updated. But... what if the user doesn't follow the post owner? Then, I will just need to skip the document update, without necessity of producing failures/errors.
const currentUserFeedRef = firestore
.collection("feeds")
.doc(currentUserId)
.collection("userFeed")
.doc(otherUserId);
const data = {
totalInteractions: admin.firestore.FieldValue.increment(value),
};
const precondition = {
exists: false, // I am trying weird things
};
if (batchOrTransaction) {
return batchOrTransaction.update(
currentUserFeedRef,
data,
precondition
);
}
Is it possible to just "skip the update if the doc doesn't exist"?
Is it possible to just "skip the update if the doc doesn't exist"?
No, not in the way that you're explaining it. Firestore updates don't silently fail.
If you need to know if a document exists before updating it, you should simply read it first and check that it exists. You can do this very easily in a transaction, and you can be sure that the update won't fail due to the document being missing if you check it this way first using the transaction object.
In fact, what you are trying to do is illustrated as the very first example in the documentation.

Missing or insufficient permissions firestore although it works on rules play ground

I have added some security rules to my project and tested them in rules playground everything seems to be working perfect but when I try to retrieve the same doc using same UID from my website it throws error: "FirebaseError: Missing or insufficient permissions".
Refrence to collection:
dbRef = db.collection("organization").doc(organization).collection(course).doc(courseId).collection("meetings");
This are the snapshot of my code:
Note: I have already tested the security rules in rules playground with different uids and paths. And it worked perfectly.
dbRef = db.collection("organization").doc(organization).collection(course).doc(courseId).collection("meetings");
This query attempts to fetch all documents under meeting collection.
There is a possibility that the above query will fetch documents where designation != 'Teacher' && courseId != course;
Therefore it will fail.
From firestore rules docs
if you have this rule:
match /stories/{storyid} {
// Only the authenticated user who authored the document can read or write
allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
}
and this request db.collection("stories").get().
The query fails even if the current user actually is the author of every story document. The reason for this behavior is that when Cloud Firestore applies your security rules, it evaluates the query against its potential result set, not against the actual properties of documents in your database. If a query could potentially include documents that violate your security rules, the query will fail.
the following query succeeds, because it includes the same constraint on the author field as the security rules:
var user = firebase.auth().currentUser;
db.collection("stories").where("author", "==", user.uid).get()
NB: playground can only test for document gets (not list collection).
Possible solution.
You can write your query in such a way that it can never return the wrong document.
Something like this. When a student is enrolled to take a meeting (or course), add his uid to that meeting's document.
doc = {student_ids = [uidofstudent1, uidOfStudent2, ...]}.
Then you can query like this:
db
.collection("organization")
.doc(organization)
.collection(course)
.doc(courseId)
.collection("meetings")
.where('student_ids', arrayContains: studentUid);
Your firestore rules can be something like this:
match /organisation/{org}/{course}/{courseId}/meetings/{meet=**} {
allow read: if request.auth.uid in resource.data.student_ids ||
isTeacher(request.auth.uid);
}

Categories

Resources