Introduction
I am adding a chat to my app (without groups, only individuals chats (between 2 users)) where if one user deletes his full chat with other user, both users will not be able to read their previous conversation, likes happens on Telegram.
Database model
With this use case, I don’t need database denormalization at all.
This is my NoSql model:
usernames/ (collection)
jack/ (document)
-userId: "41b932028490"
josh/ (document)
-userId: "212930294348"
users/ (collection)
41b932028490/ (document)
-avatar: {uri}
-username: jack
-...other data
212930294348/ (document)
-avatar: {uri}
-username: "josh"
-...other data
chats/ (collection)
chatId/ (document) <------------ (Some kind of chat room)
-members: ["41b932028490", "212930294348"]
messages/ (sub-collection)
message1Id/ (document)
-userId: "41b932028490" // sender
-type: "text"
-text: "hello world!"
message2Id/ (document)
-userId: "41b932028490" // sender
-type: "GIF"
-uri: "https://media.giphy.com/media/HP7mtfNa1E4CEqNbNL/giphy.gif"
Problem
The problem I am having is with the logic to create the chat rooms. As you can see, I have a field "members", with the ids of both users. What I have thought to do is just making a query and trying to find the room where both users (the message’s sender and the message’s receiver) are members, in the chats collection. If the room exists, I will only have to add the message. If not, I will have to create the room with the new first message.
For example, if a user "Jack" is going to send a first message to the user "Josh" (the chat room doesn’t exist yet), the server will firstly create a chat room (with both users as members), and the new first message as data.
But… what would happen if both users send the message at the same time? (Inconsistencie: 2 chat rooms might be created). To avoid those kinds of inconsistencies, I am running this logic atomically, in a database transaction, like this:
await firestore.runTransaction((transaction) => {
// Does the chat room exists in the chats' collection?
const chatsRef = firestore.collection("chats");
return chatsRef
.where("members", "array-contains", fromId) <--------- PROBLEM!
.where("members", "array-contains", toId) <----------- PROBLEM!
.limit(1)
.get()
.then((snapshot) => {
if (!snapshot.empty) {
// If yes, add the message to the chat room
const chatRoom = snapshot.docs[0];
const chatRoomPath = chatRoom.ref.path;
const newMessageRef = firestore
.doc(chatRoomPath)
.collection("messages")
.doc(); // Auto-generated uuid
const newMessageData = {
userId: fromId,
message,
date: admin.firestore.FieldValue.serverTimestamp(),
};
transaction.set(newMessageRef, newMessageData);
} else {
// TODO - Check that the receiver exists in the database (document read)
// TODO - If exists -> create chat room with the new message
// TODO - If not exists -> throw error
}
// TODO - Increment the chat counter for the receiver when adding the message
});
});
But, the docs says
you can include at most one array-contains or array-contains-any clause in a compound query.
This is where i’m stuck, as I need to find a room with both users as members… and the only other way I can think of is to perform two queries (an OR)
where("member1", "==", fromId).where("memeber2", "==", toId)
// and
where("member1", "==", toId).where("memeber2", "==", fromId)
How can I make this work with a single query? Any ideas?
Thank you.
2
Answers
Instead of using an array, I have used a map where the key is the user's id, and the value a true boolean.
Then I query like:
I suggest you create an extra field in the chats document with for example a concatenation of both user ids. You can do it in the same transaction of the chat room creation or with a firebase function.
This would simplify the query since you just search for this new field:
It took me a while to accept these kind of extra fields but it really avoid a lot of struggling with limitations of firestore queries