Firestore listener for sub collections

Viewed 7344

I have my Firestore setup in the following way:

Channels [collection] ----> channelID ---> Messages [collection] ---> messageID

How would I add snapshotListener to sub collection 'Messages' ?

  Firestore.firestore().collection("Channels").document().collection("Messages").addSnapshotListener { (querySnapshot, error) in
        guard let snapshot = querySnapshot else {
            print("Error listening for channel updates: \(error?.localizedDescription ?? "No error")")
            return
        }

        snapshot.documentChanges.forEach { change in 
           print(change)
        }
    }

This didn't work for me

5 Answers

You can't have a single listener receive updates from an unknown number of subcollection. There are no "wildcard" operators for listeners on collections. You have to choose a specific collection or query and attach a listener to that.

As Doug pointed out in his correct answer, you cannot have a single listener receive updates from an unknown (number of or unspecified) subcollection.

However, if you can determine those subcollection names, then the answer is pretty straightforward.

The idea is to read the child nodes of Channels, which will be channel_0, channel_1 etc and use those document id's to build references to the nodes you are interested in listening to.

So given this struture (which matches the structure in the question):

Channels
  channel_0
    Messages
      message_0
        msg: "chan 0 msg 0"
      message_1
        msg: "chan 0 msg 1"
      message_2
        msg: "chan 0 msg 2"
  channel_1
    Messages
      message_0
        msg: "chan 1 msg 0"
      message_1
        msg: "chan 1 msg 1"

Here's the code that adds listeners to each channel, and responds to events within that channels messages notifying in console the message id, msg text and the channel the event occurred in.

func addChannelCollectionObserver() {
    let channelsRef = self.db.collection("Channels")
    channelsRef.getDocuments(completion: { snapshot, error in

        guard let documents = snapshot?.documents else {
            print("Collection was empty")
            return
        }

        for doc in documents {
            let docID = doc.documentID
            let eachChannelRef = channelsRef.document(docID)
            let messagesRef = eachChannelRef.collection("Messages")

            messagesRef.addSnapshotListener { querySnapshot, error in

                querySnapshot?.documentChanges.forEach { diff in
                    if diff.type == .added {
                        let doc = diff.document
                        let msgId = doc.documentID
                        let channelId = messagesRef.parent!.documentID
                        let msg = doc.get("msg") as? String ?? "no message"
                        print(" added msgId: \(msgId) with msg: \(msg) in channel: \(channelId)")
                    }

                    if diff.type == .modified {
                        let doc = diff.document
                        let msgId = doc.documentID
                        let msg = doc.get("msg") as? String ?? "no message"
                        print(" modified msgId: \(msgId) with msg: \(msg)")
                    }

                    if diff.type == .removed {
                        let doc = diff.document
                        let msgId = doc.documentID
                        let msg = doc.get("msg") as? String ?? "no message"
                        print(" removed msgId: \(msgId) with msg: \(msg)")
                    }
                }
            }
        }
    })
}

When first run the output will show, as expected, each child node. From then on it will output any addititions, modifications or deletions.

 added msgId: message_0 with msg: chan 0 msg 0 in channel: channel_0
 added msgId: message_1 with msg: chan 0 msg 1 in channel: channel_0
 added msgId: message_2 with msg: chan 0 msg 2 in channel: channel_0
 added msgId: message_0 with msg: chan 1 msg 0 in channel: channel_1
 added msgId: message_1 with msg: chan 1 msg 1 in channel: channel_1

the code needs some additional error checking as well for some of the optionals but it should provide a solution.

use collection group query for this by listening to all the collections with the same name

    db.collectionGroup("Messages"). docRef.addSnapshotListener(new EventListener<DocumentSnapshot>() {
  @Override
  public void onEvent(@Nullable DocumentSnapshot snapshot,
                      @Nullable FirestoreException e) {
    if (snapshot != null && snapshot.exists()) {
      System.out.println("Current data: " + snapshot.getData());
    } else {
      System.out.print("Current data: null");
    }
  }
});

If you want to subscribe to Firstore subcollection changes in the backend, here is the NodeJS example from Firebase docs:

// Listen for changes in all documents in the 'users' collection and all subcollections
exports.useMultipleWildcards = functions.firestore
    .document('users/{userId}/{messageCollectionId}/{messageId}')
    .onWrite((change, context) => {
      // If we set `/users/marie/incoming_messages/134` to {body: "Hello"} then
      // context.params.userId == "marie";
      // context.params.messageCollectionId == "incoming_messages";
      // context.params.messageId == "134";
      // ... and ...
      // change.after.data() == {body: "Hello"}
    });

Notice that even a subcollection name can be used with wildcards {messageCollectionId}, i.e. we can subscribe to any subsollection of the users collection.

According the question Channels [collection] ----> channelID ---> Messages [collection] ---> messageID the code can be:

exports.onMessagesSubcollectionChange = functions.firestore
    .document('Channels/{channelID}/Messages/{messageID}')
    .onWrite((change, context) => {
   // the rest code
   // context.params.channelID
   // and context.params.messageID will be fulfilled.
}

Not sure whether it is supported on the client side.

I think it is because with

Firestore.firestore().collection("Channels").document().collection("Messages")

you are not defining a correct CollectionReference since you don't identify the document of the "Channels" collection.

You should do:

Firestore.firestore().collection("Channels").document(channelID).collection("Messages")
Related