How to use Multi WhereNotIn in fireStore with Kotlin? - firebase

I need to use multi WhereNotIn as conditions in fireStore.
But fireStore only supplies single WhereNotIn.
How to make it possible with multi condition?
var storeBuilder: Query?
userDB.document("$userId")
.get()
.addOnSuccessListener { document ->
var blockedUser =
document.data?.getValue("blockedUser") as ArrayList<*>
var blockedDiary =
document.data?.getValue("blockedDiary") as ArrayList<*>
if (blockedUser.isNotEmpty() && blockedDiary.isNotEmpty()) {
storeBuilder = diaryDB
.whereNotIn("diaryId", blockedDiary)
.whereNotIn("userId", blockedUser)
} else if (blockedUser.isNotEmpty() && blockedDiary.isEmpty()) {
storeBuilder = diaryDB
.whereNotIn("userId", blockedUser)
} else if (blockedUser.isEmpty() && blockedDiary.isNotEmpty()) {
storeBuilder = diaryDB
.whereNotIn("diaryId", blockedDiary)
} else {
storeBuilder = diaryDB
}
This line is problem among above:
storeBuilder = diaryDB
.whereNotIn("diaryId", blockedDiary)
.whereNotIn("userId", blockedUser)

As mentioned in the documentation, you can you at most one in, not-in or array-contains-any per query. The multiple in case may be solved by running multiple query but that's not the case with not-in as one query may return results that were not included by other.
The best workaround would be to use a service like Algolia that supports such queries and facet filters. Also checkout Firestore Algolia Extension that might be useful in syncing the data.

Related

Why my rule in firebase database is not working?

I'm trying to add a rule that automatically merges two users if the user already exist with the same email and just keep one of them with new user newest data.
match /users/{userId} {
allow create: if request.resource.data.email != null;
allow update: if request.resource.data.email != null && request.auth.uid == userId;
function isDuplicateEmail() {
return get(/databases/$(database)/documents/users/$(request.resource.data.email)).exists;
}
function mergeUsers(userId) {
// Get the data of the new user
let newUser = get(/databases/$(database)/documents/users/$(userId)).data;
// Get the data of the existing user
let existingUser = get(/databases/$(database)/documents/users/$(newUser.email)).data;
// Merge the data from the two users
let mergedData = {...existingUser, ...newUser};
// Update the data of the existing user
return update(/databases/$(database)/documents/users/$(newUser.email), mergedData);
}
allow create: if !isDuplicateEmail()
allow create: if isDuplicateEmail() && mergeUsers(userId);
}
But I'm seeing an error in the rule editor: "Unexpected "}". Line 40:
let mergedData = {...existingUser, ...newUser};
What I'm missing?
Thanks.
The security rules expression language does not support the ... spread operator like JavaScript. In fact, it is not JavaScript at all - it just looks a bit like JS. You might want to read about its syntax in the documentation.
On top of that, there is no function called update. You can't modify data in security rules at all. You can only check to see if the incoming access should be allowed or denied. If you want to modify document data, you will have to write application or backend code for that.
The } is closing the match statement before the allow create statement that uses the mergeUsers() function. Try:
match /users/{userId} {
allow create: if request.resource.data.email != null;
allow update: if request.resource.data.email != null && request.auth.uid == userId;
function isDuplicateEmail() {
return get(/databases/$(database)/documents/users/$(request.resource.data.email)).exists;
}
function mergeUsers(userId) {
// Get the data of the new user
let newUser = get(/databases/$(database)/documents/users/$(userId)).data;
// Get the data of the existing user
let existingUser = get(/databases/$(database)/documents/users/$(newUser.email)).data;
// Merge the data from the two users
let mergedData = {...existingUser, ...newUser};
// Update the data of the existing user
return update(/databases/$(database)/documents/users/$(newUser.email), mergedData);
}
allow create: if !isDuplicateEmail()
allow create: if isDuplicateEmail() && mergeUsers(userId);
}
Also, if you going to use the update function, you also need to include a rule allowing the update to happen.

Firebase firestore security rule for collectionGroup query

I am trying to query and filter a collectionGroup from the client doing this:
const document = doc(db, 'forums/foo');
const posts = await getDocs(
query(
collectionGroup(db, 'posts'),
orderBy(documentId()),
startAt(document.path),
endAt(document.path + '\uf8ff')
)
);
My auth custom user claims looks like this:
{ forumIds: ['foo'] }
The documentation tells me to add the following security rule:
match /{path=**}/posts/{post} {
allow read: if request.auth != null;
}
But this is a security breach as it means that anyone can read all of the posts collections. I only want the user to read the posts in its forums. Is there no better way to secure a collectionGroup query?
(1) I have tried:
match /{path=**}/posts/{post} {
allow read: if path[1] in request.auth.token.forumIds;
}
but I get this error: Variable is not bound in path template. for 'list' # L49.
(2) I have also tried:
match /{path=**}/posts/{post} {
allow read: if resource.__name__[4] in request.auth.token.forumIds;
}
but I get this error: Property __name__ is undefined on object. for 'list' # L49.
I have also tried debugging the two previous security rules with debug and both of them return true.
Based on your stated requirements, you don't want a collection group query at all. A collection group query intends to fetch all of the documents in all of the named collections. You can only filter the results based on the contents of the document like you would any other query.
Since you have a list of forums that the user should be able to read, you should just query them each individually and combine the results in the app. Security rules are not going to be able to filter them out for you because security rules are not filters.
See also:
https://medium.com/firebase-developers/what-does-it-mean-that-firestore-security-rules-are-not-filters-68ec14f3d003
https://firebase.google.com/docs/firestore/security/rules-query#rules_are_not_filters

How do I query ordering documents by multiple computed values in Firestore?

Let's I had documents contain two fields 'likeCount' and 'viewCount'. Is it possible to order them by a sum of those two fields?
If not, how do I implement ordering by popularity? Ordering by popularity is a common feature in a variety of apps but I can't find any documentation or tutorials for this.
One solution is to have an extra field in each document which holds the value of the sum. Then you can easily sort based on this value.
To update this field, you can either do it when you update the document from your app, OR, if you don't have the info in the app when you update the doc (i.e. it would require fetching the document to get the value of the two fields) you can update the value in the backend, with a Cloud Function.
For example, a Cloud Function along the following lines would do the trick:
exports.updateGlobalCount = functions.firestore
.document('collection/{docId}')
.onUpdate((change, context) => {
const previousValue = change.before.data();
const newValue = change.after.data();
if (previousValue.likeCount !== newValue.likeCount || previousValue.viewCount !== newValue.viewCount) {
return change.after.ref.update({ globalCount: newValue.likeCount + newValue.viewCount })
} else {
return null;
}
});

What's the best way to paginate and filters large set of data in Firebase?

I have a large Firestore collection with 10,000 documents.
I want to show these documents in a table by paging and filtering the results at 25 at a time.
My idea, to limit the "reads" (and therefore the costs), was to request only 25 documents at a time (using the 'limit' method), and to load the next 25 documents at the page change.
But there's a problem. In order to show the number of pages I have to know the total number of documents and I would be forced to query all the documents to find that number.
I could opt for an infinite scroll, but even in this case I would never know the total number of results that my filter has found.
Another option would be to request all documents at the beginning and then paging and filtering using the client.
so, what is the best way to show data in this type of situation by optimizing performance and costs?
Thanks!
You will find in the Firestore documentation a page dedicated to Paginating data with query cursors.
I paste here the example which "combines query cursors with the limit() method".
var first = db.collection("cities")
.orderBy("population")
.limit(25);
return first.get().then(function (documentSnapshots) {
// Get the last visible document
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
console.log("last", lastVisible);
// Construct a new query starting at this document,
// get the next 25 cities.
var next = db.collection("cities")
.orderBy("population")
.startAfter(lastVisible)
.limit(25);
});
If you opt for an infinite scroll, you can easily know if you have reached the end of the collection by looking at the value of documentSnapshots.size. If it is under 25 (the value used in the example), you know that you have reached the end of the collection.
If you want to show the total number of documents in the collection, the best is to use a distributed counter which holds the number of documents, as explained in this answer: https://stackoverflow.com/a/61250956/3371862
Firestore does not provide a way to know how many results would be returned by a query without actually executing the query and reading each document. If you need a total count, you will have to somehow track that yourself in another document. There are plenty of suggestions on Stack Overflow about counting documents in collections.
Cloud Firestore collection count
How to get a count of number of documents in a collection with Cloud Firestore
However, the paging API itself will not help you. You need to track it on your own, which is just not very easy, especially for flexible queries that could have any number of filters.
My guess is you would be using Mat-Paginator and the next button is disabled because you cannot specify the exact length? In that case or not, a simple workaround for this is to get (pageSize +1) documents each time from the Firestore sorted by a field (such as createdAt), so that after a new page is loaded, you will always have one document in the next page which will enable the "next" button on the paginator.
What worked best for me:
Create a simple query
Create a simple pagination query
Combine both (after validating each one works separately)
Simple Pagination Query
const queryHandler = query(
db.collection('YOUR-COLLECTION-NAME'),
orderBy('ORDER-FIELD-GOES-HERE'),
startAt(0),
limit(50)
)
const result = await getDocs(queryHandler)
which will return the first 50 results (ordered by your criteria)
Simple Query
const queryHandler = query(
db.collection('YOUR-COLLECTION-NAME'),
where('FIELD-NAME', 'OPERATOR', 'VALUE')
)
const result = await getDocs(queryHandler)
Note that the result object has both query field (with relevant query) and docs field (to populate actual data)
So... combining both will result with:
const queryHandler = query(
db.collection('YOUR-COLLECTION-NAME'),
where('FIELD-NAME', 'OPERATOR', 'VALUE'),
orderBy('FIELD-NAME'),
startAt(0),
limit(50)
)
const result = await getDocs(queryHandler)
Please note that the field in the where clause and in orderBy must be the same one! Also, it is worth mentioning that you may be required to create an index (for some use cases) or that this operation will fail while using equality operators and so on.
My tip: inspect the error itself where you will find a detailed description describing why the operation failed and what should be done in order to fix it (see an example output using js client in image below)
Firebase V9 functional approach. Don't forget to enable persistence so you won't get huge bills. Don't forget to use where() function if some documents have restrictions in rules. Firestore will throw error if even one document is not allowed to read by user. In case bellow documents has to have isPublic = true.
firebase.ts
function paginatedCollection(collectionPath: string, initDocumentsLimit: number, initQueryConstraint: QueryConstraint[]) {
const data = vueRef<any[]>([]) // Vue 3 Ref<T> object You can change it to even simple array.
let snapshot: QuerySnapshot<DocumentData>
let firstDoc: QueryDocumentSnapshot<DocumentData>
let unSubSnap: Unsubscribe
let docsLimit: number = initDocumentsLimit
let queryConst: QueryConstraint[] = initQueryConstraint
const onPagination = (option?: "endBefore" | "startAfter" | "startAt") => {
if (option && !snapshot) throw new Error("Your first onPagination invoked function has to have no arguments.")
let que = queryConst
option === "endBefore" ? que = [...que, limitToLast(docsLimit), endBefore(snapshot.docs[0])] : que = [...que, limit(docsLimit)]
if (option === "startAfter") que = [...que, startAfter(snapshot.docs[snapshot.docs.length - 1])]
if (option === "startAt") que = [...que, startAt(snapshot.docs[0])]
const q = query(collection(db, collectionPath), ...que)
const unSubscribtion = onSnapshot(q, snap => {
if (!snap.empty && !option) { firstDoc = snap.docs[0] }
if (option === "endBefore") {
const firstDocInSnap = JSON.stringify(snap.docs[0])
const firstSaved = JSON.stringify(firstDoc)
if (firstDocInSnap === firstSaved || snap.empty || snap.docs.length < docsLimit) {
return onPagination()
}
}
if (option === "startAfter" && snap.empty) {
onPagination("startAt")
}
if (!snap.empty) {
snapshot = snap
data.value = []
snap.forEach(docSnap => {
const doc = docSnap.data()
doc.id = docSnap.id
data.value = [...data.value, doc]
})
}
})
if (unSubSnap) unSubSnap()
unSubSnap = unSubscribtion
}
function setLimit(documentsLimit: number) {
docsLimit = documentsLimit
}
function setQueryConstraint(queryConstraint: QueryConstraint[]) {
queryConst = queryConstraint
}
function unSub() {
if (unSubSnap) unSubSnap()
}
return { data, onPagination, unSub, setLimit, setQueryConstraint }
}
export { paginatedCollection }
How to use example in Vue 3 in TypeScript
const { data, onPagination, unSub } = paginatedCollection("posts", 8, [where("isPublic", "==", true), where("category", "==", "Blog"), orderBy("createdAt", "desc")])
onMounted(() => onPagination()) // Lifecycle function
onUnmounted(() => unSub()) // Lifecycle function
function next() {
onPagination('startAfter')
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function prev() {
onPagination('endBefore')
window.scrollTo({ top: 0, behavior: 'smooth' })
}
You might have problem with knowing which document is last one for example to disable button.

Update collection with an array in firebase

I need to update a collection in values like this :
{
"email" : "x#gmail.com",
"fullName" : "Mehr",
"locations" : ["sss","dsds","adsdsd"]
}
Locations needs to be an array. in firebase how can I do that ... and also it should check duplicated.
I did like this :
const locations=[]
locations.push(id)
firebase.database().ref(`/users/ + ${userId}`).push({ locations })
Since you need to check for duplicates, you'll need to first read the value of the array, and then update it. In the Firebase Realtime Database that combination can is done through a transaction. You can run the transaction on the locations node itself here:
var locationsRef = firebase.database().ref(`/users/${userId}/locations`);
var newLocation = "xyz";
locationsRef.transaction(function(locations) {
if (locations) {
if (locations.indexOf(newLocation) === -1) {
locations.push(newLocation);
}
}
return locations;
});
As you can see, this loads the locations, ensures the new location is present once, and then writes it back to the database.
Note that Firebase recommends using arrays for set-like data structures such as this. Consider using the more direct mapping of a mathematical set to JavaScript:
"locations" : {
"sss": true,
"dsds": true,
"adsdsd": true
}
One advantage of this structure is that adding a new value is an idempotent operation. Say that we have a location "sss". We add that to the location with:
locations["sss"] = true;
Now there are two options:
"sss" was not yet in the node, in which case this operation adds it.
"sss" was already in the node, in which case this operation does nothing.
For more on this, see best practices for arrays in Firebase.
you can simply push the items in a loop:
if(locations.length > 0) {
var ref = firebase.database().ref(`/users/ + ${userId}`).child('locations');
for(i=0; i < locations.length; i++) {
ref.push(locations[i]);
}
}
this also creates unique keys for the items, instead of a numerical index (which tends to change).
You can use update rather than push method. It would much easier for you. Try it like below
var locationsObj={};
if(locations.length > 0) {
for(i=0; i < locations.length; i++) {
var key= firebase.database().ref(`/users/ + ${userId}`).child('locations').push().key;
locationsObj[`/users/ + ${userId}` +'/locations/' + key] =locations[i];
}
firebase.database().ref().update(locationsObj).then(function(){// which return the promise.
console.log("successfully updated");
})
}
Note : update method is used to update multiple paths at a same time. which will be helpful in this case, but if you use push in the loop then you have to wait for the all the push to return the promises. In the update method it will take care of the all promises and returns at once. Either you get success or error.

Resources