Firebase : query all the chat rooms my user is involved in - firebase

I've got a Collection message which contains both a document (representing the chat room) and a nested collection which contains all the messages of that conversation.
Now I'd like to request all the documents (chat rooms) in which my user is involved. So if one of the id1 or id2 fields in the users map is equal to my user id, I collect that document.
I've noticed that I can't use array queries as I'm using maps and not arrays.
So I don't know what would be the best approach to proceed to that query.
Stream<List<ChatRoom>> getChatRooms(String userId) {
final messageCollection = FirebaseFirestore.instance.collection('messages');
// var query = messagesCollection.where('users' in [userId])

See Frank's comment below: The best solution is to "use an array of user IDs (userIDs: ["ABCDEF", "GHIJKL"]) and an array-contains condition. In this use-case it seems that would save on the number of needed indexes (and thus on the cost of storage)".
If you really need to keep the map for other reasons, you can very well have the two fields in the doc. It's not a problem to duplicate the data.
If the value you assign to each userId in the map does not have to be meaningful, you can assign a Boolean value of true and then query as follows:
final messageCollection = FirebaseFirestore.instance.collection('messages');
messageCollection.where('users.id1', isEqualTo: true)
.get()
.then(...);
So, your map will look like:
users
id1: true
id2: true
Clarification:
The idea is to use the user ids as keys and have a value of true. Let's imagine two users with the following ids: "ABCDEF" and "GHIJKL"
Instead of having a map like
users
id1: "ABCDEF"
id2: "GHIJKL"
you could have it as follows:
users
ABCDEF: true
GHIJKL: true
Note that you could very well have the two maps in the doc, if for some reason you really need to keep the first map.

Related

How to get document with certain fields from firebase to flutter?

I need a flutter query for getting data from firebase;
The query should go to User collection and search every document with UserType = 1 field. There is a "classes" collection in each document and each of "classes" have other documents such as "math", "music" etc. I need to get the document with the field classCode == myInput
QuerySnapshot query1 = await _fireStore.collection("Users").where(["userType"], isEqualTo: 1).get();
I don't know how to move from here.
You have two main options for implement this.
The first is pretty close to how you describe in your question:
Find the user docs for your condition,
For each of those search the classes subcollection for the classCode.
The second approach makes use of a collection group query to:
Find all classes docs (across all the subcollections) with the searched user type.
Possibly then look up the users for those.
The best approach depends on mostly on what results you need. If you results are user documents, the first approach is usually pretty easy. If the results are classes then I'd definitely try the second approach first.

Is it possible to fetch all documents whose sub-collection contains a specific document ID?

I am trying to fetch all documents whose sub-collection contain a specific document ID. Is there any way to do this?
For example, if the boxed document under 'enquiries' sub-collection exists, then I need the boxed document ID from 'books' collection. I couldn't figure out how to go backwards to get the parent document ID.
I make the assumption that all the sub-collections have the same name, i.e. enquiries. Then, you could do as follows:
Add a field docId in your enquiries document that contains the document ID.
Execute a Collection Group query in order to get all the documents with the desired docId value (Firestore.instance.collectionGroup("enquiries").where("docId", isEqualTo: "ykXB...").getDocuments()).
Then, you loop over the results of the query and for each DocumentReference you call twice the parent() methods (first time you will get the CollectionReference and second time you will get the DocumentReference of the parent document).
You just have to use the id property and you are done.
Try the following:
Firestore.instance.collection("books").where("author", isEqualTo: "Arumugam").getDocuments().then((value) {
value.documents.forEach((result) {
var id = result.documentID;
Firestore.instance.collection("books").document(id).collection("enquiries").getDocuments().then((querySnapshot) {
querySnapshot.documents.forEach((result) {
print(result.data);
});
First you need to retrieve the id under the books collection, to be able to do that you have to do a query for example where("author", isEqualTo: "Arumugam"). After retrieving the id you can then do a query to retrieve the documents inside the collection enquiries
For example, if the boxed document under 'enquiries' sub-collection exists, then I need the boxed document ID from 'books' collection.
There is no way you can do that in a single go.
I couldn't figure out how to go backwards to get the parent document ID.
There is no going back in Firestore as you probably were thinking. In Firebase Realtime Database we have a method named getParent(), which does exactly what you want but in Firestore we don't.
Queries in Firestore are shallow, meaning that it only get items from the collection that the query is run against. Firestore doesn't support queries across different collections in one go. A single query may only use the properties of documents in a single collection. So the solution to solving your problem is to perform two get() calls. The first one would be to check that document for existence in the enquiries subcollection, and if it exists, simply create another get() call to get the document from the books collection.
Renaud Tarnec's answer is great for fetching the IDs of the relevant books.
If you need to fetch more than the ID, there is a trick you could use in some scenarios. I imagine your goal is to show some sort of an index of all books associated with a particular enquiry ID. If the data you'd like to show in that index is not too long (can be serialized in less than 1500 bytes) and if it is not changing frequently, you could try to use the document ID as the placeholder for that data.
For example, let's say you wanted to display a list of book titles and authors corresponding to some enquiryId. You could create the book ID in the collection with something like so:
// Assuming admin SDK
const bookId = nanoid();
const author = 'Brandon Sanderson';
const title = 'Mistborn: The Final Empire';
// If title + author are not unique, you could add the bookId to the array
const uniquePayloadKey = Buffer.from(JSON.stringify([author, title])).toString('base64url');
booksColRef.doc(uniquePayloadKey).set({ bookId })
booksColRef.doc(uniquePayloadKey).collection('enquiries').doc(enquiryId).set({ enquiryId })
Then, after running the collection group query per Renaud Tarnec's answer, you could extract that serialized information with a regexp on the path, and deserialize. E.g.:
// Assuming Web 9 SDK
const books = query(collectionGroup(db, 'enquiries'), where('enquiryId', '==', enquiryId));
return getDocs(books).then(snapshot => {
const data = []
snapshot.forEach(doc => {
const payload = doc.ref.path.match(/books\/(.*)\/enquiries/)[1];
const [author, title] = JSON.parse(atob(details));
data.push({ author, title })
});
return data;
});
The "store payload in ID" trick can be used only to present some basic information for your child-driven search results. If your book document has a lot of information you'd like to display once the user clicks on one of the books returned by the enquiry, you may want to store this in separate documents whose IDs are the real bookIds. The bookId field added under the unique payload key allows such lookups when necessary.
You can reuse the same data structure for returning book results from different starting points, not just enquiries, without duplicating this structure. If you stored many authors per book, for example, you could add an authors sub-collection to search by. As long as the information you want to display in the resulting index page is the same and can be serialized within the 1500-byte limit, you should be good.
The (quite substantial) downside of this approach is that it is not possible to rename document IDs in Firestore. If some of the details in the payload change (e.g. an admin fixes a book titles), you will need to create all the sub-collections under it and delete the old data. This can be quite costly - at least 1 read, 1 write, and 1 delete for every document in every sub-collection. So keep in mind it may not be pragmatic for fast changing data.
The 1500-byte limit for key names is documented in Usage and Limits.
If you are concerned about potential hotspots this can generate per Best Practices for Cloud Firestore, I imagine that adding the bookId as a prefix to the uniquePayloadKey (with a delimiter that allows you to throw it away) would do the trick - but I am not certain.

Order by on missing field constrains result

I'm running into an issue with Firestore that is pretty unintuitive to me and so I'm wondering if anyone can help me understand why Firestore is giving this result.
Let's say I have a collection called "people" with fields "firstName" and "lastName". Let's say I have 20 documents in that collection that have the lastName "Quaid". I then have a field "inCanada" that is only present in a subset of those 20 documents, let's say 5. The other 15 documents don't have that field.
What is surprising is that an orderBy clause using that optional field is actually filtering the result set rather than just sorting it, which doesn't make sense to me when compared with other databases.
In the below example, I expected both result sets to have 20 elements, however the second set that has the orderBy only has those 5 documents where that field is present.
Could someone please explain why Firestore does this?
const Admin = require('firebase-admin');
(async function () {
const initFirebase = require('../initFirebase');
initFirebase();
const people = Admin.firestore().collection('people');
const quaids = people.where('lastName', '==', 'Quaid')
const quaids1 = await quaids.get();
const quaids2 = await quaids.orderBy('inCanada', 'desc').get();
console.log(quaids1._size); // 20
console.log(quaids2._size); // 5
})();
In a nutshell, this is because Firestore queries are based on indexes: each time you create a doc Firestore creates an index for each field of the doc.
So since the field "inCanada" is only present "in a subset of the 20 documents", only this subset of documents is present in the "inCanada" index, resulting in the situation you mention.
This official video from Todd Kerpelman from the Firebase team explains it very well: https://www.youtube.com/watch?v=Ofux_4c94FI#t=4m17s. (This link will open the video at 4m17s i.e. when the explanations on the index mechanism start. However the part on the impact of indexes on the querying possibilities is more around 6m22s!)
If you want to include the other documents in your query results you should write a value for the field "inCanada" for those documents, for example use a false value (and use true for the docs that are "inCanada").

How to query for matches between users in cloud firestore?

I have a following collections
-likes (collection)
-{uid} (document)
{otheruserUID: true, anotherUID: true ...}
-likedBy (collection)
-{uid} (document)
{otheruserUID: true, anotherUID: true ...}
A user can like other users. What I want to query for is given a user, query for all matches of that user. Should I query whole likes and likedby data and run match in result and produce match results? Is there any other easy way to do this? Or may be better way to model the data?
Personally, I would simply have a single collection, called likes. Each like generates a new document with an auto-id and contains 3 fields: user (an object containing the id and name of the user), likedBy (an object containing the id and name of the user who liked them) and timestamp (when they were liked).
You'll be able to carry out the following queries:
// Find all users who liked likedUser, sorted by user
db.collection('likes').where('likedBy.name', '!=', null).where('user.id', '==', likedUser).orderBy('likedBy.name');
// Find all users who were liked by likedByUser, sorted by user
db.collection('likes').where('user.name', '!=', null).where('likedBy.id', '==', likedByUser).orderBy('user.name');
The first time that you run these queries, you will get an error, telling you to create an index. This error will include the URL to create the index for you.
The first where is required to allow the orderBy to work, see the documentation section Range filter and orderBy on the same field

Cloud Firestore: Query secured documents by field content

EDIT: Seems to be an open issue in Firestore. Also see this post.
In Google Cloud Firestore, I want to model a collection of groups. Each group contains a name, the list of it's users and some secretGroupData. For me, the natural way to do this would be:
/groups
/group1 {
name: "Group 1"
users: { //object can be queried, simple array not
"user1": true,
"user5": true
}
secretGroupData: ...
}
/group2 { ... }
Given a user like user1, I want to query all groups he is member of. This query works fine:
groupsRef.where("users.user1", "==", true)
However, I want to secure the group data. This query only works, when all groups are readable for all users. When I protect the group to be readable only by the group members, by the rule
match /groups/{groupId} {
allow read: if resource.data.users[request.auth.uid] == true;
}
the above query does not work any more, because as soon as it sees a group where the current user is not a member of, read access is denied and the whole query fails.
What is the best solution for this problem in Firestore? Should I
tell Firestore to return only the allowed groups and ignore the other ones, instead of throwing an error? If so, how can I achieve this?
make the groups readable for all users and move the secretGroupData into subcollections, where I can then restrict the access to just the group members
add redundancy by adding the IDs of all groups of a user into the user's profile document (/users/user1/groupIds: ["group1"]), so I know the groups beforehand and can query them by ID
use a totally different solution?
Thank you very much for your ideas.

Resources